From e425d825bcbc9729a41fc97a8265cdbc5f39d9b2 Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Tue, 14 Mar 2023 19:45:06 +0700 Subject: [PATCH] feat: custom Vendor (#215) * feat: custom Vendor * feat: custom Vendor * fix: application with custom tts/stt vendor * fix custom speech name when editing * fix: all comments * fix: remove custom in the list and show extra (custom) * fix: prettier and application sythesizer selector * fix: addd VITE_DISABLE_CUSTOM_SPEECH --------- Co-authored-by: Quan HL --- src/api/constants.ts | 5 + src/api/types.ts | 3 + .../internal/views/applications/form.tsx | 220 +++++++++++++----- .../internal/views/speech-services/form.tsx | 172 ++++++++++++-- .../internal/views/speech-services/index.tsx | 10 +- src/vendor/index.tsx | 5 + src/vendor/types.ts | 3 +- 7 files changed, 342 insertions(+), 76 deletions(-) diff --git a/src/api/constants.ts b/src/api/constants.ts index 87c32e8..73fbe21 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -28,6 +28,11 @@ export const API_BASE_URL = /** Serves mock API responses from a local dev API server */ export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL; +/** Disable custom speech vendor*/ +export const DISABLE_CUSTOM_SPEECH: boolean = JSON.parse( + import.meta.env.VITE_DISABLE_CUSTOM_SPEECH || "false" +); + /** TCP Max Port */ export const TCP_MAX_PORT = 65535; diff --git a/src/api/types.ts b/src/api/types.ts index 2124ac1..4d05d51 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -309,6 +309,9 @@ export interface SpeechCredential { stt_region: null | string; instance_id: null | string; riva_server_uri: null | string; + auth_token: null | string; + custom_stt_url: null | string; + custom_tts_url: null | string; } export interface Alert { diff --git a/src/containers/internal/views/applications/form.tsx b/src/containers/internal/views/applications/form.tsx index 01cb084..dc6828b 100644 --- a/src/containers/internal/views/applications/form.tsx +++ b/src/containers/internal/views/applications/form.tsx @@ -21,6 +21,7 @@ import { useSpeechVendors, VENDOR_DEEPGRAM, VENDOR_SONIOX, + VENDOR_CUSTOM, } from "src/vendor"; import { postApplication, @@ -40,6 +41,7 @@ import type { Voice, VoiceLanguage, Language, + VendorOptions, } from "src/vendor/types"; import type { @@ -48,9 +50,10 @@ import type { Application, WebhookMethod, UseApiDataMap, + SpeechCredential, } from "src/api/types"; import { MSG_REQUIRED_FIELDS, MSG_WEBHOOK_FIELDS } from "src/constants"; -import { isUserAccountScope, useRedirect } from "src/utils"; +import { hasLength, isUserAccountScope, useRedirect } from "src/utils"; import { setAccountFilter, setLocation } from "src/store/localStore"; type ApplicationFormProps = { @@ -83,6 +86,10 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { useState(VENDOR_GOOGLE); const [recogLang, setRecogLang] = useState(LANG_EN_US); const [message, setMessage] = useState(""); + const [apiUrl, setApiUrl] = useState(""); + const [credentials] = useApiData(apiUrl); + const [softTtsVendor, setSoftTtsVendor] = useState(vendors); + const [softSttVendor, setSoftSttVendor] = useState(vendors); /** This lets us map and render the same UI for each... */ const webhooks = [ @@ -186,6 +193,40 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { } }; + useEffect(() => { + if (credentials && hasLength(credentials)) { + const v = credentials + .filter((tv) => tv.vendor.startsWith(VENDOR_CUSTOM) && tv.use_for_tts) + .map((tv) => + Object.assign({ + name: + tv.vendor.substring(VENDOR_CUSTOM.length + 1) + + ` (${VENDOR_CUSTOM})`, + value: tv.vendor, + }) + ); + setSoftTtsVendor(vendors.concat(v)); + + const v2 = credentials + .filter((tv) => tv.vendor.startsWith(VENDOR_CUSTOM) && tv.use_for_stt) + .map((tv) => + Object.assign({ + name: + tv.vendor.substring(VENDOR_CUSTOM.length + 1) + + ` (${VENDOR_CUSTOM})`, + value: tv.vendor, + }) + ); + setSoftSttVendor(vendors.concat(v2)); + } + }, [credentials]); + + useEffect(() => { + if (accountSid) { + setApiUrl(`Accounts/${accountSid}/SpeechCredentials`); + } + }, [accountSid]); + useEffect(() => { setLocation(); if (application && application.data) { @@ -382,15 +423,22 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { id="synthesis_vendor" name="synthesis_vendor" value={synthVendor} - options={vendors.filter( + options={softTtsVendor.filter( (vendor) => vendor.value != VENDOR_DEEPGRAM && - vendor.value != VENDOR_SONIOX + vendor.value != VENDOR_SONIOX && + vendor.value !== VENDOR_CUSTOM )} onChange={(e) => { const vendor = e.target.value as keyof SynthesisVendors; setSynthVendor(vendor); + /** When Custom Vendor is used, user you have to input the lange and voice. */ + if (vendor.toString().startsWith(VENDOR_CUSTOM)) { + setSynthVoice(""); + return; + } + /** When using Google and en-US, ensure "Standard-C" is used as default */ if ( e.target.value === VENDOR_GOOGLE && @@ -419,55 +467,88 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { setSynthVoice(newLang!.voices[0].value); }} /> - {synthVendor && synthLang && ( - <> - - ({ + {synthVendor && + !synthVendor.toString().startsWith(VENDOR_CUSTOM) && + synthLang && ( + <> + + ({ name: lang.name, value: lang.code, - }) - )} - onChange={(e) => { - const language = e.target.value; - setSynthLang(language); + }))} + onChange={(e) => { + const language = e.target.value; + setSynthLang(language); - /** When using Google and en-US, ensure "Standard-C" is used as default */ - if ( - synthVendor === VENDOR_GOOGLE && - language === LANG_EN_US - ) { - setSynthVoice(LANG_EN_US_STANDARD_C); - return; + /** When using Google and en-US, ensure "Standard-C" is used as default */ + if ( + synthVendor === VENDOR_GOOGLE && + language === LANG_EN_US + ) { + setSynthVoice(LANG_EN_US_STANDARD_C); + return; + } + + const newLang = synthesis[ + synthVendor as keyof SynthesisVendors + ].find((lang) => lang.code === language); + + setSynthVoice(newLang!.voices[0].value); + }} + /> + + lang.code === synthLang + ) + .flatMap((lang: VoiceLanguage) => + lang.voices.map((voice: Voice) => ({ + name: voice.name, + value: voice.value, + })) + ) as Voice[] } - - const newLang = synthesis[ - synthVendor as keyof SynthesisVendors - ].find((lang) => lang.code === language); - - setSynthVoice(newLang!.voices[0].value); + onChange={(e) => setSynthVoice(e.target.value)} + /> + + )} + {synthVendor.toString().startsWith(VENDOR_CUSTOM) && ( + <> + + { + setSynthLang(e.target.value); }} /> - - Voice + lang.code === synthLang) - .flatMap((lang: VoiceLanguage) => - lang.voices.map((voice: Voice) => ({ - name: voice.name, - value: voice.value, - })) - ) as Voice[] - } - onChange={(e) => setSynthVoice(e.target.value)} + onChange={(e) => { + setSynthVoice(e.target.value); + }} /> )} @@ -480,13 +561,18 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { id="recognizer_vendor" name="recognizer_vendor" value={recogVendor} - options={vendors.filter( - (vendor) => vendor.value != VENDOR_WELLSAID + options={softSttVendor.filter( + (vendor) => + vendor.value != VENDOR_WELLSAID && + vendor.value !== VENDOR_CUSTOM )} onChange={(e) => { const vendor = e.target.value as keyof RecognizerVendors; setRecogVendor(vendor); + /**When vendor is custom, Language is input by user */ + if (vendor.toString() === VENDOR_CUSTOM) return; + /** Google and AWS have different language lists */ /** If the new language doesn't map then default to "en-US" */ const newLang = recognizers[vendor].find( @@ -501,19 +587,37 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => { } }} /> - {recogVendor && recogLang && ( + {recogVendor && + !recogVendor.toString().startsWith(VENDOR_CUSTOM) && + recogLang && ( + <> + + ({ + name: lang.name, + value: lang.code, + }))} + onChange={(e) => { + setRecogLang(e.target.value); + }} + /> + + )} + {recogVendor.toString().startsWith(VENDOR_CUSTOM) && ( <> - - Language + ({ - name: lang.name, - value: lang.code, - }))} onChange={(e) => { setRecogLang(e.target.value); }} diff --git a/src/containers/internal/views/speech-services/form.tsx b/src/containers/internal/views/speech-services/form.tsx index 84fc22b..e4f9fb9 100644 --- a/src/containers/internal/views/speech-services/form.tsx +++ b/src/containers/internal/views/speech-services/form.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { Button, ButtonGroup, MS } from "@jambonz/ui-kit"; import { Link, useNavigate } from "react-router-dom"; @@ -9,6 +9,7 @@ import { Selector, Passwd, AccountSelect, + Checkzone, } from "src/components/forms"; import { toastError, toastSuccess, useSelectState } from "src/store"; import { @@ -28,6 +29,7 @@ import { VENDOR_IBM, VENDOR_NVIDIA, VENDOR_SONIOX, + VENDOR_CUSTOM, } from "src/vendor"; import { MSG_REQUIRED_FIELDS } from "src/constants"; import { @@ -41,6 +43,7 @@ import { CredentialStatus } from "./status"; import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types"; import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types"; import { setAccountFilter, setLocation } from "src/store/localStore"; +import { DISABLE_CUSTOM_SPEECH } from "src/api/constants"; type SpeechServiceFormProps = { credential?: UseApiDataMap; @@ -53,7 +56,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { const regions = useRegionVendors(); const [accounts] = useServiceProviderData("Accounts"); const [accountSid, setAccountSid] = useState(""); + const [initialTtsCheck, setInitialTtsCheck] = useState(false); const [ttsCheck, setTtsCheck] = useState(false); + const [initialSttCheck, setInitialSttCheck] = useState(false); const [sttCheck, setSttCheck] = useState(false); const [vendor, setVendor] = useState>( "" as Lowercase @@ -78,6 +83,12 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { const [customSttEndpoint, setCustomSttEndpoint] = useState(""); const [tmpCustomSttEndpoint, setTmpCustomSttEndpoint] = useState(""); const [rivaServerUri, setRivaServerUri] = useState(""); + const [customVendorName, setCustomVendorName] = useState(""); + const [customVendorAuthToken, setCustomVendorAuthToken] = useState(""); + const [tmpCustomVendorTtsUrl, setTmpCustomVendorTtsUrl] = useState(""); + const [customVendorTtsUrl, setCustomVendorTtsUrl] = useState(""); + const [tmpCustomVendorSttUrl, setTmpCustomVendorSttUrl] = useState(""); + const [customVendorSttUrl, setCustomVendorSttUrl] = useState(""); const handleFile = (file: File) => { const handleError = () => { @@ -142,6 +153,14 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { ...(vendor === VENDOR_NVIDIA && { riva_server_uri: rivaServerUri || null, }), + ...(vendor === VENDOR_CUSTOM && { + vendor: (vendor + ":" + customVendorName) as Lowercase, + use_for_tts: ttsCheck ? 1 : 0, + use_for_stt: sttCheck ? 1 : 0, + custom_tts_url: customVendorTtsUrl || null, + custom_stt_url: customVendorSttUrl || null, + auth_token: customVendorAuthToken || null, + }), }; if (credential && credential.data) { @@ -198,7 +217,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { setLocation(); if (credential && credential.data) { if (credential.data.vendor) { - setVendor(credential.data.vendor); + const v = credential.data.vendor.startsWith(VENDOR_CUSTOM) + ? VENDOR_CUSTOM + : vendor; + setVendor(v); } if (credential.data.account_sid) { @@ -207,14 +229,18 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { if (credential.data.use_for_stt) { setSttCheck(true); + setInitialSttCheck(true); } else { setSttCheck(false); + setInitialSttCheck(false); } if (credential.data.use_for_tts) { setTtsCheck(true); + setInitialTtsCheck(true); } else { setTtsCheck(false); + setInitialTtsCheck(false); } if (credential.data.service_key) { @@ -279,6 +305,16 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { setCustomSttEndpoint(credential.data.custom_stt_endpoint || ""); setTmpCustomTtsEndpoint(credential.data.custom_tts_endpoint || ""); setTmpCustomSttEndpoint(credential.data.custom_stt_endpoint || ""); + setCustomVendorName( + credential.data.vendor.startsWith(VENDOR_CUSTOM) + ? credential.data.vendor.substring(VENDOR_CUSTOM.length + 1) + : credential.data.vendor + ); + setCustomVendorAuthToken(credential.data.auth_token || ""); + setCustomVendorSttUrl(credential.data.custom_stt_url || ""); + setTmpCustomVendorSttUrl(credential.data.custom_stt_url || ""); + setCustomVendorTtsUrl(credential.data.custom_tts_url || ""); + setTmpCustomVendorTtsUrl(credential.data.custom_tts_url || ""); } }, [credential]); @@ -307,7 +343,11 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { name: "Select a vendor", value: "", }, - ].concat(vendors)} + ] + .concat(vendors) + .filter( + (v) => !DISABLE_CUSTOM_SPEECH || v.value !== VENDOR_CUSTOM + )} onChange={(e) => { setVendor(e.target.value as Lowercase); setRegion(""); @@ -317,6 +357,23 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { disabled={credential ? true : false} required /> + {vendor === VENDOR_CUSTOM && ( + <> + + setCustomVendorName(e.target.value)} + disabled={credential ? true : false} + /> + + )}
{
{vendor && (
- {vendor !== VENDOR_DEEPGRAM && vendor !== VENDOR_SONIOX && ( - - )} - {vendor !== VENDOR_WELLSAID && ( + {vendor !== VENDOR_DEEPGRAM && + vendor !== VENDOR_SONIOX && + vendor != VENDOR_CUSTOM && ( + + )} + {vendor !== VENDOR_WELLSAID && vendor !== VENDOR_CUSTOM && ( )} + {vendor === VENDOR_CUSTOM && ( + + + + + + )} +
+ )} + {vendor === VENDOR_CUSTOM && ( +
+ + setCustomVendorAuthToken(e.target.value)} + disabled={credential ? true : false} + />
)} {vendor === VENDOR_GOOGLE && ( diff --git a/src/containers/internal/views/speech-services/index.tsx b/src/containers/internal/views/speech-services/index.tsx index caa4a4e..33f946e 100644 --- a/src/containers/internal/views/speech-services/index.tsx +++ b/src/containers/internal/views/speech-services/index.tsx @@ -26,6 +26,7 @@ import type { SpeechCredential, Account } from "src/api/types"; import { ScopedAccess } from "src/components/scoped-access"; import { Scope } from "src/store/types"; import { getAccountFilter, setLocation } from "src/store/localStore"; +import { VENDOR_CUSTOM } from "src/vendor"; export const SpeechServices = () => { const user = useSelectState("user"); @@ -140,7 +141,14 @@ export const SpeechServices = () => { title="Edit application" className="i" > - Vendor: {credential.vendor} + + Vendor:{" "} + {credential.vendor.startsWith(VENDOR_CUSTOM) + ? credential.vendor.substring( + VENDOR_CUSTOM.length + 1 + ) + : credential.vendor} + diff --git a/src/vendor/index.tsx b/src/vendor/index.tsx index 904cf71..069ae2c 100644 --- a/src/vendor/index.tsx +++ b/src/vendor/index.tsx @@ -18,6 +18,7 @@ export const VENDOR_DEEPGRAM = "deepgram"; export const VENDOR_IBM = "ibm"; export const VENDOR_NVIDIA = "nvidia"; export const VENDOR_SONIOX = "soniox"; +export const VENDOR_CUSTOM = "custom"; export const vendors: VendorOptions[] = [ { @@ -56,6 +57,10 @@ export const vendors: VendorOptions[] = [ name: "Soniox", value: VENDOR_SONIOX, }, + { + name: "Custom", + value: VENDOR_CUSTOM, + }, ]; export const useRegionVendors = () => { diff --git a/src/vendor/types.ts b/src/vendor/types.ts index 235b9ae..85a76ee 100644 --- a/src/vendor/types.ts +++ b/src/vendor/types.ts @@ -7,7 +7,8 @@ export type Vendor = | "Deepgram" | "IBM" | "Nvidia" - | "Soniox"; + | "Soniox" + | "Custom"; export interface VendorOptions { name: Vendor;