Query voice conversion models

This commit is contained in:
Brandon Thomas committed 2023-04-19 02:08:03 -07:00
1 parent 2da69e8ae7
commit 60a085bcef
7 files changed
+293 -19

No files matched your search

+6
View File
@@ -392,6 +392,12 @@ class ApiConfig {
return `${this.getApiOrigin()}/voice_clone_requests/check`;
}
// =============== Voice Conversion ===============
listVoiceConversionModels(): string {
return `${this.getApiOrigin()}/v1/voice_conversion/model_list`;
}
// =============== Generic Model Downloads ===============
enqueueRemoteDownloadJob(): string {
@@ -0,0 +1,60 @@
import { ApiConfig } from "../ApiConfig";
interface VoiceConversionModelListResponsePayload {
success: boolean,
models: Array<VoiceConversionModelListItem>,
}
export interface VoiceConversionModelListItem {
token: string,
model_type: string,
title: string,
creator: CreatorDetails,
creator_set_visibility: string,
ietf_language_tag: string,
ietf_primary_language_subtag: string,
is_front_page_featured: boolean,
created_at: string,
updated_at: string,
}
export interface CreatorDetails {
user_token: string,
username: string,
display_name: string,
gravatar_hash: string,
}
// TODO: user ratings
//export interface UserRatings {
// positive_count: number,
// negative_count: number,
// // Total count does not take into account "neutral" ratings.
// total_count: number,
//}
export async function ListVoiceConversionModels() : Promise<Array<VoiceConversionModelListItem>| undefined> {
const endpoint = new ApiConfig().listVoiceConversionModels();
return await fetch(endpoint, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
credentials: 'include',
})
.then(res => res.json())
.then(res => {
const response : VoiceConversionModelListResponsePayload = res;
if (!response.success) {
return;
}
return response?.models;
})
.catch(e => {
return undefined;
});
}
+20
View File
@@ -47,6 +47,7 @@ import {
import { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
import { GetModelInferenceJobStatus, GetModelInferenceJobStatusIsOk } from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
import { VoiceConversionModelUploadJob } from "@storyteller/components/src/jobs/VoiceConversionModelUploadJob";
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
i18n
.use(initReactI18next) // passes i18n down to react-i18next
@@ -139,6 +140,9 @@ interface State {
// Current text entered
textBuffer: string;
voiceConversionModels: VoiceConversionModelListItem[];
maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem;
}
function newVocodes() {
@@ -199,6 +203,9 @@ class App extends React.Component<Props, State> {
voiceConversionModelUploadJobs: [],
textBuffer: "",
voiceConversionModels: [],
maybeSelectedVoiceConversionModel: undefined,
};
}
@@ -320,6 +327,14 @@ class App extends React.Component<Props, State> {
this.setState({ isShowingBootstrapLanguageNotice: false });
};
setAllVoiceConversionModels = (models: VoiceConversionModelListItem[]) => {
this.setState({ voiceConversionModels: models })
}
setMaybeSelectedVoiceConversionModel =(model?: VoiceConversionModelListItem) => {
this.setState({ maybeSelectedVoiceConversionModel: model })
}
checkInferenceJob = async (jobToken: string) => {
const lookupResult = await GetModelInferenceJobStatus(jobToken);
@@ -776,6 +791,11 @@ class App extends React.Component<Props, State> {
}
selectedTtsLanguageScope={this.props.selectedTtsLanguageScope}
setSelectedTtsLanguageScope={this.props.setSelectedTtsLanguageScope}
voiceConversionModels={this.state.voiceConversionModels}
setVoiceConversionModels={this.setAllVoiceConversionModels}
maybeSelectedVoiceConversionModel={this.state.maybeSelectedVoiceConversionModel}
setMaybeSelectedVoiceConversionModel={this.setMaybeSelectedVoiceConversionModel}
/>
</Route>
</Switch>
@@ -21,6 +21,7 @@ class Analytics {
private static readonly TTS_RESULT = "tts_result";
private static readonly UI = "ui";
private static readonly TOPBAR = "topbar";
private static readonly VOICE_CONVERSION = "voice_conversion";
// ========== USER ==========
@@ -220,6 +221,12 @@ class Analytics {
Analytics.sendCategorizedEvent(this.TTS_RESULT, 'click_share_whatsapp');
}
// ========== VOICE CONVERSION ==========
static ttsOpenPrimaryVoiceConversionSelectMenu() {
Analytics.sendCategorizedEvent(this.VOICE_CONVERSION, 'open_primary_voice_select_menu');
}
// ========== UI ==========
static uiTurnOnAnimations() {
@@ -72,6 +72,7 @@ import { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob";
//import { LandingPage } from "./pages/landing/LandingPage";
import { VcModelListPage } from "./pages/vc/vc_model_list/VcModelListPage";
import { UploadVoiceConversionModel } from "./pages/upload/UploadVoiceConversionModel";
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
interface Props {
sessionWrapper: SessionWrapper;
@@ -149,6 +150,12 @@ interface Props {
selectedTtsLanguageScope: string;
setSelectedTtsLanguageScope: (selectedTtsLanguageScope: string) => void;
voiceConversionModels: Array<VoiceConversionModelListItem>;
setVoiceConversionModels: (ttsVoices: Array<VoiceConversionModelListItem>) => void;
maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem;
setMaybeSelectedVoiceConversionModel: (maybeSelectedVoiceConversionModel: VoiceConversionModelListItem) => void;
}
interface State {}
@@ -486,6 +493,10 @@ class PageContainer extends React.Component<Props, State> {
sessionSubscriptionsWrapper={
this.props.sessionSubscriptionsWrapper
}
voiceConversionModels={this.props.voiceConversionModels}
setVoiceConversionModels={this.props.setVoiceConversionModels}
maybeSelectedVoiceConversionModel={this.props.maybeSelectedVoiceConversionModel}
setMaybeSelectedVoiceConversionModel={this.props.setMaybeSelectedVoiceConversionModel}
/>
</Route>
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper";
import { t } from "i18next";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { container, panel, sessionItem } from "../../../../../data/animation";
import { container, panel } from "../../../../../data/animation";
import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
import { VcPageHero } from "./VcPageHero";
import Select, { createFilter } from "react-select";
@@ -12,25 +12,66 @@ import {
faBarsStaggered,
faFiles,
faHeadphones,
faLink,
faMicrophone,
faRightLeft,
faTimer,
faTrash,
} from "@fortawesome/pro-solid-svg-icons";
import UploadComponent from "./UploadComponent";
import RecordComponent from "./RecordComponent";
import { Link } from "react-router-dom";
import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle";
import { ListVoiceConversionModels, VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
import { VcModelListSearch } from "./VcModelListSearch";
interface Props {
sessionWrapper: SessionWrapper;
sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
voiceConversionModels: Array<VoiceConversionModelListItem>;
setVoiceConversionModels: (ttsVoices: Array<VoiceConversionModelListItem>) => void;
maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem;
setMaybeSelectedVoiceConversionModel: (maybeSelectedVoiceConversionModel: VoiceConversionModelListItem) => void;
}
function VcModelListPage(props: Props) {
const [loading, setLoading] = useState(false);
usePrefixedDocumentTitle("Voice Conversion");
let {
setVoiceConversionModels,
voiceConversionModels,
maybeSelectedVoiceConversionModel,
setMaybeSelectedVoiceConversionModel,
} = props;
const ttsModelsLoaded = voiceConversionModels.length > 0;
const listModels = useCallback(async () => {
if (ttsModelsLoaded) {
return; // Already queried.
}
const models = await ListVoiceConversionModels();
if (models) {
setVoiceConversionModels(models);
if (!maybeSelectedVoiceConversionModel && models.length > 0) {
let model = models[0];
const featuredModels = models.filter((m) => m.is_front_page_featured);
if (featuredModels.length > 0) {
// Random featured model
model =
featuredModels[Math.floor(Math.random() * featuredModels.length)];
}
setMaybeSelectedVoiceConversionModel(model);
}
}
}, [
setVoiceConversionModels,
maybeSelectedVoiceConversionModel,
setMaybeSelectedVoiceConversionModel,
ttsModelsLoaded,
]);
const handleLoading = () => {
setLoading(true);
setTimeout(() => {
@@ -39,11 +80,14 @@ function VcModelListPage(props: Props) {
};
useEffect(() => {
listModels();
const timeout = setTimeout(() => {
setLoading(false);
}, 2000);
return () => clearTimeout(timeout);
}, []);
}, [
handleLoading
]);
const speakButtonClass = loading
? "btn btn-primary w-100 disabled"
@@ -82,8 +126,6 @@ function VcModelListPage(props: Props) {
// </div>
// );
usePrefixedDocumentTitle("Voice Conversion");
return (
<motion.div initial="hidden" animate="visible" variants={container}>
<VcPageHero
@@ -100,23 +142,20 @@ function VcModelListPage(props: Props) {
>
{/* Explore Rollout */}
<label className="sub-title">
Choose Target Voice (XX to choose from)
Choose Target Voice ({voiceConversionModels.length} to choose from)
</label>
<div className="input-icon-search pb-4">
<span className="form-control-feedback">
<FontAwesomeIcon icon={faMicrophone} />
</span>
<Select
value="test"
classNames={SearchFieldClass}
// On mobile, we don't want the onscreen keyboard to take up half the UI.
autoFocus={false}
isSearchable={false}
// NB: The following settings improve upon performance.
// See: https://github.com/JedWatson/react-select/issues/3128
filterOption={createFilter({ ignoreAccents: false })}
/>
<VcModelListSearch
voiceConversionModels={props.voiceConversionModels}
setVoiceConversionModels={props.setVoiceConversionModels}
maybeSelectedVoiceConversionModel={props.maybeSelectedVoiceConversionModel}
setMaybeSelectedVoiceConversionModel={props.setMaybeSelectedVoiceConversionModel}
/>
</div>
<div className="row gx-5 gy-5">
@@ -0,0 +1,131 @@
import { VoiceConversionModelListItem } from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels";
import React from "react";
import Select, { createFilter } from "react-select";
import { SearchFieldClass } from "../../tts/tts_model_list/search/components/SearchFieldClass";
import { FastReactSelectOption } from "../../../_common/react_select/FastReactSelectOption";
import { Analytics } from "../../../../../common/Analytics";
import { FixedSingleValueSelectOption } from "../../../_common/react_select/FixedSingleValueSelectOption";
interface Props {
voiceConversionModels: Array<VoiceConversionModelListItem>;
setVoiceConversionModels: (ttsVoices: Array<VoiceConversionModelListItem>) => void;
maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem;
setMaybeSelectedVoiceConversionModel: (maybeSelectedVoiceConversionModel: VoiceConversionModelListItem) => void;
}
function VcModelListSearch(props: Props) {
const handleChange = (option: any, actionMeta: any) => {
const voiceConversionModelToken = option?.value;
// TODO: Cache a lookup table
//const maybeNewVoiceConversionModel = props.allVoiceConversionModelsByTokenMap.get(token);
const maybeNewVoiceConversionModel = props.voiceConversionModels.find((voiceConversionModel) => {
return voiceConversionModel.token == voiceConversionModelToken;
});
if (maybeNewVoiceConversionModel !== undefined) {
props.setMaybeSelectedVoiceConversionModel(maybeNewVoiceConversionModel);
}
};
interface DropdownOption {
label: string;
value: string;
creatorName?: string;
}
let options : DropdownOption[] = props.voiceConversionModels
.map((voiceConversionModel) => {
return {
label: voiceConversionModel.title,
value: voiceConversionModel.token,
creatorName: voiceConversionModel.creator.display_name,
};
});
let selectedOption = options.find(
(option) => option.value === props.maybeSelectedVoiceConversionModel?.token
);
if (selectedOption === undefined && options.length > 0) {
// NB: We shouldn't select the first item in the list since that won't update the currently
// selected model. If the user were to close the dialogue, they'd think they had picked a voice,
// when in reality no state would have changed. By forcing the user to choose, the user will set
// the state appropriately.
selectedOption = {
label: "Select voice...",
value: "*",
creatorName: undefined,
};
}
let isLoading = false;
if (props.voiceConversionModels.length === 0) {
// NB: react-select will cache values, even across different instances (!!!)
// This can cause confusion when initializing a select instance before the data
// is loaded, and the select will never update to show the new data.
// The proper way to change voices after load from a placeholder "Loading..."
// label is to use controlled props / value as is done here:
isLoading = true;
selectedOption = {
label: "Loading...",
value: "*",
creatorName: undefined,
};
} else if (options.length === 0) {
// NB: Perhaps the user has refined their search to be too narrow (langauge + category)
selectedOption = {
label: "No results (remove some filters)",
value: "*",
creatorName: undefined,
};
}
// Function to build the options themselves, so we can introduce extra elements.
const formatOptionLabel = (data: DropdownOption, formatOptionLabelMeta: any) => {
let creatorName = <></>
if (data.creatorName !== undefined) {
creatorName = (
<span className="opacity-50">
{" "} — {data.creatorName}
</span>
);
}
return (
<div>
{data.label}{creatorName}
</div>
)
};
return (
<>
<Select
value={selectedOption}
options={options}
classNames={SearchFieldClass}
onChange={handleChange}
onMenuOpen={() => {
Analytics.ttsOpenPrimaryVoiceConversionSelectMenu();
}}
autoFocus={false}
isLoading={isLoading}
isSearchable={true}
// NB: The following settings improve upon performance.
// See: https://github.com/JedWatson/react-select/issues/3128
filterOption={createFilter({ ignoreAccents: false })}
components={
{
SingleValue: FixedSingleValueSelectOption,
Option: FastReactSelectOption,
} as any
}
formatOptionLabel={formatOptionLabel}
/>
</>
)
}
export { VcModelListSearch };