Compare commits

...

1 Commits

Author SHA1 Message Date
Hoan Luu Huu
4fc6b1ae40 support google gemini tts (#590)
* support google gemini tts

* wip
2026-01-23 10:07:08 -05:00
2 changed files with 222 additions and 187 deletions

View File

@@ -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";

View File

@@ -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>
)} )}
</> </>
)} )}