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 <quanluuhoang8@gmail.com>
This commit is contained in:
Hoan Luu Huu
2023-03-14 19:45:06 +07:00
committed by GitHub
parent a8d28da221
commit e425d825bc
7 changed files with 342 additions and 76 deletions
+5
View File
@@ -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;
+3
View File
@@ -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 {
@@ -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<keyof RecognizerVendors>(VENDOR_GOOGLE);
const [recogLang, setRecogLang] = useState(LANG_EN_US);
const [message, setMessage] = useState("");
const [apiUrl, setApiUrl] = useState("");
const [credentials] = useApiData<SpeechCredential[]>(apiUrl);
const [softTtsVendor, setSoftTtsVendor] = useState<VendorOptions[]>(vendors);
const [softSttVendor, setSoftSttVendor] = useState<VendorOptions[]>(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 && (
<>
<label htmlFor="synthesis_lang">Language</label>
<Selector
id="synthesis_lang"
name="synthesis_lang"
value={synthLang}
options={synthesis[synthVendor as keyof SynthesisVendors].map(
(lang: VoiceLanguage) => ({
{synthVendor &&
!synthVendor.toString().startsWith(VENDOR_CUSTOM) &&
synthLang && (
<>
<label htmlFor="synthesis_lang">Language</label>
<Selector
id="synthesis_lang"
name="synthesis_lang"
value={synthLang}
options={synthesis[
synthVendor as keyof SynthesisVendors
].map((lang: VoiceLanguage) => ({
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);
}}
/>
<label htmlFor="synthesis_voice">Voice</label>
<Selector
id="synthesis_voice"
name="synthesis_voice"
value={synthVoice}
options={
synthesis[synthVendor as keyof SynthesisVendors]
.filter(
(lang: VoiceLanguage) => 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) && (
<>
<label htmlFor="custom_vendor_synthesis_lang">Language</label>
<input
id="custom_vendor_synthesis_lang"
type="text"
name="custom_vendor_synthesis_lang"
placeholder="Required"
required
value={synthLang}
onChange={(e) => {
setSynthLang(e.target.value);
}}
/>
<label htmlFor="synthesis_voice">Voice</label>
<Selector
id="synthesis_voice"
name="synthesis_voice"
<label htmlFor="custom_vendor_synthesis_voice">Voice</label>
<input
id="custom_vendor_synthesis_voice"
type="text"
name="custom_vendor_synthesis_voice"
placeholder="Required"
required
value={synthVoice}
options={
synthesis[synthVendor as keyof SynthesisVendors]
.filter((lang: VoiceLanguage) => 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 && (
<>
<label htmlFor="recognizer_lang">Language</label>
<Selector
id="recognizer_lang"
name="recognizer_lang"
value={recogLang}
options={recognizers[
recogVendor as keyof RecognizerVendors
].map((lang: Language) => ({
name: lang.name,
value: lang.code,
}))}
onChange={(e) => {
setRecogLang(e.target.value);
}}
/>
</>
)}
{recogVendor.toString().startsWith(VENDOR_CUSTOM) && (
<>
<label htmlFor="recognizer_lang">Language</label>
<Selector
id="recognizer_lang"
name="recognizer_lang"
<label htmlFor="custom_vendor_recognizer_voice">Language</label>
<input
id="custom_vendor_recognizer_voice"
type="text"
name="custom_vendor_recognizer_voice"
placeholder="Required"
required
value={recogLang}
options={recognizers[
recogVendor as keyof RecognizerVendors
].map((lang: Language) => ({
name: lang.name,
value: lang.code,
}))}
onChange={(e) => {
setRecogLang(e.target.value);
}}
@@ -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<SpeechCredential>;
@@ -53,7 +56,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const regions = useRegionVendors();
const [accounts] = useServiceProviderData<Account[]>("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<Lowercase<Vendor>>(
"" as Lowercase<Vendor>
@@ -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<Vendor>,
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<Vendor>);
setRegion("");
@@ -317,6 +357,23 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
disabled={credential ? true : false}
required
/>
{vendor === VENDOR_CUSTOM && (
<>
<label htmlFor="custom_vendor_name">
Name<span>*</span>
</label>
<input
id="custom_vendor_name"
required
type="text"
name="custom_vendor_name"
placeholder="Vendor Name"
value={customVendorName}
onChange={(e) => setCustomVendorName(e.target.value)}
disabled={credential ? true : false}
/>
</>
)}
</fieldset>
<fieldset>
<AccountSelect
@@ -329,19 +386,21 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
</fieldset>
{vendor && (
<fieldset>
{vendor !== VENDOR_DEEPGRAM && vendor !== VENDOR_SONIOX && (
<label htmlFor="use_for_tts" className="chk">
<input
id="use_for_tts"
name="use_for_tts"
type="checkbox"
onChange={(e) => setTtsCheck(e.target.checked)}
defaultChecked={ttsCheck}
/>
<div>Use for text-to-speech</div>
</label>
)}
{vendor !== VENDOR_WELLSAID && (
{vendor !== VENDOR_DEEPGRAM &&
vendor !== VENDOR_SONIOX &&
vendor != VENDOR_CUSTOM && (
<label htmlFor="use_for_tts" className="chk">
<input
id="use_for_tts"
name="use_for_tts"
type="checkbox"
onChange={(e) => setTtsCheck(e.target.checked)}
defaultChecked={ttsCheck}
/>
<div>Use for text-to-speech</div>
</label>
)}
{vendor !== VENDOR_WELLSAID && vendor !== VENDOR_CUSTOM && (
<label htmlFor="use_for_stt" className="chk">
<input
id="use_for_stt"
@@ -353,6 +412,87 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
<div>Use for speech-to-text</div>
</label>
)}
{vendor === VENDOR_CUSTOM && (
<Fragment>
<Checkzone
hidden
name="custom_vendor_use_for_tts"
label="Use for text-to-speech"
initialCheck={initialTtsCheck}
handleChecked={(e) => {
setTtsCheck(e.target.checked);
if (!e.target.checked) {
setTmpCustomVendorTtsUrl(customVendorTtsUrl);
setCustomVendorTtsUrl("");
} else {
setCustomVendorTtsUrl(tmpCustomVendorTtsUrl);
}
}}
>
<label htmlFor="custom_vendor_use_for_tts">
TTS HTTP URL<span>*</span>
</label>
<input
id="custom_vendor_use_for_tts"
type="text"
name="custom_vendor_use_for_tts"
placeholder="Required"
required={ttsCheck}
value={customVendorTtsUrl}
onChange={(e) => {
setCustomVendorTtsUrl(e.target.value);
}}
/>
</Checkzone>
<Checkzone
hidden
name="custom_vendor_use_for_stt"
label="Use for speech-to-text"
initialCheck={initialSttCheck}
handleChecked={(e) => {
setSttCheck(e.target.checked);
if (!e.target.checked) {
setTmpCustomVendorSttUrl(customVendorSttUrl);
setCustomVendorSttUrl("");
} else {
setCustomVendorSttUrl(tmpCustomVendorSttUrl);
}
}}
>
<label htmlFor="custom_vendor_use_for_stt">
STT websocket URL<span>*</span>
</label>
<input
id="custom_vendor_use_for_stt"
type="text"
name="custom_vendor_use_for_stt"
placeholder="Required"
required={sttCheck}
value={customVendorSttUrl}
onChange={(e) => {
setCustomVendorSttUrl(e.target.value);
}}
/>
</Checkzone>
</Fragment>
)}
</fieldset>
)}
{vendor === VENDOR_CUSTOM && (
<fieldset>
<label htmlFor="custom_vendor_auth_token">
Authentication Token
</label>
<input
id="custom_vendor_auth_token"
type="text"
name="custom_vendor_auth_token"
placeholder="Authentication Token"
value={customVendorAuthToken}
onChange={(e) => setCustomVendorAuthToken(e.target.value)}
disabled={credential ? true : false}
/>
</fieldset>
)}
{vendor === VENDOR_GOOGLE && (
@@ -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"
>
<strong>Vendor: {credential.vendor}</strong>
<strong>
Vendor:{" "}
{credential.vendor.startsWith(VENDOR_CUSTOM)
? credential.vendor.substring(
VENDOR_CUSTOM.length + 1
)
: credential.vendor}
</strong>
<Icons.ArrowRight />
</Link>
</ScopedAccess>
+5
View File
@@ -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 = () => {
+2 -1
View File
@@ -7,7 +7,8 @@ export type Vendor =
| "Deepgram"
| "IBM"
| "Nvidia"
| "Soniox";
| "Soniox"
| "Custom";
export interface VendorOptions {
name: Vendor;