Compare commits

..

5 Commits

Author SHA1 Message Date
Quan HL
d8adb8dbe0 fix wrong css for filter on each component 2023-10-20 12:28:39 +07:00
Hoan Luu Huu
3a19ff6840 upgrade wavesuffer to 7.3.4 (#329)
* upgrade wavesuffer to 7.3.4

* fix typo issue for wavesurfer

* fix

* fix
2023-10-18 19:54:59 +02:00
Hoan Luu Huu
729cefb06c fix css for recent calls in small screen (#331) 2023-10-16 12:51:49 +02:00
Hoan Luu Huu
26e3856603 add elevenlabs (#330)
* add elevenlabs

* wip

* wip

* fix review comments

* fetch voice and language for eleven labs

* fix revciew comment

* fix revciew comment

* fix revciew comment

* fixed review comment
2023-10-16 10:21:19 +02:00
Hoan Luu Huu
f5302583b5 fix cobalt default language (#326) 2023-09-27 07:50:11 -04:00
23 changed files with 524 additions and 155 deletions

45
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"react-dom": "^18.0.0",
"react-feather": "^2.0.10",
"react-router-dom": "^6.3.0",
"wavesurfer.js": "^6.6.3"
"wavesurfer.js": "^7.3.4"
},
"devDependencies": {
"@types/cors": "^2.8.12",
@@ -30,7 +30,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/uuid": "^9.0.1",
"@types/wavesurfer.js": "^6.0.6",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-react": "^1.3.0",
@@ -900,12 +899,6 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"node_modules/@types/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==",
"dev": true
},
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@@ -1014,15 +1007,6 @@
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true
},
"node_modules/@types/wavesurfer.js": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-6.0.6.tgz",
"integrity": "sha512-fD54o0RXZXxkOb+69Rt6rGViaHpIc1Mmde2aOX9qPhlQhrCPepybGnsekiG407+7scPlaK+hmuPez5AnnmlzGg==",
"dev": true,
"dependencies": {
"@types/debounce": "*"
}
},
"node_modules/@types/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
@@ -6867,9 +6851,9 @@
}
},
"node_modules/wavesurfer.js": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-6.6.3.tgz",
"integrity": "sha512-XqUOXe8+SOTe8uKCHaqW0vJ5etCCQvq/NgaPycn9HAX/nUi+2zoWD+w9i7H5vBT9UCDNawOia+vS5Ct3kZGQzA=="
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.3.4.tgz",
"integrity": "sha512-x2Ue4Dh+4RoaWay3LOLHhXgkdxPAIoC/BcbXh0yk8WNhQH2NboPoa52XXoCo4jEfjSe1bc7nxuM5vBIxUMZyBA=="
},
"node_modules/which": {
"version": "2.0.2",
@@ -7678,12 +7662,6 @@
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"@types/debounce": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==",
"dev": true
},
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@@ -7787,15 +7765,6 @@
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true
},
"@types/wavesurfer.js": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-6.0.6.tgz",
"integrity": "sha512-fD54o0RXZXxkOb+69Rt6rGViaHpIc1Mmde2aOX9qPhlQhrCPepybGnsekiG407+7scPlaK+hmuPez5AnnmlzGg==",
"dev": true,
"requires": {
"@types/debounce": "*"
}
},
"@types/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz",
@@ -11657,9 +11626,9 @@
}
},
"wavesurfer.js": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-6.6.3.tgz",
"integrity": "sha512-XqUOXe8+SOTe8uKCHaqW0vJ5etCCQvq/NgaPycn9HAX/nUi+2zoWD+w9i7H5vBT9UCDNawOia+vS5Ct3kZGQzA=="
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.3.4.tgz",
"integrity": "sha512-x2Ue4Dh+4RoaWay3LOLHhXgkdxPAIoC/BcbXh0yk8WNhQH2NboPoa52XXoCo4jEfjSe1bc7nxuM5vBIxUMZyBA=="
},
"which": {
"version": "2.0.2",

View File

@@ -42,6 +42,8 @@
},
"dependencies": {
"@jambonz/ui-kit": "^0.0.21",
"@stripe/react-stripe-js": "^2.1.1",
"@stripe/stripe-js": "^1.54.1",
"dayjs": "^1.11.5",
"immutability-helper": "^3.1.1",
"react": "^18.0.0",
@@ -50,9 +52,7 @@
"react-dom": "^18.0.0",
"react-feather": "^2.0.10",
"react-router-dom": "^6.3.0",
"wavesurfer.js": "^6.6.3",
"@stripe/react-stripe-js": "^2.1.1",
"@stripe/stripe-js": "^1.54.1"
"wavesurfer.js": "^7.3.4"
},
"devDependencies": {
"@types/cors": "^2.8.12",
@@ -61,7 +61,6 @@
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/uuid": "^9.0.1",
"@types/wavesurfer.js": "^6.0.6",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-react": "^1.3.0",

View File

@@ -197,6 +197,14 @@ export const AUDIO_FORMAT_OPTIONS = [
value: "wav",
},
];
export const DEFAULT_ELEVENLABS_MODEL = "eleven_multilingual_v2";
export const ELEVENLABS_MODEL_OPTIONS = [
{ name: "Multilingual v2", value: "eleven_multilingual_v2" },
{ name: "Multilingual v1", value: "eleven_multilingual_v1" },
{ name: "English v1", value: "eleven_monolingual_v1" },
];
/** Password Length options */
export const PASSWORD_MIN = 8;

