From 0aba1957e845fe7a62ee02f67f28609f69c23645 Mon Sep 17 00:00:00 2001 From: BFlat Date: Fri, 18 Oct 2024 20:22:07 +0700 Subject: [PATCH] F5-TTS product page (#720) * f5-tts product page * footer fix * ai tools section on f5-tts update * final updates --- .../src/api/f5_tts/GenerateF5TtsAudio.ts | 23 ++ .../components/src/jobs/InferenceJob.ts | 1 + .../components/marketing/AITools/AITools.tsx | 4 + .../packages/fakeyou.com/src/hooks/index.tsx | 2 + .../fakeyou.com/src/hooks/useF5Store/index.ts | 1 + .../src/hooks/useF5Store/useF5Store.tsx | 45 +++ .../src/hooks/useInferenceJobsPolling.tsx | 2 + .../fakeyou.com/src/scss/site-refresh.scss | 12 + .../fakeyou.com/src/v2/view/PageContainer.tsx | 2 +- .../view/_common/SessionF5TtsResultsList.tsx | 284 ++++++++++++++++ .../fakeyou.com/src/v2/view/nav/FooterNav.tsx | 4 +- .../pages/audio_gen/vc/VCRecordComponent.tsx | 6 +- .../F5TTS.scss} | 106 ++++-- .../src/v2/view/pages/f5_tts/F5TTS.tsx | 314 ++++++++++++++++++ .../VoiceDesignerV2.tsx | 4 +- .../f5_tts/components/F5RecordComponent.tsx | 205 ++++++++++++ .../f5_tts/components/F5UploadComponent.tsx | 227 +++++++++++++ 17 files changed, 1208 insertions(+), 34 deletions(-) create mode 100644 src/website/packages/components/src/api/f5_tts/GenerateF5TtsAudio.ts create mode 100644 src/website/packages/fakeyou.com/src/hooks/useF5Store/index.ts create mode 100644 src/website/packages/fakeyou.com/src/hooks/useF5Store/useF5Store.tsx create mode 100644 src/website/packages/fakeyou.com/src/v2/view/_common/SessionF5TtsResultsList.tsx rename src/website/packages/fakeyou.com/src/v2/view/pages/{voice_designer_v2/VoiceDesignerV2.scss => f5_tts/F5TTS.scss} (55%) create mode 100644 src/website/packages/fakeyou.com/src/v2/view/pages/f5_tts/F5TTS.tsx rename src/website/packages/fakeyou.com/src/v2/view/pages/{voice_designer_v2 => f5_tts}/VoiceDesignerV2.tsx (99%) create mode 100644 src/website/packages/fakeyou.com/src/v2/view/pages/f5_tts/components/F5RecordComponent.tsx create mode 100644 src/website/packages/fakeyou.com/src/v2/view/pages/f5_tts/components/F5UploadComponent.tsx diff --git a/src/website/packages/components/src/api/f5_tts/GenerateF5TtsAudio.ts b/src/website/packages/components/src/api/f5_tts/GenerateF5TtsAudio.ts new file mode 100644 index 0000000000..5c5a5680ee --- /dev/null +++ b/src/website/packages/components/src/api/f5_tts/GenerateF5TtsAudio.ts @@ -0,0 +1,23 @@ +import MakeRequest from "../MakeRequest"; + +export interface GenerateF5TtsAudioRequest { + uuid_idempotency_token: string; + source_media_file_token: string; + inference_text: string; + creator_set_visibility: string; +} + +export interface GenerateF5TtsAudioResponse { + success: boolean; + inference_job_token: string; +} + +export const GenerateF5TtsAudio = MakeRequest< + string, + GenerateF5TtsAudioRequest, + GenerateF5TtsAudioResponse, + {} +>({ + method: "POST", + routingFunction: () => "/v1/tts/f5_inference", +}); diff --git a/src/website/packages/components/src/jobs/InferenceJob.ts b/src/website/packages/components/src/jobs/InferenceJob.ts index c37898e273..ce138bac56 100644 --- a/src/website/packages/components/src/jobs/InferenceJob.ts +++ b/src/website/packages/components/src/jobs/InferenceJob.ts @@ -20,6 +20,7 @@ export enum FrontendInferenceJobType { VideoWorkflow, // workflow ? VideoStyleTransfer, // video_filter LivePortrait, // live_portrait + F5Tts, // f5_tts } export enum AllInferenceJobs { diff --git a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx index 5bd43e8cb5..c404548820 100644 --- a/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx +++ b/src/website/packages/fakeyou.com/src/components/marketing/AITools/AITools.tsx @@ -207,6 +207,10 @@ export default function AITools() { items = [items[0], items[1], items[2], items[3], items[4], items[5]]; } + if (currentPath.includes("/f5-tts")) { + items = [items[0], items[1], items[2], items[3], items[4], items[5]]; + } + if (currentPath.includes("/style-video")) { items = [items[1], items[2], items[8]]; } diff --git a/src/website/packages/fakeyou.com/src/hooks/index.tsx b/src/website/packages/fakeyou.com/src/hooks/index.tsx index 946159c21c..d7e70e83ce 100644 --- a/src/website/packages/fakeyou.com/src/hooks/index.tsx +++ b/src/website/packages/fakeyou.com/src/hooks/index.tsx @@ -11,6 +11,7 @@ import useCameraState, { CameraState } from "./useCameraState"; import useChanger from "./useChanger"; import useCoverImgUpload from "./useCoverImgUpload"; import useDebounce from "./useDebounce"; +import useF5Store from "./useF5Store"; import useFile from "./useFile"; import useHover, { HoverValues } from "./useHover"; import useId from "./useId"; @@ -50,6 +51,7 @@ export { useChanger, useCoverImgUpload, useDebounce, + useF5Store, useFile, useHover, useId, diff --git a/src/website/packages/fakeyou.com/src/hooks/useF5Store/index.ts b/src/website/packages/fakeyou.com/src/hooks/useF5Store/index.ts new file mode 100644 index 0000000000..b3af31696d --- /dev/null +++ b/src/website/packages/fakeyou.com/src/hooks/useF5Store/index.ts @@ -0,0 +1 @@ +export { default } from "./useF5Store"; diff --git a/src/website/packages/fakeyou.com/src/hooks/useF5Store/useF5Store.tsx b/src/website/packages/fakeyou.com/src/hooks/useF5Store/useF5Store.tsx new file mode 100644 index 0000000000..128880c28c --- /dev/null +++ b/src/website/packages/fakeyou.com/src/hooks/useF5Store/useF5Store.tsx @@ -0,0 +1,45 @@ +import { create } from "zustand"; + +interface F5StoreState { + mediaUploadToken: string | undefined; + setMediaUploadToken: (token: string | undefined) => void; + hasUploadedFile: boolean; + setHasUploadedFile: (value: boolean) => void; + hasRecordedFile: boolean; + setHasRecordedFile: (value: boolean) => void; + recordingBlobStore: Blob | undefined; + setRecordingBlobStore: (blob: Blob | undefined) => void; + isUploadDisabled: boolean; + setIsUploadDisabled: (value: boolean) => void; + file: any; + setFile: (file: any) => void; + audioLink: string | undefined; + setAudioLink: (link: string | undefined) => void; + formIsCleared: boolean; + setFormIsCleared: (cleared: boolean) => void; + text: string; + setText: (text: string) => void; +} + +const useF5Store = create(set => ({ + mediaUploadToken: undefined, + setMediaUploadToken: token => set({ mediaUploadToken: token }), + hasUploadedFile: false, + setHasUploadedFile: value => set({ hasUploadedFile: value }), + hasRecordedFile: false, + setHasRecordedFile: value => set({ hasRecordedFile: value }), + recordingBlobStore: undefined, + setRecordingBlobStore: blob => set({ recordingBlobStore: blob }), + isUploadDisabled: false, + setIsUploadDisabled: value => set({ isUploadDisabled: value }), + file: undefined, + setFile: file => set({ file: file }), + audioLink: undefined, + setAudioLink: link => set({ audioLink: link }), + formIsCleared: false, + setFormIsCleared: cleared => set({ formIsCleared: cleared }), + text: "", + setText: text => set({ text }), +})); + +export default useF5Store; diff --git a/src/website/packages/fakeyou.com/src/hooks/useInferenceJobsPolling.tsx b/src/website/packages/fakeyou.com/src/hooks/useInferenceJobsPolling.tsx index 952477ca01..43c00ef75a 100644 --- a/src/website/packages/fakeyou.com/src/hooks/useInferenceJobsPolling.tsx +++ b/src/website/packages/fakeyou.com/src/hooks/useInferenceJobsPolling.tsx @@ -42,6 +42,8 @@ const JobCategoryToType = (jobCategory: string) => { return FrontendInferenceJobType.EngineComposition; case "live_portrait": return FrontendInferenceJobType.LivePortrait; + case "f5_tts": + return FrontendInferenceJobType.F5Tts; default: return FrontendInferenceJobType.Unknown; } diff --git a/src/website/packages/fakeyou.com/src/scss/site-refresh.scss b/src/website/packages/fakeyou.com/src/scss/site-refresh.scss index a04a07bc5f..ba78d0fdc2 100644 --- a/src/website/packages/fakeyou.com/src/scss/site-refresh.scss +++ b/src/website/packages/fakeyou.com/src/scss/site-refresh.scss @@ -1450,6 +1450,14 @@ styles with important tags: height: 100px; } } + + &.empty-zone-f5 { + height: 120px; + + @media (max-width: 768px) { + height: 100px; + } + } } .upload-zone-drag { @@ -1889,6 +1897,10 @@ textarea { height: 511px; } + .session-f5-section { + height: 637px; + } + .session-tts-section::-webkit-scrollbar { width: 10px; } diff --git a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx index e5e6fb5a9f..b367377798 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx @@ -443,7 +443,7 @@ const routes = [ }, { path: "/f5-tts", - component: lazy(() => import("./pages/voice_designer_v2/VoiceDesignerV2")), + component: lazy(() => import("./pages/f5_tts/F5TTS")), }, { path: "/style-video/:mediaToken?", diff --git a/src/website/packages/fakeyou.com/src/v2/view/_common/SessionF5TtsResultsList.tsx b/src/website/packages/fakeyou.com/src/v2/view/_common/SessionF5TtsResultsList.tsx new file mode 100644 index 0000000000..5ff5f7c3ff --- /dev/null +++ b/src/website/packages/fakeyou.com/src/v2/view/_common/SessionF5TtsResultsList.tsx @@ -0,0 +1,284 @@ +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { BucketConfig } from "@storyteller/components/src/api/BucketConfig"; +import { JobState } from "@storyteller/components/src/jobs/JobStates"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faClock, faHeadphonesSimple } from "@fortawesome/free-solid-svg-icons"; +import { Analytics } from "../../../common/Analytics"; +import { SessionTtsAudioPlayer } from "./SessionTtsAudioPlayer"; +import { WebUrl } from "../../../common/WebUrl"; +import { + GetPendingTtsJobCount, + GetPendingTtsJobCountIsOk, + GetPendingTtsJobCountSuccessResponse, +} from "@storyteller/components/src/api/tts/GetPendingTtsJobCount"; +import { + FrontendInferenceJobType, + InferenceJob, +} from "@storyteller/components/src/jobs/InferenceJob"; +import { useInferenceJobs, useLocalize, useSession } from "hooks"; +import { Button } from "components/common"; +import { + faArrowDownToLine, + faArrowRight, + faStars, +} from "@fortawesome/pro-solid-svg-icons"; +import LoadingSpinner from "components/common/LoadingSpinner"; + +// TODO: This is duplicated in SessionTtsInferenceResultsList ! +// Default to querying every 15 seconds, but make it configurable serverside +const DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS = 15000; + +function SessionF5TtsResultsList() { + const { t } = useLocalize("SessionVoiceConversionResultsList"); + const { t: t2 } = useLocalize("NewVC"); + const [pendingTtsJobs, setPendingTtsJobs] = + useState({ + success: true, + pending_job_count: 0, + cache_time: new Date(0), // NB: Epoch is used for vector clock's initial state + refresh_interval_millis: DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS, + }); + const { loggedInOrModal, loggedIn, sessionSubscriptions } = useSession(); + const { inferenceJobsByCategory } = useInferenceJobs(); + + useEffect(() => { + const fetch = async () => { + const response = await GetPendingTtsJobCount(); + if (GetPendingTtsJobCountIsOk(response)) { + if ( + response.cache_time.getTime() > pendingTtsJobs.cache_time.getTime() + ) { + setPendingTtsJobs(response); + } + } + }; + // TODO: We're having an outage and need to lower this. + //const interval = setInterval(async () => fetch(), 15000); + const refreshInterval = Math.max( + DEFAULT_QUEUE_REFRESH_INTERVAL_MILLIS, + pendingTtsJobs.refresh_interval_millis + ); + const interval = setInterval(async () => fetch(), refreshInterval); + fetch(); + return () => clearInterval(interval); + }, [pendingTtsJobs]); + + let results: Array = []; + + // TODO(bt,2023-04-08): Clean this utter garbage duplication up. + + // ============================= GENERIC INFERENCE ============================= + + inferenceJobsByCategory + .get(FrontendInferenceJobType.F5Tts) + .forEach((job: InferenceJob, index: number, array: InferenceJob[]) => { + const reverseIndex = array.length - index; + if (!job.maybeResultToken) { + let cssStyle = "alert alert-secondary mb-0"; + let stateDescription = "Pending..."; + let loadingSpinner = null; + let percentage = job.progressPercentage; + + switch (job.jobState) { + case JobState.PENDING: + case JobState.UNKNOWN: + stateDescription = + job.maybeExtraStatusDescription == null + ? t("resultsProgressPending") + : job.maybeExtraStatusDescription; + loadingSpinner = ( + + ); + break; + case JobState.STARTED: + cssStyle = "alert alert-success mb-0"; + stateDescription = + job.maybeExtraStatusDescription == null + ? t("resultsProgressStarted") + : job.maybeExtraStatusDescription; + loadingSpinner = ( + + ); + break; + case JobState.ATTEMPT_FAILED: + cssStyle = "alert alert-danger mb-0"; + stateDescription = `${ + (t("resultsProgressFail"), { 0: job.attemptCount || "0" }) + }}`; + loadingSpinner = ( + + ); + break; + case JobState.COMPLETE_FAILURE: + case JobState.DEAD: + cssStyle = "alert alert-danger mb-0"; + // TODO(bt,2023-01-23): Translate when I can test it + stateDescription = t("resultsProgressDead"); + break; + case JobState.COMPLETE_SUCCESS: + cssStyle = "message is-success mb-0"; + // Not sure why we're here instead of other branch! + stateDescription = t("resultsProgressSuccess"); + break; + } + + results.push( +
+
+ {loadingSpinner} + {stateDescription} + {percentage !== 0 && ` (${percentage}%)`} +
+
+ ); + } else { + let audioLink = new BucketConfig().getGcsUrl( + job.maybeResultPublicBucketMediaPath + ); + let audioPermalink = `/media/${job.maybeResultToken}`; + + let wavesurfers = ; + + results.push( +
+ {/*
+

{job.title}

+ +
*/} +
+
+
+
+
+ Audio #{reverseIndex} +
+
+

{job.maybeRawInferenceText}

+
+ +
+ {wavesurfers} +
+ + {/*
+ { + Analytics.ttsClickResultLink(); + }} + className="fw-semibold" + > + + {t("resultsAudioShareDownload")} + +
*/} +
+
+
+ ); + } + }); + + let noResultsSection = ( +
+
+ +
{t2("sessionResults.emptyTitle")}
+

{t2("sessionResults.emptySubtitle")}

+
+
+ ); + + if (results.length === 0) { + return <>{noResultsSection}; + } + + let upgradeNotice = <>; + + // Ask non-premium users to upgrade + if (results.length !== 0 && !sessionSubscriptions?.hasPaidFeatures()) { + if (loggedIn) { + upgradeNotice = ( +
+
+ + {t2("sessionResults.alertNoPlan")}{" "} + { + Analytics.ttsTooSlowUpgradePremium(); + }} + className="alert-link fw-semibold" + > + {t2("sessionResults.alertNoPlanLink")} + +
+
+ ); + } else { + upgradeNotice = ( +
+
+ + {t2("sessionResults.alertNonUser")}{" "} +
+
+ ); + } + } + + return ( +
+ {upgradeNotice} +
{results}
+
+ ); +} + +export { SessionF5TtsResultsList }; diff --git a/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx b/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx index 18e6b2c23e..0f3e285227 100644 --- a/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx +++ b/src/website/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx @@ -134,12 +134,10 @@ function FooterNav() { return null; } - const isOnF5TtsPage = location.pathname === "/f5-tts"; - return ( <> {!isOnStudioPage && ( -