mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
Query voice conversion models
This commit is contained in:
1 parent
2da69e8ae7
commit
60a085bcef
7 files changed
+293
-19
No files matched your search
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
Reference in new issue
Block a user