diff --git a/packages/fakeyou.com/src/scss/site-refresh.scss b/packages/fakeyou.com/src/scss/site-refresh.scss index aa83fcc8bc..67d4fb1b89 100644 --- a/packages/fakeyou.com/src/scss/site-refresh.scss +++ b/packages/fakeyou.com/src/scss/site-refresh.scss @@ -1491,7 +1491,7 @@ textarea { } .session-vc-section { - height: 565px; + height: 660px; } .session-tts-section::-webkit-scrollbar { diff --git a/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/packages/fakeyou.com/src/v2/view/PageContainer.tsx index 896d640401..52055651c4 100644 --- a/packages/fakeyou.com/src/v2/view/PageContainer.tsx +++ b/packages/fakeyou.com/src/v2/view/PageContainer.tsx @@ -524,6 +524,9 @@ class PageContainer extends React.Component { this.props.sessionSubscriptionsWrapper } inferenceJobsByCategory={this.props.inferenceJobsByCategory} + setMaybeSelectedInferenceJob={ + this.props.maybeSelectedVoiceConversionModel + } /> diff --git a/packages/fakeyou.com/src/v2/view/pages/vc/VcGenerateAudioPanel.tsx b/packages/fakeyou.com/src/v2/view/pages/vc/VcGenerateAudioPanel.tsx new file mode 100644 index 0000000000..4be7afe64c --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/vc/VcGenerateAudioPanel.tsx @@ -0,0 +1,411 @@ +import React, { useCallback, useEffect, useState } from "react"; +import Panel from "components/common/Panel/Panel"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faBarsStaggered, + faRightLeft, + faWaveformLines, +} from "@fortawesome/pro-solid-svg-icons"; +import { SessionVoiceConversionResultsList } from "v2/view/_common/SessionVoiceConversionResultsList"; +import { + FrontendInferenceJobType, + InferenceJob, +} from "@storyteller/components/src/jobs/InferenceJob"; +import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; +import UploadComponent from "./vc_model_list/components/UploadComponent"; +import { + EnqueueVoiceConversion, + EnqueueVoiceConversionFrequencyMethod, + EnqueueVoiceConversionIsSuccess, + EnqueueVoiceConversionRequest, +} from "@storyteller/components/src/api/voice_conversion/EnqueueVoiceConversion"; +import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapper"; +import { + VoiceConversionModelListItem, + ListVoiceConversionModels, +} from "@storyteller/components/src/api/voice_conversion/ListVoiceConversionModels"; +import { Analytics } from "common/Analytics"; +import LoadingIcon from "components/common/FileActions/LoadingIcon"; +import PitchEstimateMethodComponent from "./vc_model_list/components/PitchEstimateMethodComponent"; +import PitchShiftComponent from "./vc_model_list/components/PitchShiftComponent"; +import RecordComponent from "./vc_model_list/components/RecordComponent"; +import { v4 as uuidv4 } from "uuid"; + +interface VcGenerateAudioPanelProps { + sessionWrapper: SessionWrapper; + sessionSubscriptionsWrapper: SessionSubscriptionsWrapper; + + voiceConversionModels: Array; + setVoiceConversionModels: ( + ttsVoices: Array + ) => void; + + maybeSelectedVoiceConversionModel?: VoiceConversionModelListItem; + setMaybeSelectedVoiceConversionModel: ( + maybeSelectedVoiceConversionModel: VoiceConversionModelListItem + ) => void; + + enqueueInferenceJob: ( + jobToken: string, + frontendInferenceJobType: FrontendInferenceJobType + ) => void; + inferenceJobs: Array; + inferenceJobsByCategory: Map>; +} + +export default function VcGenerateAudioPanel(props: VcGenerateAudioPanelProps) { + const [convertLoading, setConvertLoading] = useState(false); + const [canConvert, setCanConvert] = useState(false); + + const [mediaUploadToken, setMediaUploadToken] = useState( + undefined + ); + + const [convertIdempotencyToken, setConvertIdempotencyToken] = useState( + uuidv4() + ); + + const [autoConvertF0, setAutoConvertF0] = useState(false); + + const [maybeF0MethodOverride, setMaybeF0MethodOverride] = useState< + EnqueueVoiceConversionFrequencyMethod | undefined + >(undefined); + + const [semitones, setSemitones] = useState(0); + + // NB: Something of a UI hack here. + // The 3rd party microphone component doesn't let you clear it, so we emulate form clearing + // with this variable. + const [formIsCleared, setFormIsCleared] = useState(false); + + let { + setVoiceConversionModels, + voiceConversionModels, + maybeSelectedVoiceConversionModel, + setMaybeSelectedVoiceConversionModel, + } = props; + + const vcModelsLoaded = voiceConversionModels.length > 0; + + const listModels = useCallback(async () => { + if (vcModelsLoaded) { + 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, + vcModelsLoaded, + ]); + + useEffect(() => { + listModels(); + }, [listModels]); + + const changeConvertIdempotencyToken = () => { + setConvertIdempotencyToken(uuidv4()); + }; + + const handleVoiceConversion = async () => { + if ( + props.maybeSelectedVoiceConversionModel === undefined || + mediaUploadToken === undefined + ) { + return; + } + + setConvertLoading(true); + + let request: EnqueueVoiceConversionRequest = { + uuid_idempotency_token: convertIdempotencyToken, + voice_conversion_model_token: + props.maybeSelectedVoiceConversionModel.token, + source_media_upload_token: mediaUploadToken, + }; + + if (semitones !== 0) { + request.transpose = semitones; + } + + if (maybeF0MethodOverride !== undefined) { + request.override_f0_method = maybeF0MethodOverride; + } + + if (autoConvertF0) { + request.auto_predict_f0 = true; + } + + Analytics.voiceConversionGenerate( + props.maybeSelectedVoiceConversionModel.token + ); + + let result = await EnqueueVoiceConversion(request); + + if (EnqueueVoiceConversionIsSuccess(result)) { + props.enqueueInferenceJob( + result.inference_job_token, + FrontendInferenceJobType.VoiceConversion + ); + } + + setConvertLoading(false); + }; + + const handleFormSubmit = async (ev: React.FormEvent) => { + ev.preventDefault(); + }; + + const handlePitchChange = (value: any) => { + setSemitones(value); + changeConvertIdempotencyToken(); + }; + + const handlePitchMethodChange = (value: any) => { + setMaybeF0MethodOverride(value); + changeConvertIdempotencyToken(); + }; + + const handleAutoF0Change = (ev: React.FormEvent) => { + const value = (ev.target as HTMLInputElement).checked; + setAutoConvertF0(value); + changeConvertIdempotencyToken(); + }; + + const speakButtonClass = convertLoading + ? "btn btn-primary w-100 disabled" + : "btn btn-primary w-100"; + + const enableConvertButton = + canConvert && + mediaUploadToken !== undefined && + props.maybeSelectedVoiceConversionModel !== undefined; + + return ( + +
+
+
+

