From ae8b4ae1246fd17bdf132b99815e99d8dfae8cfa Mon Sep 17 00:00:00 2001 From: Hoan Luu Huu <110280845+xquanluu@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:10:59 +0700 Subject: [PATCH] support add google voice cloning key (#461) * support add google voice cloning key * fix bugs on google voice cloning * wip * fixed google custom voice --- src/api/constants.ts | 8 + src/api/index.ts | 19 ++ src/api/types.ts | 5 +- .../internal/views/speech-services/form.tsx | 240 +++++++++++++----- src/styles/_forms.scss | 5 + 5 files changed, 208 insertions(+), 69 deletions(-) diff --git a/src/api/constants.ts b/src/api/constants.ts index 72ac853..806a25a 100644 --- a/src/api/constants.ts +++ b/src/api/constants.ts @@ -2,6 +2,7 @@ import { hasValue } from "src/utils"; import type { Currency, ElevenLabsOptions, + GoogleCustomVoice, LimitField, LimitUnitOption, PasswordSettings, @@ -247,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 = { optimize_streaming_latency: 3, diff --git a/src/api/index.ts b/src/api/index.ts index abf9d9b..509f35f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -225,6 +225,16 @@ export const getBlob = (url: string) => { }); }; +export const postBlobFetch = (url: string, formdata?: FormData) => { + return fetchTransport(url, { + method: "POST", + body: formdata, + headers: { + Authorization: `Bearer ${getToken()}`, + }, + }); +}; + /** Simple wrappers for fetchTransport calls to any API, :GET, :POST, :PUT, :DELETE */ export const getFetch = (url: string) => { @@ -492,6 +502,15 @@ export const postGoogleCustomVoice = (payload: Partial) => { payload, ); }; + +export const postGoogleVoiceCloningKey = (sid: string, file: File) => { + const formData = new FormData(); + formData.append("file", file); + return postBlobFetch( + `${API_GOOGLE_CUSTOM_VOICES}/${sid}/VoiceCloningKey`, + formData, + ); +}; /** Named wrappers for `putFetch` */ export const putUser = (sid: string, payload: Partial) => { diff --git a/src/api/types.ts b/src/api/types.ts index 6710c37..002e4aa 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -381,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 { diff --git a/src/containers/internal/views/speech-services/form.tsx b/src/containers/internal/views/speech-services/form.tsx index 2c3298e..95c873a 100644 --- a/src/containers/internal/views/speech-services/form.tsx +++ b/src/containers/internal/views/speech-services/form.tsx @@ -18,6 +18,7 @@ import { getGoogleCustomVoices, getSpeechSupportedLanguagesAndVoices, postGoogleCustomVoice, + postGoogleVoiceCloningKey, postSpeechService, putGoogleCustomVoice, putSpeechService, @@ -77,7 +78,7 @@ 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, @@ -243,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); }); } }), @@ -265,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) { @@ -438,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); @@ -978,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); }} @@ -1009,7 +1052,10 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
@@ -1029,49 +1075,112 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => { /> -
- { - updateCustomVoices( - i, - "reported_usage", - e.target.value, - ); - }} - /> -
+ {!v.use_voice_cloning_key && ( +
+ { + updateCustomVoices( + i, + "reported_usage", + e.target.value, + ); + }} + /> +
+ )} -
-
- -
-
+ -
-
- { - updateCustomVoices( - i, - "model", - e.target.value, - ); - }} - /> -
-
+ {!v.use_voice_cloning_key && ( + <> +
+
+ +
+
+ +
+
+ { + updateCustomVoices( + i, + "model", + e.target.value, + ); + }} + /> +
+
+ + )} + + {v.use_voice_cloning_key === 1 && ( + <> +
+
+ {hasValue(v.voice_cloning_key) && ( +
+                                      {v.voice_cloning_key}
+                                    
+ )} +
+
+ { + 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} + /> +
+
+ + )}