mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-01-25 02:08:19 +00:00
@@ -432,6 +432,13 @@ export const DEEPGRAM_STT_ENPOINT = [
|
|||||||
{ name: "EU-hosted", value: "api.eu.deepgram.com" },
|
{ name: "EU-hosted", value: "api.eu.deepgram.com" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ElevenLabs API URI options
|
||||||
|
export const ELEVENLABS_API_URI_OPTIONS = [
|
||||||
|
{ name: "US", value: "api.elevenlabs.io" },
|
||||||
|
{ name: "EU", value: "api.eu.residency.elevenlabs.io" },
|
||||||
|
{ name: "IN", value: "api.in.residency.elevenlabs.io" },
|
||||||
|
];
|
||||||
|
|
||||||
/** User scope values values */
|
/** User scope values values */
|
||||||
export const USER_ADMIN = "admin";
|
export const USER_ADMIN = "admin";
|
||||||
export const USER_SP = "service_provider";
|
export const USER_SP = "service_provider";
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ import {
|
|||||||
DEFAULT_VERBIO_MODEL,
|
DEFAULT_VERBIO_MODEL,
|
||||||
DISABLE_ADDITIONAL_SPEECH_VENDORS,
|
DISABLE_ADDITIONAL_SPEECH_VENDORS,
|
||||||
DISABLE_CUSTOM_SPEECH,
|
DISABLE_CUSTOM_SPEECH,
|
||||||
|
ELEVENLABS_API_URI_OPTIONS,
|
||||||
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||||
VERBIO_STT_MODELS,
|
VERBIO_STT_MODELS,
|
||||||
} from "src/api/constants";
|
} from "src/api/constants";
|
||||||
@@ -110,13 +111,6 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
|||||||
const { toastError, toastSuccess } = useToast();
|
const { toastError, toastSuccess } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useSelectState("user");
|
const user = useSelectState("user");
|
||||||
|
|
||||||
// ElevenLabs API URI options
|
|
||||||
const ELEVENLABS_API_URI_OPTIONS = [
|
|
||||||
{ name: "US", value: "api.elevenlabs.io" },
|
|
||||||
{ name: "EU", value: "api.eu.residency.elevenlabs.io" },
|
|
||||||
{ name: "IN", value: "api.in.residency.elevenlabs.io" },
|
|
||||||
];
|
|
||||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||||
const regions = useRegionVendors();
|
const regions = useRegionVendors();
|
||||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||||
@@ -418,6 +412,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
|||||||
...(vendor === VENDOR_AWS && {
|
...(vendor === VENDOR_AWS && {
|
||||||
aws_region: region || null,
|
aws_region: region || null,
|
||||||
}),
|
}),
|
||||||
|
...(vendor === VENDOR_GOOGLE && {
|
||||||
|
model_id: ttsModelId || null,
|
||||||
|
}),
|
||||||
...(vendor === VENDOR_MICROSOFT && {
|
...(vendor === VENDOR_MICROSOFT && {
|
||||||
region: region || null,
|
region: region || null,
|
||||||
use_custom_tts:
|
use_custom_tts:
|
||||||
@@ -852,6 +849,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
|||||||
setOptionsInitialChecked(true);
|
setOptionsInitialChecked(true);
|
||||||
}
|
}
|
||||||
if (credential?.data?.vendor === VENDOR_GOOGLE) {
|
if (credential?.data?.vendor === VENDOR_GOOGLE) {
|
||||||
|
// Load model_id for Gemini TTS
|
||||||
|
if (credential.data.model_id) {
|
||||||
|
setTtsModelId(credential.data.model_id);
|
||||||
|
}
|
||||||
// let try to check if there is custom voices
|
// let try to check if there is custom voices
|
||||||
getGoogleCustomVoices({
|
getGoogleCustomVoices({
|
||||||
speech_credential_sid: credential.data.speech_credential_sid,
|
speech_credential_sid: credential.data.speech_credential_sid,
|
||||||
@@ -1236,218 +1237,245 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
{ttsCheck && vendor === VENDOR_GOOGLE && (
|
{ttsCheck && vendor === VENDOR_GOOGLE && (
|
||||||
<fieldset>
|
<>
|
||||||
<label htmlFor="use_custom_voice" className="chk">
|
<fieldset>
|
||||||
|
<label htmlFor="google_tts_model_id">
|
||||||
|
Model ID
|
||||||
|
<Tooltip text="Provide a model ID to enable Gemini TTS (e.g., gemini-2.5-flash-tts). Leave empty to use standard Google TTS.">
|
||||||
|
{" "}
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
id="use_custom_voice"
|
id="google_tts_model_id"
|
||||||
name="use_custom_voice"
|
name="google_tts_model_id"
|
||||||
type="checkbox"
|
type="text"
|
||||||
onChange={(e) => {
|
placeholder="e.g., gemini-2.5-flash-tts"
|
||||||
if (e.target.checked && customVoices.length === 0) {
|
value={ttsModelId}
|
||||||
setCustomVoices([DEFAULT_GOOGLE_CUSTOM_VOICE]);
|
onChange={(e) => setTtsModelId(e.target.value)}
|
||||||
}
|
|
||||||
setUseCustomVoicesCheck(e.target.checked);
|
|
||||||
}}
|
|
||||||
checked={useCustomVoicesCheck}
|
|
||||||
/>
|
/>
|
||||||
<div>Use custom voices</div>
|
</fieldset>
|
||||||
</label>
|
<fieldset>
|
||||||
{useCustomVoicesCheck && (
|
<label htmlFor="use_custom_voice" className="chk">
|
||||||
<fieldset>
|
<input
|
||||||
<label htmlFor="sip_gateways">Custom Voices</label>
|
id="use_custom_voice"
|
||||||
<MXS>
|
name="use_custom_voice"
|
||||||
<em>At least one Custom voice is required.</em>
|
type="checkbox"
|
||||||
</MXS>
|
onChange={(e) => {
|
||||||
{customVoicesMessage && (
|
if (e.target.checked && customVoices.length === 0) {
|
||||||
<Message message={customVoicesMessage} />
|
setCustomVoices([DEFAULT_GOOGLE_CUSTOM_VOICE]);
|
||||||
)}
|
}
|
||||||
{hasLength(customVoices) &&
|
setUseCustomVoicesCheck(e.target.checked);
|
||||||
customVoices.map((v, i) => (
|
}}
|
||||||
<div key={`custom_voice_${i}`} className="customVoice">
|
checked={useCustomVoicesCheck}
|
||||||
<div>
|
/>
|
||||||
|
<div>Use custom voices</div>
|
||||||
|
</label>
|
||||||
|
{useCustomVoicesCheck && (
|
||||||
|
<fieldset>
|
||||||
|
<label htmlFor="sip_gateways">Custom Voices</label>
|
||||||
|
<MXS>
|
||||||
|
<em>At least one Custom voice is required.</em>
|
||||||
|
</MXS>
|
||||||
|
{customVoicesMessage && (
|
||||||
|
<Message message={customVoicesMessage} />
|
||||||
|
)}
|
||||||
|
{hasLength(customVoices) &&
|
||||||
|
customVoices.map((v, i) => (
|
||||||
|
<div
|
||||||
|
key={`custom_voice_${i}`}
|
||||||
|
className="customVoice"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="custom_voice_name">
|
|
||||||
Name
|
|
||||||
{!v.use_voice_cloning_key
|
|
||||||
? " / Reported Usage"
|
|
||||||
: ""}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
id={`sip_ip_${i}`}
|
|
||||||
name={`sip_ip_${i}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Assigned Name"
|
|
||||||
required
|
|
||||||
value={v.name}
|
|
||||||
onChange={(e) => {
|
|
||||||
updateCustomVoices(i, "name", e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!v.use_voice_cloning_key && (
|
|
||||||
<div>
|
<div>
|
||||||
<Selector
|
<label htmlFor="custom_voice_name">
|
||||||
id={"google_custom_voices_reported_usage"}
|
Name
|
||||||
name={"google_custom_voices_reported_usage"}
|
{!v.use_voice_cloning_key
|
||||||
value={v.reported_usage}
|
? " / Reported Usage"
|
||||||
options={GOOGLE_CUSTOM_VOICES_REPORTED_USAGE}
|
: ""}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id={`sip_ip_${i}`}
|
||||||
|
name={`sip_ip_${i}`}
|
||||||
|
type="text"
|
||||||
|
placeholder="Assigned Name"
|
||||||
|
required
|
||||||
|
value={v.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateCustomVoices(
|
updateCustomVoices(
|
||||||
i,
|
i,
|
||||||
"reported_usage",
|
"name",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label
|
{!v.use_voice_cloning_key && (
|
||||||
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>
|
|
||||||
|
|
||||||
{!v.use_voice_cloning_key && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="custom_voice_name">
|
<Selector
|
||||||
Model
|
id={"google_custom_voices_reported_usage"}
|
||||||
</label>
|
name={"google_custom_voices_reported_usage"}
|
||||||
</div>
|
value={v.reported_usage}
|
||||||
</div>
|
options={
|
||||||
|
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE
|
||||||
<div>
|
}
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
id={`sip_ip_${i}`}
|
|
||||||
name={`sip_ip_${i}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Model"
|
|
||||||
required
|
|
||||||
value={v.model}
|
|
||||||
style={{ maxWidth: "100%" }}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
updateCustomVoices(
|
updateCustomVoices(
|
||||||
i,
|
i,
|
||||||
"model",
|
"reported_usage",
|
||||||
e.target.value,
|
e.target.value,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{v.use_voice_cloning_key === 1 && (
|
<label
|
||||||
<>
|
htmlFor={`use_voice_cloning_key_${i}`}
|
||||||
<div>
|
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>
|
||||||
|
|
||||||
|
{!v.use_voice_cloning_key && (
|
||||||
|
<>
|
||||||
<div>
|
<div>
|
||||||
{hasValue(v.voice_cloning_key) && (
|
<div>
|
||||||
<pre>
|
<label htmlFor="custom_voice_name">
|
||||||
<code>{v.voice_cloning_key}</code>
|
Model
|
||||||
</pre>
|
</label>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FileUpload
|
<div>
|
||||||
id={`google_voice_cloning_key_${i}`}
|
<input
|
||||||
name={`google_voice_cloning_key_${i}`}
|
id={`sip_ip_${i}`}
|
||||||
handleFile={(file) => {
|
name={`sip_ip_${i}`}
|
||||||
updateCustomVoices(
|
type="text"
|
||||||
i,
|
placeholder="Model"
|
||||||
"voice_cloning_key_file",
|
required
|
||||||
file,
|
value={v.model}
|
||||||
);
|
style={{ maxWidth: "100%" }}
|
||||||
file.text().then((text) => {
|
onChange={(e) => {
|
||||||
updateCustomVoices(
|
updateCustomVoices(
|
||||||
i,
|
i,
|
||||||
"voice_cloning_key",
|
"model",
|
||||||
text.substring(0, 100) + "...",
|
e.target.value,
|
||||||
);
|
);
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
required={!v.voice_cloning_key}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
{v.use_voice_cloning_key === 1 && (
|
||||||
className="btnty"
|
<>
|
||||||
title="Delete custom voice"
|
<div>
|
||||||
type="button"
|
<div>
|
||||||
onClick={() => {
|
{hasValue(v.voice_cloning_key) && (
|
||||||
setCustomVoicesMessage("");
|
<pre>
|
||||||
if (customVoices.length === 1) {
|
<code>{v.voice_cloning_key}</code>
|
||||||
setCustomVoicesMessage(
|
</pre>
|
||||||
"You must provide at least one custom voice.",
|
)}
|
||||||
|
</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"
|
||||||
|
title="Delete custom voice"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setCustomVoicesMessage("");
|
||||||
|
if (customVoices.length === 1) {
|
||||||
|
setCustomVoicesMessage(
|
||||||
|
"You must provide at least one custom voice.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (v.google_custom_voice_sid) {
|
||||||
|
deleteGoogleCustomVoice(
|
||||||
|
v.google_custom_voice_sid,
|
||||||
|
).finally(() => {
|
||||||
|
credential?.refetch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setCustomVoices((prev) =>
|
||||||
|
prev.filter((_, idx) => idx !== i),
|
||||||
);
|
);
|
||||||
return;
|
}}
|
||||||
}
|
>
|
||||||
if (v.google_custom_voice_sid) {
|
<Icon>
|
||||||
deleteGoogleCustomVoice(
|
<Icons.Trash2 />
|
||||||
v.google_custom_voice_sid,
|
</Icon>
|
||||||
).finally(() => {
|
</button>
|
||||||
credential?.refetch();
|
</div>
|
||||||
});
|
))}
|
||||||
}
|
<ButtonGroup left>
|
||||||
setCustomVoices((prev) =>
|
<button
|
||||||
prev.filter((_, idx) => idx !== i),
|
className="btnty"
|
||||||
);
|
type="button"
|
||||||
}}
|
title="Add Voice"
|
||||||
>
|
onClick={() => {
|
||||||
<Icon>
|
setCustomVoicesMessage("");
|
||||||
<Icons.Trash2 />
|
setCustomVoices((prev) => [
|
||||||
</Icon>
|
...prev,
|
||||||
</button>
|
DEFAULT_GOOGLE_CUSTOM_VOICE,
|
||||||
</div>
|
]);
|
||||||
))}
|
}}
|
||||||
<ButtonGroup left>
|
>
|
||||||
<button
|
<Icon subStyle="teal">
|
||||||
className="btnty"
|
<Icons.Plus />
|
||||||
type="button"
|
</Icon>
|
||||||
title="Add Voice"
|
</button>
|
||||||
onClick={() => {
|
</ButtonGroup>
|
||||||
setCustomVoicesMessage("");
|
</fieldset>
|
||||||
setCustomVoices((prev) => [
|
)}
|
||||||
...prev,
|
</fieldset>
|
||||||
DEFAULT_GOOGLE_CUSTOM_VOICE,
|
</>
|
||||||
]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon subStyle="teal">
|
|
||||||
<Icons.Plus />
|
|
||||||
</Icon>
|
|
||||||
</button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</fieldset>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user