Compare commits

...

10 Commits

Author SHA1 Message Date
Hoan Luu Huu
a586771ea6 support playht3.0 languages (#459)
* support playht3.0 languages

* wip
2024-10-16 07:21:36 -04:00
Hoan Luu Huu
7aaea04d3c support speechmatics speechcredential (#458)
* support speechmatics

* support speechmatics regions

* add env VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS
2024-10-11 08:58:23 -04:00
Hoan Luu Huu
f1d2ed8abd allow system information contains log level and account has enable_debug_log (#457)
* sip gateways support inbound pad crypto

* allow system information contains log level and account has enable_debug_log

* only admin can set account log level
2024-10-07 09:51:38 -04:00
Hoan Luu Huu
d7d61a769d sorting application name in applications index, phone number application selection (#455)
* sip gateways support inbound pad crypto

* sorting application name in applications index, phone number application selection
2024-10-03 10:23:17 -04:00
Hoan Luu Huu
c9da7946f3 sip gateways support inbound pad crypto (#452) 2024-09-11 09:37:02 +01:00
Hoan Luu Huu
5755cd8886 support admin settings private network CIDR (#447)
* support admin settings private network CIDR

* support admin settings private network CIDR

* fix review comment
2024-08-14 18:30:24 -04:00
Hoan Luu Huu
786327a0b9 support deepgram on-prem (#444) 2024-08-07 07:24:35 -04:00
Hoan Luu Huu
2c390715d8 Feat/rid of env (#443)
* Get rid of VITE_API_BASE_URL

* wip

* wip
2024-07-15 08:58:22 -04:00
Hoan Luu Huu
dcdc2c0808 add use sips scheme to outbound tls gateway (#439)
* add use sips scheme to outbound tls gateway

* update license
2024-06-15 09:13:04 -04:00
Hoan Luu Huu
a3c48e7efb support verbio speech (#434) 2024-05-29 07:57:05 -04:00
16 changed files with 592 additions and 133 deletions

6
.env
View File

@@ -1,5 +1,5 @@
VITE_API_BASE_URL=http://127.0.0.1:3000/v1
VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
#VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
## enables choosing units and lisenced account call limits
# VITE_APP_ENABLE_ACCOUNT_LIMITS_ALL=true
@@ -25,4 +25,6 @@ VITE_DEV_BASE_URL=http://127.0.0.1:3000/v1
## Base url for jambomz webapp
#VITE_APP_BASE_URL="http://jambonz.one"
## Strip publishable key
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"
#VITE_APP_STRIPE_PUBLISHABLE_KEY="pk_test_EChRaX9Tjk8csZZVSeoGqNvu00lsJzjaU1"
## ignore some specific speech vendors, defined by ADDITIONAL_SPEECH_VENDORS constant
# VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS=true

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Drachtio Communications Services, LLC
Copyright (c) 2018-2024 FirstFive8, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.

View File

@@ -1,3 +1,4 @@
import { hasValue } from "src/utils";
import type {
Currency,
ElevenLabsOptions,
@@ -12,6 +13,7 @@ import type {
WebHook,
WebhookOption,
} from "./types";
import { Vendor } from "src/vendor/types";
/** This window object is serialized and injected at docker runtime */
/** The API url is constructed with the docker containers `ip:port` */
@@ -28,6 +30,7 @@ interface JambonzWindowObject {
BASE_URL: string;
DEFAULT_SERVICE_PROVIDER_SID: string;
STRIPE_PUBLISHABLE_KEY: string;
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
}
declare global {
@@ -37,8 +40,11 @@ declare global {
}
/** https://vitejs.dev/guide/env-and-mode.html#env-files */
export const API_BASE_URL =
const CONFIGURED_API_BASE_URL =
window.JAMBONZ?.API_BASE_URL || import.meta.env.VITE_API_BASE_URL;
export const API_BASE_URL = hasValue(CONFIGURED_API_BASE_URL)
? CONFIGURED_API_BASE_URL
: `${window.location.protocol}//${window.location.hostname}/api/v1`;
/** Serves mock API responses from a local dev API server */
export const DEV_BASE_URL = import.meta.env.VITE_DEV_BASE_URL;
@@ -72,6 +78,13 @@ export const DISABLE_CALL_RECORDING: boolean =
window.JAMBONZ?.DISABLE_CALL_RECORDING === "true" ||
JSON.parse(import.meta.env.VITE_APP_DISABLE_CALL_RECORDING || "false");
/** Disable additional speech vendors */
export const DISABLE_ADDITIONAL_SPEECH_VENDORS: boolean =
window.JAMBONZ?.DISABLE_ADDITIONAL_SPEECH_VENDORS === "true" ||
JSON.parse(
import.meta.env.VITE_APP_DISABLE_ADDITIONAL_SPEECH_VENDORS || "false",
);
export const DEFAULT_SERVICE_PROVIDER_SID: string =
window.JAMBONZ?.DEFAULT_SERVICE_PROVIDER_SID ||
import.meta.env.VITE_APP_DEFAULT_SERVICE_PROVIDER_SID;
@@ -201,10 +214,31 @@ export const AUDIO_FORMAT_OPTIONS = [
},
];
export const LOG_LEVEL_OPTIONS = [
{
name: "Info",
value: "info",
},
{
name: "Debug",
value: "debug",
},
];
export const DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2";
export const DEFAULT_WHISPER_MODEL = "tts-1";
// VERBIO
export const VERBIO_STT_MODELS = [
{ name: "V1", value: "V1" },
{ name: "V2", value: "V2" },
];
export const DEFAULT_VERBIO_MODEL = "V1";
export const ADDITIONAL_SPEECH_VENDORS: Lowercase<Vendor>[] = ["speechmatics"];
// Google Custom Voice reported usage options
export const DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = "REALTIME";

View File

@@ -875,6 +875,7 @@ export const getSpeechSupportedLanguagesAndVoices = (
sid: string | undefined,
vendor: string,
label: string,
create_new: boolean = false,
) => {
const userData = parseJwt(getToken());
const apiUrl =
@@ -883,7 +884,7 @@ export const getSpeechSupportedLanguagesAndVoices = (
: `${API_SERVICE_PROVIDERS}/${sid}`) +
`/SpeechCredentials/speech/supportedLanguagesAndVoices?vendor=${vendor}${
label ? `&label=${label}` : ""
}`;
}${create_new ? "&create_new=true" : ""}`;
return getFetch<SpeechSupportedLanguagesAndVoices>(apiUrl);
};

View File

@@ -26,6 +26,8 @@ export interface LimitUnitOption {
/** User roles / permissions */
export type UserScopes = "admin" | "service_provider" | "account";
export type LogLevel = "info" | "debug";
export type UserPermissions =
| "VIEW_ONLY"
| "PROVISION_SERVICES"
@@ -122,9 +124,11 @@ export interface ForgotPassword {
}
export interface SystemInformation {
domain_name: string;
sip_domain_name: string;
monitoring_domain_name: string;
domain_name: null | string;
sip_domain_name: null | string;
monitoring_domain_name: null | string;
private_network_cidr: null | string;
log_level: LogLevel;
}
export interface TtsCache {
@@ -261,6 +265,7 @@ export interface Account {
device_to_call_ratio?: number;
trial_end_date?: null | string;
is_active: boolean;
enable_debug_log: boolean;
}
export interface Product {
@@ -402,6 +407,7 @@ export interface SpeechCredential {
custom_stt_endpoint_url: null | string;
custom_stt_endpoint: null | string;
client_id: null | string;
client_secret: null | string;
secret: null | string;
nuance_tts_uri: null | string;
nuance_stt_uri: null | string;
@@ -418,10 +424,13 @@ export interface SpeechCredential {
cobalt_server_uri: null | string;
model_id: null | string;
voice_engine: null | string;
engine_version: null | string;
model: null | string;
options: null | string;
deepgram_stt_uri: null | string;
deepgram_tts_uri: null | string;
deepgram_stt_use_tls: number;
speechmatics_stt_uri: null | string;
}
export interface Alert {
@@ -487,6 +496,7 @@ export interface SipGateway extends Gateway {
port: number | null;
pad_crypto?: boolean;
send_options_ping?: boolean;
use_sips_scheme?: boolean;
}
export interface SmppGateway extends Gateway {

View File

@@ -14,7 +14,14 @@ import {
postAccountBucketCredentialTest,
deleteAccount,
} from "src/api";
import { ClipBoard, Icons, Modal, Section, Tooltip } from "src/components";
import {
ClipBoard,
Icons,
Modal,
ScopedAccess,
Section,
Tooltip,
} from "src/components";
import {
Selector,
Checkzone,
@@ -67,6 +74,7 @@ import dayjs from "dayjs";
import { EditBoard } from "src/components/editboard";
import { ModalLoader } from "src/components/modal";
import { useAuth } from "src/router/auth";
import { Scope } from "src/store/types";
type AccountFormProps = {
apps?: Application[];
@@ -137,6 +145,7 @@ export const AccountForm = ({
const [isShowModalLoader, setIsShowModalLoader] = useState(false);
const [azureConnectionString, setAzureConnectionString] = useState("");
const [endpoint, setEndpoint] = useState("");
const [enableDebugLog, setEnableDebugLog] = useState(false);
/** This lets us map and render the same UI for each... */
const webhooks = [
@@ -381,6 +390,7 @@ export const AccountForm = ({
if (account && account.data) {
putAccount(account.data.account_sid, {
name,
enable_debug_log: enableDebugLog,
...(!ENABLE_HOSTED_SYSTEM && { sip_realm: realm || null }),
webhook_secret: account.data.webhook_secret,
siprec_hook_sid: recId || null,
@@ -450,6 +460,7 @@ export const AccountForm = ({
queue_event_hook: queueHook || null,
registration_hook: regHook || null,
service_provider_sid: currentServiceProvider.service_provider_sid,
enable_debug_log: enableDebugLog,
})
.then(({ json }) => {
toastSuccess("Account created successfully");
@@ -465,6 +476,7 @@ export const AccountForm = ({
/** Set current account data values if applicable -- e.g. "edit mode" */
useEffect(() => {
if (account && account.data) {
setEnableDebugLog(account.data.enable_debug_log);
setName(account.data.name);
if (account.data.sip_realm) {
@@ -1021,6 +1033,22 @@ export const AccountForm = ({
} cached TTS prompts`}</MS>
</fieldset>
)}
<ScopedAccess scope={Scope.admin} user={user}>
<fieldset>
<label htmlFor="enable_debug_log" className="chk">
<input
id="enable_debug_log"
name="enable_debug_log"
type="checkbox"
onChange={(e) => setEnableDebugLog(e.target.checked)}
checked={enableDebugLog}
/>
<Tooltip text="You can enable debug log for calls only to this account">
Enable debug log for this account
</Tooltip>
</label>
</fieldset>
</ScopedAccess>
{!DISABLE_CALL_RECORDING && (
<>
<fieldset>

View File

@@ -113,59 +113,62 @@ export const Applications = () => {
{!hasValue(applications) && hasLength(accounts) ? (
<Spinner />
) : hasLength(filteredApplications) ? (
filteredApplications.map((application) => {
return (
<div className="item" key={application.application_sid}>
<div className="item__info">
<div className="item__title">
<Link
to={`${ROUTE_INTERNAL_APPLICATIONS}/${application.application_sid}/edit`}
title="Edit application"
className="i"
>
<strong>{application.name}</strong>
<Icons.ArrowRight />
</Link>
</div>
<div className="item__meta">
<div>
<div
className={`i txt--${
application.account_sid ? "teal" : "grey"
}`}
filteredApplications
.sort((a, b) => a.name.localeCompare(b.name))
.map((application) => {
return (
<div className="item" key={application.application_sid}>
<div className="item__info">
<div className="item__title">
<Link
to={`${ROUTE_INTERNAL_APPLICATIONS}/${application.application_sid}/edit`}
title="Edit application"
className="i"
>
<Icons.Activity />
<span>
{
accounts?.find(
(acct) =>
acct.account_sid === application.account_sid,
)?.name
}
</span>
<strong>{application.name}</strong>
<Icons.ArrowRight />
</Link>
</div>
<div className="item__meta">
<div>
<div
className={`i txt--${
application.account_sid ? "teal" : "grey"
}`}
>
<Icons.Activity />
<span>
{
accounts?.find(
(acct) =>
acct.account_sid ===
application.account_sid,
)?.name
}
</span>
</div>
</div>
</div>
</div>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_APPLICATIONS}/${application.application_sid}/edit`}
title="Edit application"
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete application"
onClick={() => setApplication(application)}
className="btnty"
>
<Icons.Trash />
</button>
</div>
</div>
<div className="item__actions">
<Link
to={`${ROUTE_INTERNAL_APPLICATIONS}/${application.application_sid}/edit`}
title="Edit application"
>
<Icons.Edit3 />
</Link>
<button
type="button"
title="Delete application"
onClick={() => setApplication(application)}
className="btnty"
>
<Icons.Trash />
</button>
</div>
</div>
);
})
);
})
) : accountSid ? (
<M>No applications.</M>
) : (

View File

@@ -28,6 +28,8 @@ import {
VENDOR_SONIOX,
VENDOR_WELLSAID,
VENDOR_WHISPER,
VENDOR_SPEECHMATICS,
VENDOR_PLAYHT,
} from "src/vendor";
import {
LabelOptions,
@@ -198,6 +200,19 @@ export const SpeechProviderSelection = ({
if (synthesisGoogleCustomVoiceOptions.length > 0) {
updateTtsVoice(synthesisGoogleCustomVoiceOptions[0].value);
}
}
// PlayHT3.0 all voices are listed under english language, all voices can be used for multiple languages
else if (
synthVendor === VENDOR_PLAYHT &&
synthesisSupportedLanguagesAndVoices.tts.some(
(l) => l.value === "english",
)
) {
setSynthesisVoiceOptions(
synthesisSupportedLanguagesAndVoices.tts.find(
(tts) => tts.value === "english",
)!.voices,
);
} else {
setSynthesisVoiceOptions(voicesOpts);
}
@@ -261,6 +276,14 @@ export const SpeechProviderSelection = ({
updateTtsVoice(newLang!.voices[0].value);
return;
}
if (synthVendor === VENDOR_PLAYHT) {
const newLang = json.tts.find(
(lang) => lang.value === LANG_EN_US || lang.value === "english",
);
setSynthLang(newLang!.value);
updateTtsVoice(newLang!.voices[0].value);
return;
}
/** Google and AWS have different language lists */
/** If the new language doesn't map then default to "en-US" */
let newLang = json.tts.find((lang) => lang.value === synthLang);
@@ -360,8 +383,9 @@ export const SpeechProviderSelection = ({
value={synthVendor}
options={ttsVendorOptions.filter(
(vendor) =>
vendor.value != VENDOR_ASSEMBLYAI &&
vendor.value != VENDOR_SONIOX &&
vendor.value !== VENDOR_ASSEMBLYAI &&
vendor.value !== VENDOR_SONIOX &&
vendor.value !== VENDOR_SPEECHMATICS &&
vendor.value !== VENDOR_CUSTOM &&
vendor.value !== VENDOR_COBALT,
)}
@@ -383,6 +407,7 @@ export const SpeechProviderSelection = ({
value={synthLabel}
options={ttsLabelOptions}
onChange={(e) => {
shouldUpdateTtsVoice.current = true;
setSynthLabel(e.target.value);
}}
/>
@@ -410,7 +435,9 @@ export const SpeechProviderSelection = ({
id="synthesis_lang"
name="synthesis_lang"
value={synthLang}
options={synthesisLanguageOptions}
options={synthesisLanguageOptions.sort((a, b) =>
a.name.localeCompare(b.name),
)}
onChange={(e) => {
shouldUpdateTtsVoice.current = true;
const language = e.target.value;
@@ -465,7 +492,9 @@ export const SpeechProviderSelection = ({
id="synthesis_voice"
name="synthesis_voice"
value={synthVoice}
options={synthesisVoiceOptions}
options={synthesisVoiceOptions.sort((a, b) =>
a.name.localeCompare(b.name),
)}
onChange={(e) => setSynthVoice(e.target.value)}
/>
)}

View File

@@ -1096,29 +1096,24 @@ export const CarrierForm = ({
<div>Outbound</div>
</label>
</div>
{g.outbound > 0 && g.protocol === "tls/srtp" && (
<div>
<label
htmlFor={`sip_pad_crypto_${i}`}
className="chk"
>
<input
id={`sip_pad_crypto_${i}`}
name={`sip_pad_crypto_${i}`}
type="checkbox"
checked={g.pad_crypto ? true : false}
onChange={(e) => {
updateSipGateways(
i,
"pad_crypto",
e.target.checked,
);
}}
/>
<div>Pad crypto</div>
</label>
</div>
)}
<div>
<label htmlFor={`sip_pad_crypto_${i}`} className="chk">
<input
id={`sip_pad_crypto_${i}`}
name={`sip_pad_crypto_${i}`}
type="checkbox"
checked={g.pad_crypto ? true : false}
onChange={(e) => {
updateSipGateways(
i,
"pad_crypto",
e.target.checked,
);
}}
/>
<div>Pad crypto</div>
</label>
</div>
{Boolean(g.outbound) && (
<div>
<label
@@ -1142,6 +1137,30 @@ export const CarrierForm = ({
</label>
</div>
)}
{Boolean(g.outbound) &&
(g.protocol === "tls" || g.protocol === "tls/srtp") && (
<div>
<label
htmlFor={`use_sips_scheme_${i}`}
className="chk"
>
<input
id={`use_sips_scheme_${i}`}
name={`use_sips_scheme_${i}`}
type="checkbox"
checked={g.use_sips_scheme ? true : false}
onChange={(e) => {
updateSipGateways(
i,
"use_sips_scheme",
e.target.checked,
);
}}
/>
<div>Use sips scheme</div>
</label>
</div>
)}
</div>
<button

View File

@@ -200,9 +200,11 @@ export const PhoneNumberForm = ({ phoneNumber }: PhoneNumberFormProps) => {
application={[applicationSid, setApplicationSid]}
applications={
applications
? applications.filter(
(application) => application.account_sid === accountSid,
)
? applications
.filter(
(application) => application.account_sid === accountSid,
)
.sort((a, b) => a.name.localeCompare(b.name))
: []
}
/>

View File

@@ -7,11 +7,20 @@ import {
postSystemInformation,
deleteTtsCache,
} from "src/api";
import { PasswordSettings, SystemInformation, TtsCache } from "src/api/types";
import {
LogLevel,
PasswordSettings,
SystemInformation,
TtsCache,
} from "src/api/types";
import { toastError, toastSuccess } from "src/store";
import { Selector } from "src/components/forms";
import { hasValue } from "src/utils";
import { PASSWORD_LENGTHS_OPTIONS, PASSWORD_MIN } from "src/api/constants";
import { hasValue, isvalidIpv4OrCidr } from "src/utils";
import {
LOG_LEVEL_OPTIONS,
PASSWORD_LENGTHS_OPTIONS,
PASSWORD_MIN,
} from "src/api/constants";
import { Modal } from "src/components";
export const AdminSettings = () => {
@@ -25,9 +34,11 @@ export const AdminSettings = () => {
const [requireDigit, setRequireDigit] = useState(false);
const [requireSpecialCharacter, setRequireSpecialCharacter] = useState(false);
const [domainName, setDomainName] = useState("");
const [privateNetworkCidr, setPrivateNetworkCidr] = useState("");
const [sipDomainName, setSipDomainName] = useState("");
const [monitoringDomainName, setMonitoringDomainName] = useState("");
const [clearTtsCacheFlag, setClearTtsCacheFlag] = useState(false);
const [logLevel, setLogLevel] = useState<LogLevel>("info");
const handleClearCache = () => {
deleteTtsCache()
@@ -44,10 +55,22 @@ export const AdminSettings = () => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (privateNetworkCidr) {
const cidrs = privateNetworkCidr.split(",");
for (const cidr of cidrs) {
if (cidr && !isvalidIpv4OrCidr(cidr)) {
toastError(`Invalid private network CIDR "${cidr}"`);
return;
}
}
}
const systemInformationPayload: Partial<SystemInformation> = {
domain_name: domainName,
sip_domain_name: sipDomainName,
monitoring_domain_name: monitoringDomainName,
domain_name: domainName || null,
sip_domain_name: sipDomainName || null,
monitoring_domain_name: monitoringDomainName || null,
private_network_cidr: privateNetworkCidr || null,
log_level: logLevel,
};
const passwordSettingsPayload: Partial<PasswordSettings> = {
min_password_length: minPasswordLength,
@@ -61,7 +84,7 @@ export const AdminSettings = () => {
.then(() => {
passwordSettingsFetcher();
systemInformationFetcher();
toastSuccess("Password settings successfully updated");
toastSuccess("Admin settings updated successfully");
})
.catch((error) => {
toastError(error.msg);
@@ -78,11 +101,21 @@ export const AdminSettings = () => {
setMinPasswordLength(passwordSettings.min_password_length);
}
}
if (hasValue(systemInformation)) {
if (systemInformation?.domain_name) {
setDomainName(systemInformation.domain_name);
}
if (systemInformation?.sip_domain_name) {
setSipDomainName(systemInformation.sip_domain_name);
}
if (systemInformation?.monitoring_domain_name) {
setMonitoringDomainName(systemInformation.monitoring_domain_name);
}
if (systemInformation?.private_network_cidr) {
setPrivateNetworkCidr(systemInformation.private_network_cidr);
}
if (systemInformation?.log_level) {
setLogLevel(systemInformation.log_level);
}
}, [passwordSettings, systemInformation]);
return (
@@ -107,6 +140,15 @@ export const AdminSettings = () => {
value={sipDomainName}
onChange={(e) => setSipDomainName(e.target.value)}
/>
<label htmlFor="name">Private Network CIDR</label>
<input
id="private_network_cidr"
type="text"
name="private_network_cidr"
placeholder="Private network CIDR"
value={privateNetworkCidr}
onChange={(e) => setPrivateNetworkCidr(e.target.value)}
/>
<label htmlFor="name">Monitoring Domain Name</label>
<input
id="monitor_domain_name"
@@ -116,6 +158,17 @@ export const AdminSettings = () => {
value={monitoringDomainName}
onChange={(e) => setMonitoringDomainName(e.target.value)}
/>
<label htmlFor="audio_format">Log Level</label>
<Selector
id={"audio_format"}
name={"audio_format"}
value={logLevel}
options={LOG_LEVEL_OPTIONS}
onChange={(e) => {
setLogLevel(e.target.value as LogLevel);
}}
/>
</fieldset>
<fieldset>
<label htmlFor="min_password_length">Min password length</label>

View File

@@ -46,6 +46,8 @@ import {
AWS_CREDENTIAL_IAM_ASSUME_ROLE,
AWS_CREDENTIAL_ACCESS_KEY,
AWS_INSTANCE_PROFILE,
VENDOR_VERBIO,
VENDOR_SPEECHMATICS,
} from "src/vendor";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import {
@@ -73,12 +75,16 @@ import type {
} from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
import {
ADDITIONAL_SPEECH_VENDORS,
DEFAULT_ELEVENLABS_OPTIONS,
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
DEFAULT_PLAYHT_OPTIONS,
DEFAULT_RIMELABS_OPTIONS,
DEFAULT_VERBIO_MODEL,
DISABLE_ADDITIONAL_SPEECH_VENDORS,
DISABLE_CUSTOM_SPEECH,
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
VERBIO_STT_MODELS,
} from "src/api/constants";
type SpeechServiceFormProps = {
@@ -106,6 +112,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [secretAccessKey, setSecretAccessKey] = useState("");
const [clientId, setClientId] = useState("");
const [secretKey, setSecretKey] = useState("");
const [clientSecret, setClientSecret] = useState("");
const [googleServiceKey, setGoogleServiceKey] =
useState<GoogleServiceKey | null>(null);
const [sttRegion, setSttRegion] = useState("");
@@ -113,6 +120,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [ttsRegion, setTtsRegion] = useState("");
const [ttsApiKey, setTtsApiKey] = useState("");
const [ttsModelId, setTtsModelId] = useState("");
const [engineVersion, setEngineVersion] = useState(DEFAULT_VERBIO_MODEL);
const [instanceId, setInstanceId] = useState("");
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
const [initialCheckCustomStt, setInitialCheckCustomStt] = useState(false);
@@ -155,12 +163,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [options, setOptions] = useState("");
const [tmpOptions, setTmpOptions] = useState("");
const [deepgramSttUri, setDeepgramSttUri] = useState("");
const [deepgramTtsUri, setDeepgramTtsUri] = useState("");
const [tmpDeepgramSttUri, setTmpDeepgramSttUri] = useState("");
const [tmpDeepgramTtsUri, setTmpDeepgramTtsUri] = useState("");
const [deepgramSttUseTls, setDeepgramSttUseTls] = useState(false);
const [tmpDeepgramSttUseTls, setTmpDeepgramSttUseTls] = useState(false);
const [initialDeepgramOnpremCheck, setInitialDeepgramOnpremCheck] =
useState(false);
const [isDeepgramOnpremEnabled, setIsDeepgramOnpremEnabled] = useState(false);
const [initialSpeechmaticsOnpremCheck, setInitialSpeechMaticsOnpremCheck] =
useState(false);
const [speechmaticsEndpoint, setSpeechmaticsEndpoint] = useState("");
const [tmpHostedSpeechmaticsEndpoint, setTmpHostedSpeechmaticsEndpoint] =
useState("");
const [tmpOnpremSpeechmaticsEndpoint, setTmpOnpremSpeechmaticsEndpoint] =
useState("");
const [awsCredentialType, setAwsCredentialType] = useState(
AWS_CREDENTIAL_ACCESS_KEY,
);
@@ -351,8 +367,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
}),
...(vendor === VENDOR_DEEPGRAM && {
deepgram_stt_uri: deepgramSttUri || null,
deepgram_tts_uri: deepgramTtsUri || null,
deepgram_stt_use_tls: deepgramSttUseTls ? 1 : 0,
}),
...(vendor === VENDOR_SPEECHMATICS && {
speechmatics_stt_uri: speechmaticsEndpoint || null,
}),
...(vendor === VENDOR_VERBIO && {
engine_version: engineVersion,
}),
};
if (credential && credential.data) {
@@ -394,6 +417,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_DEEPGRAM ||
vendor === VENDOR_ASSEMBLYAI ||
vendor === VENDOR_SONIOX ||
vendor === VENDOR_SPEECHMATICS ||
vendor === VENDOR_ELEVENLABS ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
@@ -405,6 +429,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
userId && {
user_id: userId,
}),
...(vendor === VENDOR_VERBIO && {
client_id: clientId,
client_secret: clientSecret,
}),
riva_server_uri: vendor == VENDOR_NVIDIA ? rivaServerUri : null,
})
.then(({ json }) => {
@@ -445,6 +473,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
currentServiceProvider?.service_provider_sid,
vendor,
"",
credential ? false : true,
).then(({ json }) => {
if (json.models) {
setTtsModels(json.models);
@@ -620,13 +649,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (credential?.data?.deepgram_stt_uri) {
setDeepgramSttUri(credential.data.deepgram_stt_uri);
}
if (credential?.data?.deepgram_tts_uri) {
setDeepgramTtsUri(credential.data.deepgram_tts_uri);
}
if (credential?.data?.deepgram_stt_use_tls) {
setDeepgramSttUseTls(
credential?.data?.deepgram_stt_use_tls > 0 ? true : false,
);
}
setInitialDeepgramOnpremCheck(hasValue(credential?.data?.deepgram_stt_uri));
setIsDeepgramOnpremEnabled(hasValue(credential?.data?.deepgram_stt_uri));
if (credential?.data?.user_id) {
setUserId(credential.data.user_id);
@@ -647,6 +678,22 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
: AWS_INSTANCE_PROFILE,
);
}
if (credential?.data?.client_id) {
setClientId(credential.data.client_id);
}
if (credential?.data?.client_secret) {
setClientSecret(credential.data.client_secret);
}
if (credential?.data?.engine_version) {
setEngineVersion(credential.data.engine_version);
}
if (credential?.data?.speechmatics_stt_uri) {
setInitialSpeechMaticsOnpremCheck(
!credential.data.speechmatics_stt_uri?.includes("speechmatics.com"),
);
setSpeechmaticsEndpoint(credential.data.speechmatics_stt_uri);
}
}, [credential]);
const updateCustomVoices = (
@@ -699,7 +746,12 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
]
.concat(vendors)
.filter(
(v) => !DISABLE_CUSTOM_SPEECH || v.value !== VENDOR_CUSTOM,
(v) =>
(!DISABLE_CUSTOM_SPEECH || v.value !== VENDOR_CUSTOM) &&
(!DISABLE_ADDITIONAL_SPEECH_VENDORS ||
!ADDITIONAL_SPEECH_VENDORS.includes(
v.value as Lowercase<Vendor>,
)),
)}
onChange={(e) => {
setVendor(e.target.value as Lowercase<Vendor>);
@@ -758,6 +810,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
{vendor !== VENDOR_ASSEMBLYAI &&
vendor !== VENDOR_COBALT &&
vendor !== VENDOR_SONIOX &&
vendor !== VENDOR_SPEECHMATICS &&
vendor != VENDOR_CUSTOM && (
<label htmlFor="use_for_tts" className="chk">
<input
@@ -1183,6 +1236,58 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
</fieldset>
</>
)}
{vendor === VENDOR_VERBIO && (
<>
<fieldset>
<label htmlFor="verbio_client_id">
Client ID
{!onPremNuanceSttCheck && !onPremNuanceTtsCheck && (
<span>*</span>
)}
</label>
<input
id="verbio_client_id"
required
type="text"
name="verbio_client_id"
placeholder="Client ID"
value={clientId}
onChange={(e) => setClientId(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor="verbio_client_secret">
Client secret
{!onPremNuanceSttCheck && !onPremNuanceTtsCheck && (
<span>*</span>
)}
</label>
<input
id="verbio_client_secret"
required
type="text"
name="verbio_client_secret"
placeholder="Client secret"
value={clientSecret}
onChange={(e) => setClientSecret(e.target.value)}
disabled={credential ? true : false}
/>
</fieldset>
<fieldset>
<label htmlFor={`${vendor}_tts_model_id`}>
Engine version<span>*</span>
</label>
<Selector
id={"verbio_engine_version"}
name={"verbio_engine_version"}
value={engineVersion}
options={VERBIO_STT_MODELS}
onChange={(e) => {
setEngineVersion(e.target.value);
}}
/>
</fieldset>
</>
)}
{vendor === VENDOR_AWS && (
<fieldset>
<label htmlFor="vendor">
@@ -1262,7 +1367,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_WHISPER ||
vendor === VENDOR_PLAYHT ||
vendor === VENDOR_RIMELABS ||
vendor === VENDOR_SONIOX) && (
vendor === VENDOR_SONIOX ||
vendor === VENDOR_SPEECHMATICS) && (
<fieldset>
{vendor === VENDOR_PLAYHT && (
<>
@@ -1311,22 +1417,6 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{vendor === VENDOR_DEEPGRAM && (
<fieldset>
<label htmlFor={`${vendor}_apikey`}>
API key{!isDeepgramOnpremEnabled && <span>*</span>}
</label>
<Passwd
id={`${vendor}_apikey`}
required={!isDeepgramOnpremEnabled}
name={`${vendor}_apikey`}
placeholder="API key"
value={apiKey ? getObscuredSecret(apiKey) : apiKey}
onChange={(e) => setApiKey(e.target.value)}
disabled={credential ? true : false}
/>
</fieldset>
)}
{(vendor == VENDOR_ELEVENLABS ||
vendor == VENDOR_WHISPER ||
vendor == VENDOR_RIMELABS) &&
@@ -1429,6 +1519,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
{regions &&
regions[vendor as keyof RegionVendors] &&
vendor !== VENDOR_IBM &&
vendor !== VENDOR_SPEECHMATICS &&
vendor !== VENDOR_MICROSOFT && (
<fieldset>
<label htmlFor="region">
@@ -1529,11 +1620,52 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
disabled={hasValue(credential)}
hidden
name="use_hosted_deepgram_service"
label="Use hosted Deepgram Service"
initialCheck={!initialDeepgramOnpremCheck}
handleChecked={(e) => {
setInitialDeepgramOnpremCheck(!e.target.checked);
if (e.target.checked) {
setTmpDeepgramSttUri(deepgramSttUri);
setDeepgramSttUri("");
setTmpDeepgramSttUseTls(deepgramSttUseTls);
setDeepgramSttUseTls(false);
setTmpDeepgramTtsUri(deepgramTtsUri);
setDeepgramTtsUri("");
} else {
if (tmpDeepgramSttUri) {
setDeepgramSttUri(tmpDeepgramSttUri);
}
if (tmpDeepgramSttUseTls) {
setDeepgramSttUseTls(tmpDeepgramSttUseTls);
}
if (tmpDeepgramTtsUri) {
setDeepgramTtsUri(tmpDeepgramTtsUri);
}
}
}}
>
<label htmlFor={`${vendor}_apikey`}>
API key<span>*</span>
</label>
<Passwd
id={`${vendor}_apikey`}
required
name={`${vendor}_apikey`}
placeholder="API key"
value={credential ? getObscuredSecret(apiKey) : apiKey}
onChange={(e) => setApiKey(e.target.value)}
disabled={credential ? true : false}
/>
</Checkzone>
<Checkzone
disabled={hasValue(credential)}
hidden
name="use_on-prem_deepgram_container"
label="Use on-prem Deepgram container"
initialCheck={initialDeepgramOnpremCheck}
handleChecked={(e) => {
// setInitialDeepgramOnpremCheck(!e.target.checked);
setIsDeepgramOnpremEnabled(e.target.checked);
setInitialDeepgramOnpremCheck(e.target.checked);
if (e.target.checked) {
if (tmpDeepgramSttUri) {
setDeepgramSttUri(tmpDeepgramSttUri);
@@ -1541,36 +1673,63 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (tmpDeepgramSttUseTls) {
setDeepgramSttUseTls(tmpDeepgramSttUseTls);
}
if (tmpDeepgramTtsUri) {
setDeepgramTtsUri(tmpDeepgramTtsUri);
}
} else {
setTmpDeepgramSttUri(deepgramSttUri);
setDeepgramSttUri("");
setTmpDeepgramSttUseTls(deepgramSttUseTls);
setDeepgramSttUseTls(false);
setTmpDeepgramTtsUri(deepgramTtsUri);
setDeepgramTtsUri("");
}
}}
>
<label htmlFor="deepgram_uri_for_stt">
STT Container URI<span>*</span>
</label>
<input
id="deepgram_uri_for_stt"
required
type="text"
name="deepgram_uri_for_stt"
placeholder="Container URI for STT"
value={deepgramSttUri}
onChange={(e) => setDeepgramSttUri(e.target.value)}
/>
<label htmlFor="deepgram_stt_use_tls" className="chk">
<input
id="deepgram_stt_use_tls"
name="deepgram_stt_use_tls"
type="checkbox"
onChange={(e) => setDeepgramSttUseTls(e.target.checked)}
defaultChecked={deepgramSttUseTls}
/>
<div>Use TLS for STT</div>
</label>
<label htmlFor="deepgram_uri_for_tts">
Container URI<span>*</span>
TTS Container URI<span>*</span>
</label>
<input
id="deepgram_uri_for_tts"
required
type="text"
name="deepgram_uri_for_tts"
placeholder="Container URI for TTS"
value={deepgramSttUri}
onChange={(e) => setDeepgramSttUri(e.target.value)}
placeholder="http://"
value={deepgramTtsUri}
onChange={(e) => setDeepgramTtsUri(e.target.value)}
/>
<label htmlFor={`${vendor}_apikey`}>Api key (if required)</label>
<Passwd
id={`${vendor}_apikey`}
name={`${vendor}_apikey`}
placeholder="API key"
value={credential ? getObscuredSecret(apiKey) : apiKey}
onChange={(e) => setApiKey(e.target.value)}
disabled={credential ? true : false}
/>
<label htmlFor="deepgram_tts_use_tls" className="chk">
<input
id="deepgram_tts_use_tls"
name="deepgram_tts_use_tls"
type="checkbox"
onChange={(e) => setDeepgramSttUseTls(e.target.checked)}
defaultChecked={deepgramSttUseTls}
/>
<div>Use TLS</div>
</label>
</Checkzone>
</fieldset>
)}
@@ -1775,6 +1934,73 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
</fieldset>
</React.Fragment>
)}
{vendor === VENDOR_SPEECHMATICS &&
regions &&
regions[vendor as keyof RegionVendors] && (
<fieldset>
<Checkzone
disabled={hasValue(credential)}
hidden
name="use_hosted_speechmatics_service"
label="Use hosted Speechmatics Service"
initialCheck={!initialSpeechmaticsOnpremCheck}
handleChecked={(e) => {
setInitialSpeechMaticsOnpremCheck(!e.target.checked);
if (e.target.checked) {
setTmpOnpremSpeechmaticsEndpoint(speechmaticsEndpoint);
setSpeechmaticsEndpoint(tmpHostedSpeechmaticsEndpoint);
setTmpHostedSpeechmaticsEndpoint("");
}
}}
>
<label htmlFor="speechmatics_endpoint">
Endpoint {sttCheck && <span>*</span>}
</label>
<Selector
id="speechmatics_endpoint"
name="speechmatics_endpoint"
value={speechmaticsEndpoint}
required
options={[
{
name: "Select a endpoint",
value: "",
},
].concat(regions[vendor as keyof RegionVendors])}
onChange={(e) => setSpeechmaticsEndpoint(e.target.value)}
/>
</Checkzone>
<Checkzone
disabled={hasValue(credential)}
hidden
name="use_on-prem_speechmatics_container"
label="Use on-prem Speechmatics container"
initialCheck={initialSpeechmaticsOnpremCheck}
handleChecked={(e) => {
setInitialSpeechMaticsOnpremCheck(e.target.checked);
if (e.target.checked) {
setTmpHostedSpeechmaticsEndpoint(speechmaticsEndpoint);
setSpeechmaticsEndpoint(tmpOnpremSpeechmaticsEndpoint);
setTmpOnpremSpeechmaticsEndpoint("");
}
}}
>
<label htmlFor="speechmatics_uri_for_stt">
Endpoint URI<span>*</span>
</label>
<input
id="speechmatics_uri_for_stt"
required
type="text"
name="speechmatics_uri_for_stt"
placeholder="Speechmatics URI for STT"
value={speechmaticsEndpoint}
onChange={(e) => setSpeechmaticsEndpoint(e.target.value)}
/>
</Checkzone>
</fieldset>
)}
<fieldset>
<ButtonGroup left>

View File

@@ -89,6 +89,22 @@ export const getIpValidationType = (ipv4: string): IpType => {
return type;
};
function isValidIPV4(ip: string): boolean {
const ipv4Pattern =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
return ipv4Pattern.test(ip);
}
function isValidCIDR(cidr: string): boolean {
const cidrPattern =
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(\/([0-9]|[1-2][0-9]|3[0-2]))$/;
return cidrPattern.test(cidr);
}
export function isvalidIpv4OrCidr(input: string): boolean {
return isValidIPV4(input) || isValidCIDR(input);
}
export const getObscured = (str: string, sub = 4, char = "*") => {
const len = str.length - sub;
const obscured = str.substring(0, len).replace(/[a-zA-Z0-9]/g, char);

13
src/vendor/index.tsx vendored
View File

@@ -15,6 +15,7 @@ export const VENDOR_DEEPGRAM = "deepgram";
export const VENDOR_IBM = "ibm";
export const VENDOR_NVIDIA = "nvidia";
export const VENDOR_SONIOX = "soniox";
export const VENDOR_SPEECHMATICS = "speechmatics";
export const VENDOR_CUSTOM = "custom";
export const VENDOR_COBALT = "cobalt";
export const VENDOR_ELEVENLABS = "elevenlabs";
@@ -22,6 +23,7 @@ export const VENDOR_ASSEMBLYAI = "assemblyai";
export const VENDOR_WHISPER = "whisper";
export const VENDOR_PLAYHT = "playht";
export const VENDOR_RIMELABS = "rimelabs";
export const VENDOR_VERBIO = "verbio";
export const vendors: VendorOptions[] = [
{
@@ -60,6 +62,10 @@ export const vendors: VendorOptions[] = [
name: "Soniox",
value: VENDOR_SONIOX,
},
{
name: "Speechmatics",
value: VENDOR_SPEECHMATICS,
},
{
name: "Custom",
value: VENDOR_CUSTOM,
@@ -88,6 +94,10 @@ export const vendors: VendorOptions[] = [
name: "RimeLabs",
value: VENDOR_RIMELABS,
},
{
name: "Verbio",
value: VENDOR_VERBIO,
},
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
export const AWS_CREDENTIAL_ACCESS_KEY = "access_key";
@@ -119,17 +129,20 @@ export const useRegionVendors = () => {
import("./regions/aws-regions"),
import("./regions/ms-azure-regions"),
import("./regions/ibm-regions"),
import("./regions/speechmatics-regions"),
]).then(
([
{ default: awsRegions },
{ default: msRegions },
{ default: ibmRegions },
{ default: speechmaticsRegions },
]) => {
if (!ignore) {
setRegions({
aws: awsRegions,
microsoft: msRegions,
ibm: ibmRegions,
speechmatics: speechmaticsRegions,
});
}
},

View File

@@ -0,0 +1,18 @@
import type { Region } from "../types";
export const regions: Region[] = [
{
name: "EU (EU2 - On-demand)",
value: "eu2.rt.speechmatics.com",
},
{
name: "EU (EU1 - Enterprise)",
value: "neu.rt.speechmatics.com",
},
{
name: "US (US1 - Enterprise)",
value: "wus.rt.speechmatics.com",
},
];
export default regions;

7
src/vendor/types.ts vendored
View File

@@ -8,13 +8,15 @@ export type Vendor =
| "IBM"
| "Nvidia"
| "Soniox"
| "Speechmatics"
| "Cobalt"
| "Custom"
| "ElevenLabs"
| "assemblyai"
| "whisper"
| "playht"
| "rimelabs";
| "rimelabs"
| "verbio";
export interface VendorOptions {
name: Vendor;
@@ -70,6 +72,7 @@ export interface RegionVendors {
aws: Region[];
microsoft: Region[];
ibm: Region[];
speechmatics: Region[];
}
export interface TtsModels {
@@ -87,6 +90,7 @@ export interface RecognizerVendors {
ibm: Language[];
nvidia: Language[];
soniox: Language[];
speechmatics: Language[];
cobalt: Language[];
assemblyai: Language[];
}
@@ -102,6 +106,7 @@ export interface SynthesisVendors {
elevenlabs: VoiceLanguage[];
whisper: VoiceLanguage[];
deepgram: VoiceLanguage[];
playht: VoiceLanguage[];
}
export interface MSRawSpeech {