+ + Use Voice +

+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +
+ + +
+ +
+
+
+
+
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +
+ + +
+ +
+
+
+
+
+
+
+

+ + Session V2V Results +

+
+ +
+
+
+
+
+ ); +} diff --git a/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_view/VcModelViewPage.tsx b/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_view/VcModelViewPage.tsx index 585ef8628a..d5407e4ad9 100644 --- a/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_view/VcModelViewPage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/vc/vc_model_view/VcModelViewPage.tsx @@ -1,19 +1,15 @@ -import React, { useState } from "react"; +import React from "react"; import { - faBarsStaggered, faEdit, faEye, faMemo, faMemoCircleInfo, faMessages, faTrash, - faVolumeHigh, } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { FrontendInferenceJobType } from "@storyteller/components/src/jobs/InferenceJob"; import Panel from "components/common/Panel/Panel"; import { Link, useParams } from "react-router-dom"; -import { SessionVoiceConversionResultsList } from "v2/view/_common/SessionVoiceConversionResultsList"; import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; import PageHeaderModelView from "components/layout/PageHeaderModelView/PageHeaderModelView"; import { CommentComponent } from "v2/view/_common/comments/CommentComponent"; @@ -22,8 +18,10 @@ import { RatingButtons } from "v2/view/_common/ratings/RatingButtons"; import { RatingStats } from "v2/view/_common/ratings/RatingStats"; import ShareButton from "components/common/ShareButton/ShareButton"; import PageContainer from "components/common/PageContainer/PageContainer"; +import VcGenerateAudioPanel from "../VcGenerateAudioPanel"; interface VcModelViewPageProps { + setMaybeSelectedInferenceJob: any; sessionWrapper: SessionWrapper; sessionWrapper: SessionWrapper; sessionSubscriptionsWrapper: SessionSubscriptionsWrapper; @@ -148,34 +146,16 @@ export default function VcModelViewPage(props: VcModelViewPageProps) { extras={shareButton} /> - -
-
-

- - Use Voice -

- {/* have to replace this below with Voice conversion stuff instead of the textarea */} -
voice to voice components here
-
-
-

- - Session V2V Results -

-
- -
-
-
-
+ {}} + voiceConversionModels={[]} + setMaybeSelectedVoiceConversionModel={() => {}} + enqueueInferenceJob={() => {}} + inferenceJobs={[]} + /> {modelDescription && (