mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
Compare commits
11 Commits
v0.9.1
...
v0.9.3-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d4ffddddc | ||
|
|
294b7b2058 | ||
|
|
e32664d0e0 | ||
|
|
ae8b4ae124 | ||
|
|
a586771ea6 | ||
|
|
7aaea04d3c | ||
|
|
f1d2ed8abd | ||
|
|
d7d61a769d | ||
|
|
c9da7946f3 | ||
|
|
5755cd8886 | ||
|
|
786327a0b9 |
6
.env
6
.env
@@ -1,4 +1,4 @@
|
||||
#VITE_API_BASE_URL=http://127.0.0.1:3000/v1
|
||||
VITE_API_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
|
||||
@@ -25,4 +25,6 @@
|
||||
## 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
|
||||
@@ -2,6 +2,7 @@ import { hasValue } from "src/utils";
|
||||
import type {
|
||||
Currency,
|
||||
ElevenLabsOptions,
|
||||
GoogleCustomVoice,
|
||||
LimitField,
|
||||
LimitUnitOption,
|
||||
PasswordSettings,
|
||||
@@ -13,6 +14,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` */
|
||||
@@ -29,6 +31,7 @@ interface JambonzWindowObject {
|
||||
BASE_URL: string;
|
||||
DEFAULT_SERVICE_PROVIDER_SID: string;
|
||||
STRIPE_PUBLISHABLE_KEY: string;
|
||||
DISABLE_ADDITIONAL_SPEECH_VENDORS: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -76,6 +79,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;
|
||||
@@ -205,6 +215,17 @@ 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";
|
||||
@@ -217,6 +238,8 @@ export const VERBIO_STT_MODELS = [
|
||||
|
||||
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";
|
||||
@@ -225,6 +248,13 @@ export const GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = [
|
||||
{ name: "REALTIME", value: "REALTIME" },
|
||||
{ name: "OFFLINE", value: "OFFLINE" },
|
||||
];
|
||||
export const DEFAULT_GOOGLE_CUSTOM_VOICE: GoogleCustomVoice = {
|
||||
name: "",
|
||||
reported_usage: DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
model: "",
|
||||
use_voice_cloning_key: 0,
|
||||
voice_cloning_key_file: null,
|
||||
};
|
||||
// ElevenLabs options
|
||||
export const DEFAULT_ELEVENLABS_OPTIONS: Partial<ElevenLabsOptions> = {
|
||||
optimize_streaming_latency: 3,
|
||||
@@ -283,6 +313,11 @@ export const USER_SCOPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "Account", value: "account" },
|
||||
];
|
||||
|
||||
export const DTMF_TYPE_SELECTION: SelectorOptions[] = [
|
||||
{ name: "RFC 2833", value: "rfc2833" },
|
||||
{ name: "Tones", value: "tones" },
|
||||
];
|
||||
|
||||
/** Available webhook methods */
|
||||
export const WEBHOOK_METHODS: WebhookOption[] = [
|
||||
{
|
||||
|
||||
@@ -225,6 +225,16 @@ export const getBlob = (url: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const postBlobFetch = <Type>(url: string, formdata?: FormData) => {
|
||||
return fetchTransport<Type>(url, {
|
||||
method: "POST",
|
||||
body: formdata,
|
||||
headers: {
|
||||
Authorization: `Bearer ${getToken()}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/** Simple wrappers for fetchTransport calls to any API, :GET, :POST, :PUT, :DELETE */
|
||||
|
||||
export const getFetch = <Type>(url: string) => {
|
||||
@@ -492,6 +502,15 @@ export const postGoogleCustomVoice = (payload: Partial<GoogleCustomVoice>) => {
|
||||
payload,
|
||||
);
|
||||
};
|
||||
|
||||
export const postGoogleVoiceCloningKey = (sid: string, file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return postBlobFetch<EmptyResponse>(
|
||||
`${API_GOOGLE_CUSTOM_VOICES}/${sid}/VoiceCloningKey`,
|
||||
formData,
|
||||
);
|
||||
};
|
||||
/** Named wrappers for `putFetch` */
|
||||
|
||||
export const putUser = (sid: string, payload: Partial<UserUpdatePayload>) => {
|
||||
@@ -875,6 +894,7 @@ export const getSpeechSupportedLanguagesAndVoices = (
|
||||
sid: string | undefined,
|
||||
vendor: string,
|
||||
label: string,
|
||||
create_new: boolean = false,
|
||||
) => {
|
||||
const userData = parseJwt(getToken());
|
||||
const apiUrl =
|
||||
@@ -883,7 +903,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);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -376,7 +381,10 @@ export interface GoogleCustomVoice {
|
||||
speech_credential_sid?: string;
|
||||
name: string;
|
||||
reported_usage: string;
|
||||
model: string;
|
||||
model?: string;
|
||||
use_voice_cloning_key: number;
|
||||
voice_cloning_key?: string | null;
|
||||
voice_cloning_key_file?: File | null;
|
||||
}
|
||||
|
||||
export interface SpeechCredential {
|
||||
@@ -423,7 +431,9 @@ export interface SpeechCredential {
|
||||
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 {
|
||||
@@ -441,6 +451,8 @@ export interface CarrierRegisterStatus {
|
||||
callId: null | string;
|
||||
}
|
||||
|
||||
export type DtmfType = "rfc2833" | "tones" | "info";
|
||||
|
||||
export interface Carrier {
|
||||
voip_carrier_sid: string;
|
||||
name: string;
|
||||
@@ -467,6 +479,7 @@ export interface Carrier {
|
||||
smpp_inbound_password: null | string;
|
||||
smpp_enquire_link_interval: number;
|
||||
register_status: CarrierRegisterStatus;
|
||||
dtmf_type: DtmfType;
|
||||
}
|
||||
|
||||
export interface PredefinedCarrier extends Carrier {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import {
|
||||
DEFAULT_SIP_GATEWAY,
|
||||
DEFAULT_SMPP_GATEWAY,
|
||||
DTMF_TYPE_SELECTION,
|
||||
FQDN,
|
||||
FQDN_TOP_LEVEL,
|
||||
INVALID,
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
TECH_PREFIX_MINLENGTH,
|
||||
USER_ACCOUNT,
|
||||
} from "src/api/constants";
|
||||
import { Icons, Section } from "src/components";
|
||||
import { Icons, Section, Tooltip } from "src/components";
|
||||
import {
|
||||
Checkzone,
|
||||
Message,
|
||||
@@ -52,16 +53,17 @@ import {
|
||||
isNotBlank,
|
||||
} from "src/utils";
|
||||
|
||||
import type {
|
||||
Account,
|
||||
UseApiDataMap,
|
||||
Carrier,
|
||||
SipGateway,
|
||||
SmppGateway,
|
||||
PredefinedCarrier,
|
||||
Sbc,
|
||||
Smpp,
|
||||
Application,
|
||||
import {
|
||||
type Account,
|
||||
type UseApiDataMap,
|
||||
type Carrier,
|
||||
type SipGateway,
|
||||
type SmppGateway,
|
||||
type PredefinedCarrier,
|
||||
type Sbc,
|
||||
type Smpp,
|
||||
type Application,
|
||||
DtmfType,
|
||||
} from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import { RegisterStatus } from "./register-status";
|
||||
@@ -101,6 +103,7 @@ export const CarrierForm = ({
|
||||
const [e164, setE164] = useState(false);
|
||||
const [applicationSid, setApplicationSid] = useState("");
|
||||
const [accountSid, setAccountSid] = useState("");
|
||||
const [dtmfType, setDtmfType] = useState<DtmfType>("rfc2833");
|
||||
|
||||
const [sipRegister, setSipRegister] = useState(false);
|
||||
const [sipUser, setSipUser] = useState("");
|
||||
@@ -216,6 +219,9 @@ export const CarrierForm = ({
|
||||
if (obj.smpp_inbound_password) {
|
||||
setSmppInboundPass(obj.smpp_inbound_password);
|
||||
}
|
||||
if (obj.dtmf_type) {
|
||||
setDtmfType(obj.dtmf_type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -524,6 +530,7 @@ export const CarrierForm = ({
|
||||
smpp_password: smppPass.trim() || null,
|
||||
smpp_inbound_system_id: smppInboundSystemId.trim() || null,
|
||||
smpp_inbound_password: smppInboundPass.trim() || null,
|
||||
dtmf_type: dtmfType,
|
||||
};
|
||||
|
||||
if (carrier && carrier.data) {
|
||||
@@ -764,6 +771,23 @@ export const CarrierForm = ({
|
||||
: false
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="dtmf_type">
|
||||
<Tooltip
|
||||
text={
|
||||
"RFC 2833 is commonly used on VoIP networks. Do not change unless you are certain this carrier does not support it"
|
||||
}
|
||||
>
|
||||
DTMF type
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Selector
|
||||
id="dtmf_type"
|
||||
name="dtmf_type"
|
||||
value={dtmfType}
|
||||
options={DTMF_TYPE_SELECTION}
|
||||
onChange={(e) => setDtmfType(e.target.value as DtmfType)}
|
||||
/>
|
||||
{user &&
|
||||
disableDefaultTrunkRouting(user?.scope) &&
|
||||
accountSid &&
|
||||
@@ -1096,29 +1120,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
|
||||
|
||||
@@ -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))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -157,7 +157,10 @@ export const RecentCalls = () => {
|
||||
{!hasValue(calls) && hasLength(accounts) ? (
|
||||
<Spinner />
|
||||
) : hasLength(calls) ? (
|
||||
calls.map((call) => <DetailsItem key={call.call_sid} call={call} />)
|
||||
//call.call_sid is null incase of failure, cannot be used as key
|
||||
calls.map((call) => (
|
||||
<DetailsItem key={call.sip_callid} call={call} />
|
||||
))
|
||||
) : (
|
||||
<M>No data.</M>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
getGoogleCustomVoices,
|
||||
getSpeechSupportedLanguagesAndVoices,
|
||||
postGoogleCustomVoice,
|
||||
postGoogleVoiceCloningKey,
|
||||
postSpeechService,
|
||||
putGoogleCustomVoice,
|
||||
putSpeechService,
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
AWS_CREDENTIAL_ACCESS_KEY,
|
||||
AWS_INSTANCE_PROFILE,
|
||||
VENDOR_VERBIO,
|
||||
VENDOR_SPEECHMATICS,
|
||||
} from "src/vendor";
|
||||
import { MSG_REQUIRED_FIELDS } from "src/constants";
|
||||
import {
|
||||
@@ -74,11 +76,13 @@ 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_GOOGLE_CUSTOM_VOICE,
|
||||
DEFAULT_PLAYHT_OPTIONS,
|
||||
DEFAULT_RIMELABS_OPTIONS,
|
||||
DEFAULT_VERBIO_MODEL,
|
||||
DISABLE_ADDITIONAL_SPEECH_VENDORS,
|
||||
DISABLE_CUSTOM_SPEECH,
|
||||
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
VERBIO_STT_MODELS,
|
||||
@@ -160,12 +164,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,
|
||||
);
|
||||
@@ -232,14 +244,43 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
if (useCustomVoicesCheck) {
|
||||
Promise.all(
|
||||
customVoices.map((v) => {
|
||||
// voice cloning key is 200kb file, the content should be uploaded in separated api
|
||||
const voice_cloning_key = v.voice_cloning_key_file;
|
||||
delete v.voice_cloning_key_file;
|
||||
delete v.voice_cloning_key;
|
||||
|
||||
const uploadVoiceCloningKey = (sid: string) => {
|
||||
if (voice_cloning_key) {
|
||||
return postGoogleVoiceCloningKey(sid, voice_cloning_key);
|
||||
}
|
||||
};
|
||||
|
||||
if (v.google_custom_voice_sid) {
|
||||
const sid = v.google_custom_voice_sid;
|
||||
delete v.google_custom_voice_sid;
|
||||
return putGoogleCustomVoice(sid, v);
|
||||
return new Promise((res, rej) => {
|
||||
putGoogleCustomVoice(sid, v)
|
||||
.then((resp) => {
|
||||
if (!voice_cloning_key) {
|
||||
return res(resp);
|
||||
}
|
||||
uploadVoiceCloningKey(sid)?.then(res).catch(rej);
|
||||
})
|
||||
.catch(rej);
|
||||
});
|
||||
} else {
|
||||
return postGoogleCustomVoice({
|
||||
...v,
|
||||
speech_credential_sid: credential.data?.speech_credential_sid,
|
||||
return new Promise((res, rej) => {
|
||||
postGoogleCustomVoice({
|
||||
...v,
|
||||
speech_credential_sid: credential.data?.speech_credential_sid,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (!voice_cloning_key) {
|
||||
return res(json);
|
||||
}
|
||||
uploadVoiceCloningKey(json.sid)?.then(res).catch(rej);
|
||||
})
|
||||
.catch(rej);
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -254,7 +295,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else if (useCustomVoicesCheck && customVoices.length > 0) {
|
||||
} else if (!useCustomVoicesCheck && customVoices.length > 0) {
|
||||
Promise.all(
|
||||
customVoices.map((v) => {
|
||||
if (v.google_custom_voice_sid) {
|
||||
@@ -356,8 +397,12 @@ 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,
|
||||
}),
|
||||
@@ -402,6 +447,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 ||
|
||||
@@ -422,12 +468,32 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
.then(({ json }) => {
|
||||
if (vendor === VENDOR_GOOGLE && useCustomVoicesCheck) {
|
||||
Promise.all(
|
||||
customVoices.map((v) =>
|
||||
postGoogleCustomVoice({
|
||||
...v,
|
||||
speech_credential_sid: json.sid,
|
||||
}),
|
||||
),
|
||||
customVoices.map((v) => {
|
||||
// voice cloning key is 200kb file, the content should be uploaded in separated api
|
||||
const voice_cloning_key = v.voice_cloning_key_file;
|
||||
delete v.voice_cloning_key_file;
|
||||
delete v.voice_cloning_key;
|
||||
|
||||
const uploadVoiceCloningKey = (sid: string) => {
|
||||
if (voice_cloning_key) {
|
||||
return postGoogleVoiceCloningKey(sid, voice_cloning_key);
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
postGoogleCustomVoice({
|
||||
...v,
|
||||
speech_credential_sid: json.sid,
|
||||
})
|
||||
.then(({ json }) => {
|
||||
if (!voice_cloning_key) {
|
||||
res(json);
|
||||
}
|
||||
uploadVoiceCloningKey(json.sid)?.then(res).catch(rej);
|
||||
})
|
||||
.catch(rej);
|
||||
});
|
||||
}),
|
||||
).then(() => {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(ROUTE_INTERNAL_SPEECH);
|
||||
@@ -457,6 +523,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
currentServiceProvider?.service_provider_sid,
|
||||
vendor,
|
||||
"",
|
||||
credential ? false : true,
|
||||
).then(({ json }) => {
|
||||
if (json.models) {
|
||||
setTtsModels(json.models);
|
||||
@@ -632,13 +699,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);
|
||||
@@ -668,6 +737,13 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
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 = (
|
||||
@@ -720,7 +796,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>);
|
||||
@@ -779,6 +860,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
|
||||
@@ -946,15 +1028,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
name="use_custom_voice"
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
if (customVoices.length === 0) {
|
||||
setCustomVoices([
|
||||
{
|
||||
name: "",
|
||||
reported_usage:
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
model: "",
|
||||
},
|
||||
]);
|
||||
if (e.target.checked && customVoices.length === 0) {
|
||||
setCustomVoices([DEFAULT_GOOGLE_CUSTOM_VOICE]);
|
||||
}
|
||||
setUseCustomVoicesCheck(e.target.checked);
|
||||
}}
|
||||
@@ -977,7 +1052,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
<div>
|
||||
<div>
|
||||
<label htmlFor="custom_voice_name">
|
||||
Name / Reported Usage
|
||||
Name
|
||||
{!v.use_voice_cloning_key
|
||||
? " / Reported Usage"
|
||||
: ""}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -997,49 +1075,112 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Selector
|
||||
id={"google_custom_voices_reported_usage"}
|
||||
name={"google_custom_voices_reported_usage"}
|
||||
value={v.reported_usage}
|
||||
options={GOOGLE_CUSTOM_VOICES_REPORTED_USAGE}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"reported_usage",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!v.use_voice_cloning_key && (
|
||||
<div>
|
||||
<Selector
|
||||
id={"google_custom_voices_reported_usage"}
|
||||
name={"google_custom_voices_reported_usage"}
|
||||
value={v.reported_usage}
|
||||
options={GOOGLE_CUSTOM_VOICES_REPORTED_USAGE}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"reported_usage",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<label htmlFor="custom_voice_name">Model</label>
|
||||
</div>
|
||||
</div>
|
||||
<label
|
||||
htmlFor={`use_voice_cloning_key_${i}`}
|
||||
className="chk"
|
||||
>
|
||||
<input
|
||||
id={`use_voice_cloning_key_${i}`}
|
||||
name={`use_voice_cloning_key_${i}`}
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"use_voice_cloning_key",
|
||||
e.target.checked ? 1 : 0,
|
||||
);
|
||||
}}
|
||||
checked={v.use_voice_cloning_key ? true : false}
|
||||
/>
|
||||
<div>Use voice cloning key</div>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`sip_ip_${i}`}
|
||||
name={`sip_ip_${i}`}
|
||||
type="text"
|
||||
placeholder="Model"
|
||||
required
|
||||
value={v.model}
|
||||
style={{ maxWidth: "100%" }}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"model",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!v.use_voice_cloning_key && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<label htmlFor="custom_voice_name">
|
||||
Model
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<input
|
||||
id={`sip_ip_${i}`}
|
||||
name={`sip_ip_${i}`}
|
||||
type="text"
|
||||
placeholder="Model"
|
||||
required
|
||||
value={v.model}
|
||||
style={{ maxWidth: "100%" }}
|
||||
onChange={(e) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"model",
|
||||
e.target.value,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{v.use_voice_cloning_key === 1 && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
{hasValue(v.voice_cloning_key) && (
|
||||
<pre>
|
||||
<code>{v.voice_cloning_key}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<FileUpload
|
||||
id={`google_voice_cloning_key_${i}`}
|
||||
name={`google_voice_cloning_key_${i}`}
|
||||
handleFile={(file) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"voice_cloning_key_file",
|
||||
file,
|
||||
);
|
||||
file.text().then((text) => {
|
||||
updateCustomVoices(
|
||||
i,
|
||||
"voice_cloning_key",
|
||||
text.substring(0, 100) + "...",
|
||||
);
|
||||
});
|
||||
}}
|
||||
required={!v.voice_cloning_key}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="btnty"
|
||||
@@ -1080,12 +1221,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setCustomVoicesMessage("");
|
||||
setCustomVoices((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: "",
|
||||
reported_usage:
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
model: "",
|
||||
},
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICE,
|
||||
]);
|
||||
}}
|
||||
>
|
||||
@@ -1335,7 +1471,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 && (
|
||||
<>
|
||||
@@ -1384,22 +1521,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) &&
|
||||
@@ -1502,6 +1623,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">
|
||||
@@ -1602,11 +1724,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);
|
||||
@@ -1614,36 +1777,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>
|
||||
)}
|
||||
@@ -1848,6 +2038,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>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
} from "src/api";
|
||||
import { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
|
||||
import {
|
||||
getHumanDateTime,
|
||||
isUserAccountScope,
|
||||
hasLength,
|
||||
hasValue,
|
||||
@@ -178,24 +177,6 @@ export const SpeechServices = () => {
|
||||
<span>{getUsage(credential)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={`i txt--${
|
||||
credential.last_used ? "teal" : "grey"
|
||||
}`}
|
||||
>
|
||||
{credential.last_used ? (
|
||||
<Icons.CheckCircle />
|
||||
) : (
|
||||
<Icons.XCircle />
|
||||
)}
|
||||
<span>
|
||||
{credential.last_used
|
||||
? getHumanDateTime(credential.last_used)
|
||||
: "Never used"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CredentialStatus cred={credential} />
|
||||
</div>
|
||||
|
||||
@@ -329,6 +329,11 @@ fieldset {
|
||||
grid-template-columns: [col] 100%;
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
grid-template-columns: [col] 100%;
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
|
||||
@@ -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);
|
||||
|
||||
8
src/vendor/index.tsx
vendored
8
src/vendor/index.tsx
vendored
@@ -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";
|
||||
@@ -61,6 +62,10 @@ export const vendors: VendorOptions[] = [
|
||||
name: "Soniox",
|
||||
value: VENDOR_SONIOX,
|
||||
},
|
||||
{
|
||||
name: "Speechmatics",
|
||||
value: VENDOR_SPEECHMATICS,
|
||||
},
|
||||
{
|
||||
name: "Custom",
|
||||
value: VENDOR_CUSTOM,
|
||||
@@ -124,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,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
18
src/vendor/regions/speechmatics-regions.ts
vendored
Normal file
18
src/vendor/regions/speechmatics-regions.ts
vendored
Normal 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;
|
||||
4
src/vendor/types.ts
vendored
4
src/vendor/types.ts
vendored
@@ -8,6 +8,7 @@ export type Vendor =
|
||||
| "IBM"
|
||||
| "Nvidia"
|
||||
| "Soniox"
|
||||
| "Speechmatics"
|
||||
| "Cobalt"
|
||||
| "Custom"
|
||||
| "ElevenLabs"
|
||||
@@ -71,6 +72,7 @@ export interface RegionVendors {
|
||||
aws: Region[];
|
||||
microsoft: Region[];
|
||||
ibm: Region[];
|
||||
speechmatics: Region[];
|
||||
}
|
||||
|
||||
export interface TtsModels {
|
||||
@@ -88,6 +90,7 @@ export interface RecognizerVendors {
|
||||
ibm: Language[];
|
||||
nvidia: Language[];
|
||||
soniox: Language[];
|
||||
speechmatics: Language[];
|
||||
cobalt: Language[];
|
||||
assemblyai: Language[];
|
||||
}
|
||||
@@ -103,6 +106,7 @@ export interface SynthesisVendors {
|
||||
elevenlabs: VoiceLanguage[];
|
||||
whisper: VoiceLanguage[];
|
||||
deepgram: VoiceLanguage[];
|
||||
playht: VoiceLanguage[];
|
||||
}
|
||||
|
||||
export interface MSRawSpeech {
|
||||
|
||||
Reference in New Issue
Block a user