View File

@@ -90,6 +90,10 @@ import type {
DeleteAccount,
ChangePassword,
SignIn,
GetVoices,
LanguageOption,
VoiceOption,
GetLanguages,
} from "./types";
import { Availability, StatusCodes } from "./types";
import { JaegerRoot } from "./jaeger-types";
@@ -329,6 +333,32 @@ export const postSpeechService = (
return postFetch<SidResponse, Partial<SpeechCredential>>(apiUrl, payload);
};
export const postSpeechServiceVoices = (
sid: string,
payload: Partial<GetVoices>
) => {
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials/voices`
: `${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials/voices`;
return postFetch<VoiceOption[], Partial<GetVoices>>(apiUrl, payload);
};
export const postSpeechServiceLanguages = (
sid: string,
payload: Partial<GetLanguages>
) => {
const userData = parseJwt(getToken());
const apiUrl =
userData.scope === USER_ACCOUNT
? `${API_ACCOUNTS}/${userData.account_sid}/SpeechCredentials/languages`
: `${API_SERVICE_PROVIDERS}/${sid}/SpeechCredentials/languages`;
return postFetch<LanguageOption[], Partial<GetLanguages>>(apiUrl, payload);
};
export const postMsTeamsTentant = (payload: Partial<MSTeamsTenant>) => {
return postFetch<SidResponse, Partial<MSTeamsTenant>>(
API_MS_TEAMS_TENANTS,

View File

@@ -37,14 +37,14 @@ export interface JaegerAttribute {
value: JaegerValue;
}
export interface WaveSufferSttResult {
export interface WaveSurferSttResult {
vendor: string;
transcript: string;
confidence: number;
language_code: string;
}
export interface WaveSufferDtmfResult {
export interface WaveSurferDtmfResult {
dtmf: string;
duration: string;
}

View File

@@ -407,6 +407,7 @@ export interface SpeechCredential {
custom_tts_url: null | string;
label: null | string;
cobalt_server_uri: null | string;
model_id: null | string;
}
export interface Alert {
@@ -675,3 +676,20 @@ export interface SignIn {
jwt?: null | string;
account_sid?: null | string;
}
export interface GetVoices {
vendor: string;
label: string;
}
export interface VoiceOption extends SelectorOptions {
[key: string]: unknown;
}
export interface GetLanguages extends GetVoices {
[key: string]: unknown;
}
export interface LanguageOption extends SelectorOptions {
[key: string]: unknown;
}

View File

@@ -71,7 +71,7 @@ export const Accounts = () => {
</Icon>
</Link>
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter
placeholder="Filter accounts"
filter={[filter, setFilter]}

View File

@@ -96,7 +96,7 @@ export const Applications = () => {
</Link>
)}
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter
placeholder="Filter applications"
filter={[filter, setFilter]}

View File

@@ -1,8 +1,12 @@
import React, { useEffect, useState } from "react";
import { postSpeechServiceLanguages, postSpeechServiceVoices } from "src/api";
import { SpeechCredential } from "src/api/types";
import { Selector } from "src/components/forms";
import { SelectorOption } from "src/components/forms/selector";
import { useSelectState } from "src/store";
import { hasLength } from "src/utils";
import {
ELEVENLABS_LANG_EN,
LANG_COBALT_EN_US,
LANG_EN_US,
LANG_EN_US_STANDARD_C,
@@ -10,6 +14,7 @@ import {
VENDOR_COBALT,
VENDOR_CUSTOM,
VENDOR_DEEPGRAM,
VENDOR_ELEVENLABS,
VENDOR_GOOGLE,
VENDOR_MICROSOFT,
VENDOR_SONIOX,
@@ -66,6 +71,73 @@ export const SpeechProviderSelection = ({
const [selectedCredential, setSelectedCredential] = useState<
SpeechCredential | undefined
>();
const [synthesisVoiceOptions, setSynthesisVoiceOptions] = useState<
SelectorOption[]
>([]);
const [synthesisLanguageOptions, setSynthesisLanguageOptions] = useState<
SelectorOption[]
>([]);
const currentServiceProvider = useSelectState("currentServiceProvider");
useEffect(() => {
if (!synthesis) {
return;
}
let options = synthesis[synthVendor as keyof SynthesisVendors]
.filter((lang: VoiceLanguage) => {
// ELEVENLABS has same voice for all lange, take voices from the 1st language
if (synthVendor === VENDOR_ELEVENLABS) {
return true;
}
return lang.code === synthLang;
})
.flatMap((lang: VoiceLanguage) =>
lang.voices.map((voice: Voice) => ({
name: voice.name,
value: voice.value,
}))
) as Voice[];
setSynthesisVoiceOptions(options);
options = synthesis[synthVendor as keyof SynthesisVendors].map(
(lang: VoiceLanguage) => ({
name: lang.name,
value: lang.code,
})
);
setSynthesisLanguageOptions(options);
if (synthVendor === VENDOR_ELEVENLABS) {
postSpeechServiceVoices(
currentServiceProvider
? currentServiceProvider.service_provider_sid
: "",
{
vendor: synthVendor,
label: synthLabel,
}
).then(({ json }) => {
if (json.length > 0) {
setSynthesisVoiceOptions(json);
}
});
postSpeechServiceLanguages(
currentServiceProvider
? currentServiceProvider.service_provider_sid
: "",
{
vendor: synthVendor,
label: synthLabel,
}
).then(({ json }) => {
if (json.length > 0) {
setSynthesisLanguageOptions(json);
}
});
}
}, [synthVendor, synthesis, synthLabel]);
useEffect(() => {
if (credentials) {
@@ -112,6 +184,15 @@ export const SpeechProviderSelection = ({
return;
}
if (vendor === VENDOR_ELEVENLABS) {
const newLang = synthesis[vendor].find(
(lang) => lang.code === ELEVENLABS_LANG_EN
);
setSynthLang(ELEVENLABS_LANG_EN);
setSynthVoice(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 = synthesis[vendor].find(
@@ -154,12 +235,7 @@ export const SpeechProviderSelection = ({
id="synthesis_lang"
name="synthesis_lang"
value={synthLang}
options={synthesis[synthVendor as keyof SynthesisVendors].map(
(lang: VoiceLanguage) => ({
name: lang.name,
value: lang.code,
})
)}
options={synthesisLanguageOptions}
onChange={(e) => {
const language = e.target.value;
setSynthLang(language);
@@ -200,18 +276,7 @@ export const SpeechProviderSelection = ({
id="synthesis_voice"
name="synthesis_voice"
value={synthVoice}
options={
synthesis[synthVendor as keyof SynthesisVendors]
.filter(
(lang: VoiceLanguage) => lang.code === synthLang
)
.flatMap((lang: VoiceLanguage) =>
lang.voices.map((voice: Voice) => ({
name: voice.name,
value: voice.value,
}))
) as Voice[]
}
options={synthesisVoiceOptions}
onChange={(e) => setSynthVoice(e.target.value)}
/>
)}

View File

@@ -155,7 +155,7 @@ export const Carriers = () => {
</Icon>
</Link>
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter
placeholder="Filter carriers"
filter={[filter, setFilter]}

View File

@@ -126,7 +126,7 @@ export const Clients = () => {
</Link>
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter
placeholder="Filter clients"
filter={[filter, setFilter]}

View File

@@ -99,7 +99,7 @@ export const Lcrs = () => {
multiple carriers available.
</M>
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter placeholder="Filter lcrs" filter={[filter, setFilter]} />
<ScopedAccess user={user} scope={Scope.admin}>
<AccountFilter

View File

@@ -89,7 +89,7 @@ export const MSTeamsTenants = () => {
</Link>
)}
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter
placeholder="Filter ms teams tenants"
filter={[filter, setFilter]}

View File

@@ -129,7 +129,7 @@ export const PhoneNumbers = () => {
</Link>
)}
</section>
<section className="filters filters--spaced">
<section className="filters filters--multi">
<SearchFilter
placeholder="Filter phone numbers"
filter={[filter, setFilter]}

View File

@@ -6,14 +6,14 @@ import { Icon, P } from "@jambonz/ui-kit";
import { Icons, Modal, ModalClose } from "src/components";
import { deleteRecord, getBlob, getJaegerTrace } from "src/api";
import { DownloadedBlob, RecentCall } from "src/api/types";
import RegionsPlugin, { Region } from "wavesurfer.js/src/plugin/regions";
import TimelinePlugin from "wavesurfer.js/src/plugin/timeline";
import RegionsPlugin, { Region } from "wavesurfer.js/dist/plugins/regions";
import TimelinePlugin from "wavesurfer.js/dist/plugins/timeline";
import { API_BASE_URL } from "src/api/constants";
import {
JaegerRoot,
JaegerSpan,
WaveSufferDtmfResult,
WaveSufferSttResult,
WaveSurferDtmfResult,
WaveSurferSttResult,
} from "src/api/jaeger-types";
import {
getSpanAttributeByName,
@@ -38,23 +38,26 @@ export const Player = ({ call }: PlayerProps) => {
const [isReady, setIsReady] = useState(false);
const [playBackTime, setPlayBackTime] = useState("");
const [jaegerRoot, setJeagerRoot] = useState<JaegerRoot>();
const [waveSufferRegionData, setWaveSufferRegionData] =
useState<WaveSufferSttResult | null>();
const [waveSufferDtmfData, setWaveSufferDtmfData] =
useState<WaveSufferDtmfResult | null>();
const [waveSurferRegionData, setWaveSurferRegionData] =
useState<WaveSurferSttResult | null>();
const [waveSurferDtmfData, setWaveSurferDtmfData] =
useState<WaveSurferDtmfResult | null>();
const [regionChecked, setRegionChecked] = useState(false);
const wavesurferId = `wavesurfer--${call_sid}`;
const wavesurferTimelineId = `timeline-${wavesurferId}`;
const waveSufferRef = useRef<WaveSurfer | null>(null);
const waveSurferRef = useRef<WaveSurfer | null>(null);
const waveSurferRegionsPluginRef = useRef<RegionsPlugin | null>();
const [record, setRecord] = useState<DownloadedBlob | null>(null);
const [deleteRecordUrl, setDeleteRecordUrl] = useState("");
const drawDtmfRegionForSpan = (s: JaegerSpan, startPoint: JaegerSpan) => {
if (waveSufferRef.current) {
const r = waveSufferRef.current.regions.list[s.spanId];
if (waveSurferRegionsPluginRef.current) {
waveSurferRef.current;
const r = waveSurferRegionsPluginRef.current
.getRegions()
.find((r) => r.id === s.spanId);
if (!r) {
const [dtmfValue] = getSpanAttributeByName(s.attributes, "dtmf");
const [durationValue] = getSpanAttributeByName(
@@ -67,29 +70,28 @@ export const Player = ({ call }: PlayerProps) => {
1_000_000_000;
const duration =
Number(durationValue.value.stringValue.replace("ms", "")) / 1_000;
// as duration of DTMF is short, cannot be shown in wavesuffer,
// as duration of DTMF is short, cannot be shown in wavesurfer,
// adjust region width here.
const delta = duration <= 0.1 ? 0.1 : duration;
const end = start + delta;
const region = waveSufferRef.current.addRegion({
const region = waveSurferRegionsPluginRef.current.addRegion({
id: s.spanId,
start,
end,
color: "rgba(138, 43, 226, 0.15)",
drag: false,
loop: false,
resize: false,
});
changeRegionMouseStyle(region);
const att: WaveSufferDtmfResult = {
const att: WaveSurferDtmfResult = {
dtmf: dtmfValue.value.stringValue,
duration: durationValue.value.stringValue,
};
region.on("click", () => {
setWaveSufferDtmfData(att);
setWaveSurferDtmfData(att);
});
}
}
@@ -115,8 +117,10 @@ export const Player = ({ call }: PlayerProps) => {
startPoint: JaegerSpan,
channel = 0
) => {
if (waveSufferRef.current) {
const r = waveSufferRef.current.regions.list[s.spanId];
if (waveSurferRegionsPluginRef.current) {
const r = waveSurferRegionsPluginRef.current
.getRegions()
.find((r) => r.id === s.spanId);
if (!r) {
const start =
(s.startTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000 +
@@ -124,18 +128,18 @@ export const Player = ({ call }: PlayerProps) => {
const end =
(s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000;
const region = waveSufferRef.current.addRegion({
const region = waveSurferRegionsPluginRef.current.addRegion({
id: s.spanId,
start,
end,
color: "rgba(255, 0, 0, 0.15)",
drag: false,
loop: false,
resize: false,
});
changeRegionMouseStyle(region, channel);
const [sttResult] = getSpanAttributeByName(s.attributes, "stt.result");
let att: WaveSufferSttResult;
let att: WaveSurferSttResult;
if (sttResult) {
const data = JSON.parse(sttResult.value.stringValue);
att = {
@@ -168,13 +172,13 @@ export const Player = ({ call }: PlayerProps) => {
}
region.on("click", () => {
setWaveSufferRegionData(att);
setWaveSurferRegionData(att);
});
}
}
};
const buildWavesufferRegion = () => {
const buildWavesurferRegion = () => {
if (jaegerRoot) {
const spans = getSpansFromJaegerRoot(jaegerRoot);
const [startPoint] = getSpansByName(spans, "background-listen:listen");
@@ -218,7 +222,7 @@ export const Player = ({ call }: PlayerProps) => {
};
useEffect(() => {
buildWavesufferRegion();
buildWavesurferRegion();
}, [jaegerRoot, isReady]);
useEffect(() => {
@@ -251,8 +255,9 @@ export const Player = ({ call }: PlayerProps) => {
}
useEffect(() => {
if (waveSufferRef.current !== null || !record) return;
waveSufferRef.current = WaveSurfer.create({
if (waveSurferRef.current !== null || !record) return;
waveSurferRegionsPluginRef.current = RegionsPlugin.create();
waveSurferRef.current = WaveSurfer.create({
container: `#${wavesurferId}`,
waveColor: "#da1c5c",
progressColor: "grey",
@@ -260,62 +265,68 @@ export const Player = ({ call }: PlayerProps) => {
cursorWidth: 1,
cursorColor: "lightgray",
normalize: true,
responsive: true,
fillParent: true,
splitChannels: true,
scrollParent: true,
autoScroll: true,
splitChannels: [],
minPxPerSec: 100,
plugins: [
RegionsPlugin.create({}),
waveSurferRegionsPluginRef.current,
TimelinePlugin.create({
container: `#${wavesurferTimelineId}`,
timeInterval: 0.2,
primaryLabelInterval: 5,
secondaryLabelInterval: 1,
style: {
fontSize: "15px",
color: "#000000",
fontWeight: "bold",
},
}),
],
});
waveSufferRef.current.load(record?.data_url);
waveSurferRef.current.load(record?.data_url);
// All event should be after load
waveSufferRef.current.on("finish", () => {
waveSurferRef.current.on("finish", () => {
setIsPlaying(false);
});
waveSufferRef.current.on("play", () => {
waveSurferRef.current.on("play", () => {
setIsPlaying(true);
});
waveSufferRef.current.on("pause", () => {
waveSurferRef.current.on("pause", () => {
setIsPlaying(false);
});
waveSufferRef.current.on("ready", () => {
waveSurferRef.current.on("ready", () => {
setIsReady(true);
setPlayBackTime(formatTime(waveSufferRef.current?.getDuration() || 0));
setPlayBackTime(formatTime(waveSurferRef.current?.getDuration() || 0));
});
waveSufferRef.current.on("audioprocess", () => {
setPlayBackTime(formatTime(waveSufferRef.current?.getCurrentTime() || 0));
waveSurferRef.current.on("audioprocess", () => {
setPlayBackTime(formatTime(waveSurferRef.current?.getCurrentTime() || 0));
});
}, [record]);
const togglePlayback = () => {
if (waveSufferRef.current) {
if (waveSurferRef.current) {
if (!isPlaying) {
waveSufferRef.current.play();
waveSurferRef.current.play();
} else {
waveSufferRef.current.pause();
waveSurferRef.current.pause();
}
}
};
const setPlaybackJump = (delta: number) => {
if (waveSufferRef.current) {
const idx = waveSufferRef.current.getCurrentTime() + delta;
if (waveSurferRef.current) {
const idx = waveSurferRef.current.getCurrentTime() + delta;
const value =
idx <= 0
? 0
: idx >= waveSufferRef.current.getDuration()
? waveSufferRef.current.getDuration() - 1
: idx >= waveSurferRef.current.getDuration()
? waveSurferRef.current.getDuration() - 1
: idx;
waveSufferRef.current.setCurrentTime(value);
waveSurferRef.current.setTime(value);
setPlayBackTime(formatTime(value));
}
};
@@ -326,7 +337,6 @@ export const Player = ({ call }: PlayerProps) => {
<>
<div className="media-container">
<div id={wavesurferId} />
<div id={wavesurferTimelineId} />
<div className="media-container__center">
<strong>{playBackTime}</strong>
</div>
@@ -404,8 +414,9 @@ export const Player = ({ call }: PlayerProps) => {
checked={regionChecked}
onChange={(e) => {
setRegionChecked(e.target.checked);
if (waveSufferRef.current) {
const regionsList = waveSufferRef.current.regions.list;
if (waveSurferRegionsPluginRef.current) {
const regionsList =
waveSurferRegionsPluginRef.current.getRegions();
for (const [, region] of Object.entries(regionsList)) {
region.element.style.display = e.target.checked ? "" : "none";
}
@@ -415,8 +426,8 @@ export const Player = ({ call }: PlayerProps) => {
<div>Overlay STT and DTMF events</div>
</label>
</div>
{waveSufferRegionData && (
<ModalClose handleClose={() => setWaveSufferRegionData(null)}>
{waveSurferRegionData && (
<ModalClose handleClose={() => setWaveSurferRegionData(null)}>
<div className="spanDetailsWrapper__header">
<P>
<strong>Speech to text result</strong>
@@ -424,43 +435,43 @@ export const Player = ({ call }: PlayerProps) => {
</div>
<div className="spanDetailsWrapper">
<div className="spanDetailsWrapper__detailsWrapper">
{waveSufferRegionData.vendor && (
{waveSurferRegionData.vendor && (
<div className="spanDetailsWrapper__details">
<div className="spanDetailsWrapper__details_header">
<strong>Vendor:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferRegionData.vendor}
{waveSurferRegionData.vendor}
</div>
</div>
)}
{waveSufferRegionData.confidence !== 0 && (
{waveSurferRegionData.confidence !== 0 && (
<div className="spanDetailsWrapper__details">
<div className="spanDetailsWrapper__details_header">
<strong>Confidence:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferRegionData.confidence}
{waveSurferRegionData.confidence}
</div>
</div>
)}
{waveSufferRegionData.language_code && (
{waveSurferRegionData.language_code && (
<div className="spanDetailsWrapper__details">
<div className="spanDetailsWrapper__details_header">
<strong>Language code:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferRegionData.language_code}
{waveSurferRegionData.language_code}
</div>
</div>
)}
{waveSufferRegionData.transcript && (
{waveSurferRegionData.transcript && (
<div className="spanDetailsWrapper__details">
<div className="spanDetailsWrapper__details_header">
<strong>Transcript:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferRegionData.transcript}
{waveSurferRegionData.transcript}
</div>
</div>
)}
@@ -468,8 +479,8 @@ export const Player = ({ call }: PlayerProps) => {
</div>
</ModalClose>
)}
{waveSufferDtmfData && (
<ModalClose handleClose={() => setWaveSufferDtmfData(null)}>
{waveSurferDtmfData && (
<ModalClose handleClose={() => setWaveSurferDtmfData(null)}>
<div className="spanDetailsWrapper__header">
<P>
<strong>Dtmf result</strong>
@@ -482,7 +493,7 @@ export const Player = ({ call }: PlayerProps) => {
<strong>Dtmf:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferDtmfData.dtmf}
{waveSurferDtmfData.dtmf}
</div>
</div>
@@ -491,7 +502,7 @@ export const Player = ({ call }: PlayerProps) => {
<strong>Duration:</strong>
</div>
<div className="spanDetailsWrapper__details_body">
{waveSufferDtmfData.duration}
{waveSurferDtmfData.duration}
</div>
</div>
</div>

View File

@@ -71,6 +71,7 @@
}
.media-container {
overflow-x: auto;
border: 1px solid black;
border-radius: ui-vars.$px01;
padding: 13px;

View File

@@ -31,6 +31,7 @@ import {
VENDOR_SONIOX,
VENDOR_CUSTOM,
VENDOR_COBALT,
VENDOR_ELEVENLABS,
} from "src/vendor";
import { MSG_REQUIRED_FIELDS } from "src/constants";
import {
@@ -45,7 +46,11 @@ import { CredentialStatus } from "./status";
import type { RegionVendors, GoogleServiceKey, Vendor } from "src/vendor/types";
import type { Account, SpeechCredential, UseApiDataMap } from "src/api/types";
import { setAccountFilter, setLocation } from "src/store/localStore";
import { DISABLE_CUSTOM_SPEECH } from "src/api/constants";
import {
DEFAULT_ELEVENLABS_MODEL,
DISABLE_CUSTOM_SPEECH,
ELEVENLABS_MODEL_OPTIONS,
} from "src/api/constants";
type SpeechServiceFormProps = {
credential?: UseApiDataMap<SpeechCredential>;
@@ -77,6 +82,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
const [sttApiKey, setSttApiKey] = useState("");
const [ttsRegion, setTtsRegion] = useState("");
const [ttsApiKey, setTtsApiKey] = useState("");
const [ttsModelId, setTtsModelId] = useState(DEFAULT_ELEVENLABS_MODEL);
const [instanceId, setInstanceId] = useState("");
const [initialCheckCustomTts, setInitialCheckCustomTts] = useState(false);
const [initialCheckCustomStt, setInitialCheckCustomStt] = useState(false);
@@ -197,6 +203,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
...(vendor === VENDOR_COBALT && {
cobalt_server_uri: cobaltServerUri || null,
}),
...(vendor === VENDOR_ELEVENLABS && {
model_id: ttsModelId || null,
}),
};
if (credential && credential.data) {
@@ -231,7 +240,8 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
vendor === VENDOR_MICROSOFT ||
vendor === VENDOR_WELLSAID ||
vendor === VENDOR_DEEPGRAM ||
vendor === VENDOR_SONIOX
vendor === VENDOR_SONIOX ||
vendor === VENDOR_ELEVENLABS
? apiKey
: null,
}),
@@ -388,6 +398,9 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
if (credential.data.cobalt_server_uri) {
setCobaltServerUri(credential.data.cobalt_server_uri);
}
if (credential.data.model_id) {
setTtsModelId(credential.data.model_id);
}
}
}, [credential]);
@@ -490,18 +503,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
<div>Use for text-to-speech</div>
</label>
)}
{vendor !== VENDOR_WELLSAID && vendor !== VENDOR_CUSTOM && (
<label htmlFor="use_for_stt" className="chk">
<input
id="use_for_stt"
name="use_for_stt"
type="checkbox"
onChange={(e) => setSttCheck(e.target.checked)}
defaultChecked={sttCheck}
/>
<div>Use for speech-to-text</div>
</label>
)}
{vendor !== VENDOR_WELLSAID &&
vendor !== VENDOR_CUSTOM &&
vendor !== VENDOR_ELEVENLABS && (
<label htmlFor="use_for_stt" className="chk">
<input
id="use_for_stt"
name="use_for_stt"
type="checkbox"
onChange={(e) => setSttCheck(e.target.checked)}
defaultChecked={sttCheck}
/>
<div>Use for speech-to-text</div>
</label>
)}
{vendor === VENDOR_CUSTOM && (
<Fragment>
<Checkzone
@@ -773,6 +788,7 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
)}
{(vendor === VENDOR_WELLSAID ||
vendor === VENDOR_DEEPGRAM ||
vendor == VENDOR_ELEVENLABS ||
vendor === VENDOR_SONIOX) && (
<fieldset>
<label htmlFor={`${vendor}_apikey`}>
@@ -789,6 +805,20 @@ export const SpeechServiceForm = ({ credential }: SpeechServiceFormProps) => {
/>
</fieldset>
)}
{vendor == VENDOR_ELEVENLABS && (
<fieldset>
<label htmlFor={`${vendor}_apikey`}>Model</label>
<Selector
id={"audio_format"}
name={"audio_format"}
value={ttsModelId}
options={ELEVENLABS_MODEL_OPTIONS}
onChange={(e) => {
setTtsModelId(e.target.value);
}}
/>
</fieldset>
)}
{regions &&
regions[vendor as keyof RegionVendors] &&
vendor !== VENDOR_IBM &&

View File

@@ -112,7 +112,7 @@ export const SpeechServices = () => {
</Icon>
</Link>
</section>
<section className="filters filters--ender">
<section className="filters filters--multi">
<ScopedAccess user={user} scope={Scope.service_provider}>
<AccountFilter
account={[accountSid, setAccountSid]}

View File

@@ -88,7 +88,7 @@ export const Users = () => {
</Icon>
</Link>
</section>
<section className="filters filters--mix">
<section className="filters filters--multi">
<section>
<SearchFilter
placeholder="Filter users"

View File

@@ -19,14 +19,45 @@
grid-gap: ui-vars.$px02;
}
}
> :first-child {
margin-left: auto;
}
&--multi {
overflow-x: auto;
white-space: nowrap;
grid-gap: ui-vars.$px02;
> :first-child {
margin-left: auto;
@media (max-width: 1400px) {
display: grid;
grid-template-columns: repeat(4, 1fr);
> * {
justify-self: end;
}
}
@media (max-width: 1200px) {
display: grid;
grid-template-columns: repeat(3, 1fr);
> * {
justify-self: end;
}
}
@media (max-width: 1000px) {
display: grid;
grid-template-columns: repeat(2, 1fr);
> * {
justify-self: end;
}
}
@media (max-width: 500px) {
display: grid;
grid-template-columns: repeat(1, 1fr);
> * {
justify-self: end;
}
}
}

View File

@@ -8,6 +8,7 @@ import type {
} from "./types";
export const LANG_EN_US = "en-US";
export const ELEVENLABS_LANG_EN = "en";
export const LANG_COBALT_EN_US = "en_US-8khz";
export const LANG_EN_US_STANDARD_C = "en-US-Standard-C";
export const VENDOR_GOOGLE = "google";
@@ -21,6 +22,7 @@ export const VENDOR_NVIDIA = "nvidia";
export const VENDOR_SONIOX = "soniox";
export const VENDOR_CUSTOM = "custom";
export const VENDOR_COBALT = "cobalt";
export const VENDOR_ELEVENLABS = "elevenlabs";
export const vendors: VendorOptions[] = [
{
@@ -67,6 +69,10 @@ export const vendors: VendorOptions[] = [
name: "Cobalt",
value: VENDOR_COBALT,
},
{
name: "ElevenLabs",
value: VENDOR_ELEVENLABS,
},
].sort((a, b) => a.name.localeCompare(b.name)) as VendorOptions[];
export const useRegionVendors = () => {
@@ -129,6 +135,7 @@ export const useSpeechVendors = () => {
import("./speech-synthesis/nuance-speech-synthesis-lang"),
import("./speech-synthesis/ibm-speech-synthesis-lang"),
import("./speech-synthesis/nvidia-speech-synthesis-lang"),
import("./speech-synthesis/elevellabs-speech-synthesis-lang"),
]).then(
([
{ default: awsRecognizer },
@@ -147,6 +154,7 @@ export const useSpeechVendors = () => {
{ default: nuanceSynthesis },
{ default: ibmSynthesis },
{ default: nvidiaynthesis },
{ default: elevenLabsSynthesis },
]) => {
if (!ignore) {
setSpeech({
@@ -158,6 +166,7 @@ export const useSpeechVendors = () => {
nuance: nuanceSynthesis,
ibm: ibmSynthesis,
nvidia: nvidiaynthesis,
elevenlabs: elevenLabsSynthesis,
},
recognizers: {
aws: awsRecognizer,

View File

@@ -0,0 +1,196 @@
import type { VoiceLanguage } from "../types";
export const languages: VoiceLanguage[] = [
{
code: "ar",
name: "Arabic",
voices: [
{
value: "pNInz6obpgDQGcFmaJgB",
name: "Adam - american, deep, middle aged, male, narration",
},
{
value: "ErXwobaYiN019PkySvjV",
name: "Antoni - american, well-rounded, young, male, narration",
},
{
value: "VR6AewLTigWG4xSOukaG",
name: "Arnold - american, crisp, middle aged, male, narration",
},
{
value: "EXAVITQu4vr4xnSDxMaL",
name: "Bella - american, soft, young, female, narration",
},
{
value: "N2lVS1w4EtoT3dr4eOWO",
name: "Callum - american, hoarse, middle aged, male, video games",
},
{
value: "IKne3meq5aSn9XLyUdCD",
name: "Charlie - australian, casual, middle aged, male, conversational",
},
{
value: "XB0fDUnXU5powFXDhCwa",
name: "Charlotte - english-swedish, seductive, middle aged, female, video games",
},
{
value: "2EiwWnXFnvU5JabPnv8n",
name: "Clyde - american, war veteran, middle aged, male, video games",
},
{
value: "onwK4e9ZLuTAKqWW03F9",
name: "Daniel - british, deep, middle aged, male, news presenter",
},
{
value: "CYw3kZ02Hs0563khs1Fj",
name: "Dave - british-essex, conversational, young, male, video games",
},
{
value: "AZnzlk1XvdvUeBnXmlld",
name: "Domi - american, strong, young, female, narration",
},
{
value: "ThT5KcBeYPX3keUQqHPh",
name: "Dorothy - british, pleasant, young, female, children's stories",
},
{
value: "MF3mGyEYCl7XYWbV9V6O",
name: "Elli - american, emotional, young, female, narration",
},
{
value: "LcfcDJNUP1GQjkzn1xUU",
name: "Emily - american, calm, young, female, meditation",
},
{
value: "g5CIjZEefAph4nQFvHAz",
name: "Ethan - american, undefined, young, male, ASMR",
},
{
value: "D38z5RcWu1voky8WS1ja",
name: "Fin - irish, sailor, old, male, video games",
},
{
value: "jsCqWAovK2LkecY7zXl4",
name: "Freya - american, undefined, young, female, undefined",
},
{
value: "jBpfuIE2acCO8z3wKNLl",
name: "Gigi - american, childlish, young, female, animation",
},
{
value: "zcAOhNBS3c14rBihAFp1",
name: "Giovanni - english-italian, foreigner, young, male, audiobook",
},
{
value: "z9fAnlkpzviPz146aGWa",
name: "Glinda - american, witch, middle aged, female, video games",
},
{
value: "oWAxZDx7w5VEj9dCyTzz",
name: "Grace - american-southern, undefined, young, female, audiobook ",
},
{
value: "SOYHLrjzK2X1ezoPC6cr",
name: "Harry - american, anxious, young, male, video games",
},
{
value: "ZQe5CZNOzWyzPSCn5a3c",
name: "James - australian, calm , old, male, news",
},
{
value: "bVMeCyTHy58xNoL34h3p",
name: "Jeremy - american-irish, excited, young, male, narration",
},
{
value: "t0jbNlBVZ17f02VDIeMI",
name: "Jessie - american, raspy , old, male, video games",
},
{
value: "Zlb1dXrM653N07WRdFW3",
name: "Joseph - british, undefined, middle aged, male, news",
},
{
value: "TxGEqnHWrfWFTfGW9XjX",
name: "Josh - american, deep, young, male, narration",
},
{
value: "TX3LPaxmHKxFdv7VOQHJ",
name: "Liam - american, undefined, young, male, narration",
},
{
value: "XrExE9yKIg1WjnnlVkGX",
name: "Matilda - american, warm, young, female, audiobook",
},
{
value: "Yko7PKHZNXotIFUBG7I9",
name: "Matthew - british, undefined, middle aged, male, audiobook",
},
{
value: "flq6f7yk4E4fJM5XTYuZ",
name: "Michael - american, undefined, old, male, audiobook",
},
{
value: "zrHiDhphv9ZnVXBqCLjz",
name: "Mimi - english-swedish, childish, young, female, animation",
},
{
value: "piTKgcLEGmPE4e6mEKli",
name: "Nicole - american, whisper, young, female, audiobook",
},
{
value: "ODq5zmih8GrVes37Dizd",
name: "Patrick - american, shouty, middle aged, male, video games",
},
{
value: "21m00Tcm4TlvDq8ikWAM",
name: "Rachel - american, calm, young, female, narration",
},
{
value: "wViXBPUzp2ZZixB1xQuM",
name: "Ryan - american, soldier, middle aged, male, audiobook",
},
{
value: "yoZ06aMxZJJ28mfd3POQ",
name: "Sam - american, raspy, young, male, narration",
},
{
value: "pMsXgVXv3BLzUgSXRplE",
name: "Serena - american, pleasant, middle aged, female, interactive",
},
{
value: "GBv7mTt0atIp3Br8iCZE",
name: "Thomas - american, calm, young, male, meditation",
},
],
},
{ code: "bg", name: "Bulgarian", voices: [] },
{ code: "zh", name: "Chinese", voices: [] },
{ code: "hr", name: "Croatian", voices: [] },
{ code: "cs", name: "Czech", voices: [] },
{ code: "da", name: "Danish", voices: [] },
{ code: "nl", name: "Dutch", voices: [] },
{ code: "en", name: "English", voices: [] },
{ code: "fil", name: "Filipino", voices: [] },
{ code: "fi", name: "Finnish", voices: [] },
{ code: "fr", name: "French", voices: [] },
{ code: "de", name: "German", voices: [] },
{ code: "el", name: "Greek", voices: [] },
{ code: "hi", name: "Hindi", voices: [] },
{ code: "id", name: "Indonesian", voices: [] },
{ code: "it", name: "Italian", voices: [] },
{ code: "ja", name: "Japanese", voices: [] },
{ code: "ko", name: "Korean", voices: [] },
{ code: "ms", name: "Malay", voices: [] },
{ code: "pl", name: "Polish", voices: [] },
{ code: "pt", name: "Portuguese", voices: [] },
{ code: "ro", name: "Romanian", voices: [] },
{ code: "ru", name: "Russian", voices: [] },
{ code: "sk", name: "Slovak", voices: [] },
{ code: "es", name: "Spanish", voices: [] },
{ code: "sv", name: "Swedish", voices: [] },
{ code: "ta", name: "Tamil", voices: [] },
{ code: "tr", name: "Turkish", voices: [] },
{ code: "uk", name: "Ukrainian", voices: [] },
];
export default languages;

4
src/vendor/types.ts vendored
View File

@@ -9,7 +9,8 @@ export type Vendor =
| "Nvidia"
| "Soniox"
| "Cobalt"
| "Custom";
| "Custom"
| "ElevenLabs";
export interface VendorOptions {
name: Vendor;
@@ -82,6 +83,7 @@ export interface SynthesisVendors {
nuance: VoiceLanguage[];
ibm: VoiceLanguage[];
nvidia: VoiceLanguage[];
elevenlabs: VoiceLanguage[];
}
export interface MSRawSpeech {