mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2026-10-04 02:04:17 +00:00
feat google custom voice (#338)
* feat google custom voice * google custom voice * wip * wip * wip
This commit is contained in:
@@ -205,6 +205,15 @@ export const ELEVENLABS_MODEL_OPTIONS = [
|
||||
{ name: "Multilingual v1", value: "eleven_multilingual_v1" },
|
||||
{ name: "English v1", value: "eleven_monolingual_v1" },
|
||||
];
|
||||
|
||||
// Google Custom Voice reported usage options
|
||||
|
||||
export const DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = "REALTIME";
|
||||
export const GOOGLE_CUSTOM_VOICES_REPORTED_USAGE = [
|
||||
{ name: "REPORTED_USAGE_UNSPECIFIED", value: "REPORTED_USAGE_UNSPECIFIED" },
|
||||
{ name: "REALTIME", value: "REALTIME" },
|
||||
{ name: "OFFLINE", value: "OFFLINE" },
|
||||
];
|
||||
/** Password Length options */
|
||||
|
||||
export const PASSWORD_MIN = 8;
|
||||
@@ -352,3 +361,4 @@ export const API_PRICE = `${API_BASE_URL}/Prices`;
|
||||
export const API_SUBSCRIPTIONS = `${API_BASE_URL}/Subscriptions`;
|
||||
export const API_CHANGE_PASSWORD = `${API_BASE_URL}/change-password`;
|
||||
export const API_SIGNIN = `${API_BASE_URL}/signin`;
|
||||
export const API_GOOGLE_CUSTOM_VOICES = `${API_BASE_URL}/GoogleCustomVoices`;
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
API_SUBSCRIPTIONS,
|
||||
API_CHANGE_PASSWORD,
|
||||
API_SIGNIN,
|
||||
API_GOOGLE_CUSTOM_VOICES,
|
||||
} from "./constants";
|
||||
import { ROUTE_LOGIN } from "src/router/routes";
|
||||
import {
|
||||
@@ -94,6 +95,8 @@ import type {
|
||||
LanguageOption,
|
||||
VoiceOption,
|
||||
GetLanguages,
|
||||
GoogleCustomVoice,
|
||||
GoogleCustomVoicesQuery,
|
||||
} from "./types";
|
||||
import { Availability, StatusCodes } from "./types";
|
||||
import { JaegerRoot } from "./jaeger-types";
|
||||
@@ -501,6 +504,13 @@ export const postChangepassword = (payload: Partial<ChangePassword>) => {
|
||||
export const postSignIn = (payload: Partial<SignIn>) => {
|
||||
return postFetch<SignIn, Partial<SignIn>>(API_SIGNIN, payload);
|
||||
};
|
||||
|
||||
export const postGoogleCustomVoice = (payload: Partial<GoogleCustomVoice>) => {
|
||||
return postFetch<SidResponse, Partial<GoogleCustomVoice>>(
|
||||
API_GOOGLE_CUSTOM_VOICES,
|
||||
payload
|
||||
);
|
||||
};
|
||||
/** Named wrappers for `putFetch` */
|
||||
|
||||
export const putUser = (sid: string, payload: Partial<UserUpdatePayload>) => {
|
||||
@@ -630,6 +640,17 @@ export const putActivationCode = (
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
export const putGoogleCustomVoice = (
|
||||
sid: string,
|
||||
payload: Partial<GoogleCustomVoice>
|
||||
) => {
|
||||
return putFetch<EmptyResponse, Partial<GoogleCustomVoice>>(
|
||||
`${API_GOOGLE_CUSTOM_VOICES}/${sid}`,
|
||||
payload
|
||||
);
|
||||
};
|
||||
|
||||
/** Named wrappers for `deleteFetch` */
|
||||
|
||||
export const deleteUser = (sid: string) => {
|
||||
@@ -719,6 +740,10 @@ export const deleteClient = (sid: string) => {
|
||||
export const deleteRecord = (url: string) => {
|
||||
return deleteFetch<EmptyResponse>(url);
|
||||
};
|
||||
|
||||
export const deleteGoogleCustomVoice = (sid: string) => {
|
||||
return deleteFetch<EmptyResponse>(`${API_GOOGLE_CUSTOM_VOICES}/${sid}`);
|
||||
};
|
||||
/** Named wrappers for `getFetch` */
|
||||
|
||||
export const getUser = (sid: string) => {
|
||||
@@ -771,6 +796,13 @@ export const getAvailability = (domain: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getGoogleCustomVoices = (
|
||||
query: Partial<GoogleCustomVoicesQuery>
|
||||
) => {
|
||||
const qryStr = getQuery<Partial<GoogleCustomVoicesQuery>>(query);
|
||||
return getFetch<GoogleCustomVoice[]>(`${API_GOOGLE_CUSTOM_VOICES}?${qryStr}`);
|
||||
};
|
||||
|
||||
/** Wrappers for APIs that can have a mock dev server response */
|
||||
|
||||
export const getMe = () => {
|
||||
|
||||
@@ -372,6 +372,14 @@ export interface RecentCall {
|
||||
recording_url?: string;
|
||||
}
|
||||
|
||||
export interface GoogleCustomVoice {
|
||||
google_custom_voice_sid?: string;
|
||||
speech_credential_sid?: string;
|
||||
name: string;
|
||||
reported_usage: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface SpeechCredential {
|
||||
speech_credential_sid: string;
|
||||
service_provider_sid: null | string;
|
||||
@@ -528,6 +536,13 @@ export interface CallQuery extends PageQuery {
|
||||
answered?: string;
|
||||
}
|
||||
|
||||
export interface GoogleCustomVoicesQuery {
|
||||
speech_credential_sid?: string;
|
||||
label?: string;
|
||||
account_sid?: string;
|
||||
service_provider_sid: string;
|
||||
}
|
||||
|
||||
export interface PagedResponse<Type> {
|
||||
page_size: number;
|
||||
total: number;
|
||||
|
||||
@@ -63,6 +63,7 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
const navigate = useNavigate();
|
||||
const { synthesis, recognizers } = useSpeechVendors();
|
||||
const user = useSelectState("user");
|
||||
const currentServiceProvider = useSelectState("currentServiceProvider");
|
||||
const [accounts] = useServiceProviderData<Account[]>("Accounts");
|
||||
const [applications] = useApiData<Application[]>("Applications");
|
||||
const [applicationName, setApplicationName] = useState("");
|
||||
@@ -674,6 +675,10 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
);
|
||||
})}
|
||||
<SpeechProviderSelection
|
||||
serviceProviderSid={
|
||||
currentServiceProvider?.service_provider_sid || ""
|
||||
}
|
||||
accountSid={accountSid}
|
||||
credentials={credentials}
|
||||
synthesis={synthesis}
|
||||
ttsVendor={[synthVendor, setSynthVendor]}
|
||||
@@ -701,6 +706,10 @@ export const ApplicationForm = ({ application }: ApplicationFormProps) => {
|
||||
}}
|
||||
>
|
||||
<SpeechProviderSelection
|
||||
serviceProviderSid={
|
||||
currentServiceProvider?.service_provider_sid || ""
|
||||
}
|
||||
accountSid={accountSid}
|
||||
credentials={credentials}
|
||||
synthesis={synthesis}
|
||||
ttsVendor={[
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { postSpeechServiceLanguages, postSpeechServiceVoices } from "src/api";
|
||||
import {
|
||||
getGoogleCustomVoices,
|
||||
postSpeechServiceLanguages,
|
||||
postSpeechServiceVoices,
|
||||
} from "src/api";
|
||||
import { SpeechCredential } from "src/api/types";
|
||||
import { Selector } from "src/components/forms";
|
||||
import { SelectorOption } from "src/components/forms/selector";
|
||||
@@ -30,6 +34,8 @@ import {
|
||||
VoiceLanguage,
|
||||
} from "src/vendor/types";
|
||||
type SpeechProviderSelectionProbs = {
|
||||
accountSid: string;
|
||||
serviceProviderSid: string;
|
||||
credentials: SpeechCredential[] | undefined;
|
||||
synthesis: SynthesisVendors | undefined;
|
||||
ttsVendor: [
|
||||
@@ -53,6 +59,8 @@ type SpeechProviderSelectionProbs = {
|
||||
};
|
||||
|
||||
export const SpeechProviderSelection = ({
|
||||
accountSid,
|
||||
serviceProviderSid,
|
||||
credentials,
|
||||
synthesis,
|
||||
ttsVendor: [synthVendor, setSynthVendor],
|
||||
@@ -136,8 +144,33 @@ export const SpeechProviderSelection = ({
|
||||
setSynthesisLanguageOptions(json);
|
||||
}
|
||||
});
|
||||
} else if (synthVendor === VENDOR_GOOGLE) {
|
||||
getGoogleCustomVoices({
|
||||
...(synthLabel && { label: synthLabel }),
|
||||
account_sid: accountSid,
|
||||
service_provider_sid: serviceProviderSid,
|
||||
}).then(({ json }) => {
|
||||
const customVOices = json.map((v) => ({
|
||||
name: `${v.name} (Custom)`,
|
||||
value: `custom_${v.google_custom_voice_sid}`,
|
||||
}));
|
||||
options = synthesis[synthVendor as keyof SynthesisVendors]
|
||||
.filter((lang: VoiceLanguage) => {
|
||||
return lang.code === synthLang;
|
||||
})
|
||||
.flatMap((lang: VoiceLanguage) =>
|
||||
lang.voices.map((voice: Voice) => ({
|
||||
name: voice.name,
|
||||
value: voice.value,
|
||||
}))
|
||||
) as Voice[];
|
||||
setSynthesisVoiceOptions([...customVOices, ...options]);
|
||||
if (customVOices.length > 0) {
|
||||
setSynthVoice(customVOices[0].value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [synthVendor, synthesis, synthLabel]);
|
||||
}, [synthVendor, synthesis, synthLabel, accountSid, serviceProviderSid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (credentials) {
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Button, ButtonGroup, MS } from "@jambonz/ui-kit";
|
||||
import { Button, ButtonGroup, Icon, MS, MXS } from "@jambonz/ui-kit";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
import { ROUTE_INTERNAL_SPEECH } from "src/router/routes";
|
||||
import { Section, Tooltip } from "src/components";
|
||||
import { Icons, Section, Tooltip } from "src/components";
|
||||
import {
|
||||
FileUpload,
|
||||
Selector,
|
||||
Passwd,
|
||||
AccountSelect,
|
||||
Checkzone,
|
||||
Message,
|
||||
} from "src/components/forms";
|
||||
import { toastError, toastSuccess, useSelectState } from "src/store";
|
||||
import {
|
||||
deleteGoogleCustomVoice,
|
||||
getGoogleCustomVoices,
|
||||
postGoogleCustomVoice,
|
||||
postSpeechService,
|
||||
putGoogleCustomVoice,
|
||||
putSpeechService,
|
||||
useServiceProviderData,
|
||||
} from "src/api";
|
||||
@@ -39,17 +44,25 @@ import {
|
||||
getObscuredSecret,
|
||||
isUserAccountScope,
|
||||
isNotBlank,
|
||||
hasLength,
|
||||
} from "src/utils";
|
||||
import { getObscuredGoogleServiceKey } from "./utils";
|
||||
import { CredentialStatus } from "./status";
|
||||
|
||||
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
|
||||
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
|
||||
import type {
|
||||
Account,
|
||||
GoogleCustomVoice,
|
||||
SpeechCredential,
|
||||
UseApiDataMap,
|
||||
} from "src/api/types";
|
||||
import { setAccountFilter, setLocation } from "src/store/localStore";
|
||||
import {
|
||||
DEFAULT_ELEVENLABS_MODEL,
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
DISABLE_CUSTOM_SPEECH,
|
||||
ELEVENLABS_MODEL_OPTIONS,
|
||||
GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
} from "src/api/constants";
|
||||
|
||||
type SpeechServiceFormProps = {
|
||||
@@ -117,6 +130,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
const [onPremNuanceSttUrl, setOnPremNuanceSttUrl] = useState("");
|
||||
const [cobaltServerUri, setCobaltServerUri] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
const [useCustomVoicesCheck, setUseCustomVoicesCheck] = useState(false);
|
||||
const [customVoices, setCustomVoices] = useState<GoogleCustomVoice[]>([]);
|
||||
const [customVoicesMessage, setCustomVoicesMessage] = useState("");
|
||||
|
||||
const handleFile = (file: File) => {
|
||||
const handleError = () => {
|
||||
@@ -144,6 +160,62 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handlePutGoogleCustomVoices = () => {
|
||||
if (!credential || !credential.data) {
|
||||
return;
|
||||
}
|
||||
if (useCustomVoicesCheck) {
|
||||
Promise.all(
|
||||
customVoices.map((v) => {
|
||||
if (v.google_custom_voice_sid) {
|
||||
const sid = v.google_custom_voice_sid;
|
||||
delete v.google_custom_voice_sid;
|
||||
return putGoogleCustomVoice(sid, v);
|
||||
} else {
|
||||
return postGoogleCustomVoice({
|
||||
...v,
|
||||
speech_credential_sid: credential.data?.speech_credential_sid,
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toastSuccess("Speech credential updated successfully");
|
||||
credential.refetch();
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_SPEECH}/${credential?.data?.speech_credential_sid}/edit`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else if (useCustomVoicesCheck && customVoices.length > 0) {
|
||||
Promise.all(
|
||||
customVoices.map((v) => {
|
||||
if (v.google_custom_voice_sid) {
|
||||
return deleteGoogleCustomVoice(v.google_custom_voice_sid);
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
toastSuccess("Speech credential updated successfully");
|
||||
credential.refetch();
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_SPEECH}/${credential?.data?.speech_credential_sid}/edit`
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
});
|
||||
} else {
|
||||
toastSuccess("Speech credential updated successfully");
|
||||
credential.refetch();
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -162,9 +234,6 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
use_for_tts: ttsCheck ? 1 : 0,
|
||||
use_for_stt: sttCheck ? 1 : 0,
|
||||
label: label || null,
|
||||
...(vendor === VENDOR_AWS && {
|
||||
aws_region: region || null,
|
||||
}),
|
||||
...(vendor === VENDOR_MICROSOFT && {
|
||||
region: region || null,
|
||||
use_custom_tts:
|
||||
@@ -218,11 +287,15 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
)
|
||||
.then(() => {
|
||||
if (credential && credential.data) {
|
||||
toastSuccess("Speech credential updated successfully");
|
||||
credential.refetch();
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
|
||||
);
|
||||
if (credential.data.vendor === VENDOR_GOOGLE) {
|
||||
handlePutGoogleCustomVoices();
|
||||
} else {
|
||||
toastSuccess("Speech credential updated successfully");
|
||||
credential.refetch();
|
||||
navigate(
|
||||
`${ROUTE_INTERNAL_SPEECH}/${credential.data.speech_credential_sid}/edit`
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -247,10 +320,25 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
}),
|
||||
riva_server_uri: vendor == VENDOR_NVIDIA ? rivaServerUri : null,
|
||||
})
|
||||
.then(() => {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(ROUTE_INTERNAL_SPEECH);
|
||||
setAccountFilter(accountSid);
|
||||
.then(({ json }) => {
|
||||
if (vendor === VENDOR_GOOGLE && useCustomVoicesCheck) {
|
||||
Promise.all(
|
||||
customVoices.map((v) =>
|
||||
postGoogleCustomVoice({
|
||||
...v,
|
||||
speech_credential_sid: json.sid,
|
||||
})
|
||||
)
|
||||
).then(() => {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(ROUTE_INTERNAL_SPEECH);
|
||||
setAccountFilter(accountSid);
|
||||
});
|
||||
} else {
|
||||
toastSuccess("Speech credential created successfully");
|
||||
navigate(ROUTE_INTERNAL_SPEECH);
|
||||
setAccountFilter(accountSid);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toastError(error.msg);
|
||||
@@ -402,8 +490,34 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
setTtsModelId(credential.data.model_id);
|
||||
}
|
||||
}
|
||||
if (credential?.data?.vendor === VENDOR_GOOGLE) {
|
||||
// let try to check if there is custom voices
|
||||
getGoogleCustomVoices({
|
||||
speech_credential_sid: credential.data.speech_credential_sid,
|
||||
}).then(({ json }) => {
|
||||
setCustomVoices(json);
|
||||
setUseCustomVoicesCheck(json.length > 0);
|
||||
});
|
||||
}
|
||||
}, [credential]);
|
||||
|
||||
const updateCustomVoices = (
|
||||
index: number,
|
||||
key: string,
|
||||
value: typeof customVoices[number][keyof GoogleCustomVoice]
|
||||
) => {
|
||||
setCustomVoices((prev) =>
|
||||
prev.map((g, i) =>
|
||||
i === index
|
||||
? {
|
||||
...g,
|
||||
[key]: value,
|
||||
}
|
||||
: g
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section slim>
|
||||
<form className="form form--internal" onSubmit={handleSubmit}>
|
||||
@@ -647,6 +761,166 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
|
||||
</pre>
|
||||
</fieldset>
|
||||
)}
|
||||
{ttsCheck && vendor === VENDOR_GOOGLE && (
|
||||
<fieldset>
|
||||
<label htmlFor="use_custom_voice" className="chk">
|
||||
<input
|
||||
id="use_custom_voice"
|
||||
name="use_custom_voice"
|
||||
type="checkbox"
|
||||
onChange={(e) => {
|
||||
if (customVoices.length === 0) {
|
||||
setCustomVoices([
|
||||
{
|
||||
name: "",
|
||||
reported_usage:
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
model: "",
|
||||
},
|
||||
]);
|
||||
}
|
||||
setUseCustomVoicesCheck(e.target.checked);
|
||||
}}
|
||||
checked={useCustomVoicesCheck}
|
||||
/>
|
||||
<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 / 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Icon>
|
||||
<Icons.Trash2 />
|
||||
</Icon>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<ButtonGroup left>
|
||||
<button
|
||||
className="btnty"
|
||||
type="button"
|
||||
title="Add Voice"
|
||||
onClick={() => {
|
||||
setCustomVoicesMessage("");
|
||||
setCustomVoices((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: "",
|
||||
reported_usage:
|
||||
DEFAULT_GOOGLE_CUSTOM_VOICES_REPORTED_USAGE,
|
||||
model: "",
|
||||
},
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<Icon subStyle="teal">
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
</button>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{vendor === VENDOR_NUANCE && (
|
||||
|
||||
@@ -294,3 +294,53 @@ fieldset {
|
||||
.bucket_tag {
|
||||
@extend .lcr;
|
||||
}
|
||||
|
||||
.customVoice {
|
||||
padding: ui-vars.$px02;
|
||||
border-radius: ui-vars.$px01;
|
||||
border: 2px solid ui-vars.$grey;
|
||||
max-width: ui-vars.$width-mobile;
|
||||
position: relative;
|
||||
|
||||
> div {
|
||||
display: grid;
|
||||
grid-gap: ui-vars.$px02;
|
||||
align-items: center;
|
||||
|
||||
&:nth-child(1) {
|
||||
grid-template-columns: [col] 100%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
grid-template-columns: [col] calc(40% - #{ui-vars.$px02 * 2}) [col] 60%;
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
grid-template-columns: [col] 100%;
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
grid-template-columns: [col] 100%;
|
||||
margin-top: ui-vars.$px02;
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 50%;
|
||||
transform: translate3d(50%, 50%, 0);
|
||||
|
||||
@include mixins.small() {
|
||||
top: auto;
|
||||
bottom: auto;
|
||||
transform: none;
|
||||
position: relative;
|
||||
margin-top: ui-vars.$px02;
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user