diff --git a/packages/components/src/api/ApiConfig.ts b/packages/components/src/api/ApiConfig.ts index 7433c63b15..bb007050c7 100644 --- a/packages/components/src/api/ApiConfig.ts +++ b/packages/components/src/api/ApiConfig.ts @@ -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 { diff --git a/packages/components/src/api/voice_conversion/ListVoiceConversionModels.ts b/packages/components/src/api/voice_conversion/ListVoiceConversionModels.ts new file mode 100644 index 0000000000..761d98cbc7 --- /dev/null +++ b/packages/components/src/api/voice_conversion/ListVoiceConversionModels.ts @@ -0,0 +1,60 @@ +import { ApiConfig } from "../ApiConfig"; + +interface VoiceConversionModelListResponsePayload { + success: boolean, + models: Array, +} + +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| 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; + }); +} diff --git a/packages/fakeyou.com/src/App.tsx b/packages/fakeyou.com/src/App.tsx index e041710e17..611a2a1eb5 100644 --- a/packages/fakeyou.com/src/App.tsx +++ b/packages/fakeyou.com/src/App.tsx @@ -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 { voiceConversionModelUploadJobs: [], textBuffer: "", + + voiceConversionModels: [], + maybeSelectedVoiceConversionModel: undefined, }; } @@ -320,6 +327,14 @@ class App extends React.Component { 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 { } selectedTtsLanguageScope={this.props.selectedTtsLanguageScope} setSelectedTtsLanguageScope={this.props.setSelectedTtsLanguageScope} + + voiceConversionModels={this.state.voiceConversionModels} + setVoiceConversionModels={this.setAllVoiceConversionModels} + maybeSelectedVoiceConversionModel={this.state.maybeSelectedVoiceConversionModel} + setMaybeSelectedVoiceConversionModel={this.setMaybeSelectedVoiceConversionModel} /> diff --git a/packages/fakeyou.com/src/common/Analytics.ts b/packages/fakeyou.com/src/common/Analytics.ts index 7839ee7dd4..dcc5915657 100644 --- a/packages/fakeyou.com/src/common/Analytics.ts +++ b/packages/fakeyou.com/src/common/Analytics.ts @@ -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() { diff --git a/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/packages/fakeyou.com/src/v2/view/PageContainer.tsx index 3e7854d5a1..ebb8c647d0 100644 --- a/packages/fakeyou.com/src/v2/view/PageContainer.tsx +++ b/packages/fakeyou.com/src/v2/view/PageContainer.tsx @@ -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; + setVoiceConversionModels: (ttsVoices: Array) => void; + + maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem; + setMaybeSelectedVoiceConversionModel: (maybeSelectedVoiceConversionModel: VoiceConversionModelListItem) => void; } interface State {} @@ -486,6 +493,10 @@ class PageContainer extends React.Component { sessionSubscriptionsWrapper={ this.props.sessionSubscriptionsWrapper } + voiceConversionModels={this.props.voiceConversionModels} + setVoiceConversionModels={this.props.setVoiceConversionModels} + maybeSelectedVoiceConversionModel={this.props.maybeSelectedVoiceConversionModel} + setMaybeSelectedVoiceConversionModel={this.props.setMaybeSelectedVoiceConversionModel} /> diff --git a/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_list/VcModelListPage.tsx b/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_list/VcModelListPage.tsx index a706200cb1..fceb021575 100644 --- a/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_list/VcModelListPage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_list/VcModelListPage.tsx @@ -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; + setVoiceConversionModels: (ttsVoices: Array) => 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) { // // ); - usePrefixedDocumentTitle("Voice Conversion"); - return ( {/* Explore Rollout */}
- { + 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 }; \ No newline at end of file