diff --git a/packages/components/src/api/GetApiHost.ts b/packages/components/src/api/GetApiHost.ts index aaf6b3a2ba..f49b5d2b6f 100644 --- a/packages/components/src/api/GetApiHost.ts +++ b/packages/components/src/api/GetApiHost.ts @@ -22,8 +22,6 @@ export default function GetApiHost() { return hostConfig("api.fakeyou.com"); case "devproxy.storyteller.ai": return hostConfig("api.storyteller.ai"); - case "dev.fakeyou.com": - return hostConfig("api.dev.fakeyou.com"); // NB: for dev machines with nginx proxies case "dev.fakeyou.com": return hostConfig("api.dev.fakeyou.com:12345",false); // false disables SSL default: diff --git a/packages/components/src/env/FakeYouFrontendEnvironment.ts b/packages/components/src/env/FakeYouFrontendEnvironment.ts index fc315730fb..1e98f34bf8 100644 --- a/packages/components/src/env/FakeYouFrontendEnvironment.ts +++ b/packages/components/src/env/FakeYouFrontendEnvironment.ts @@ -10,19 +10,16 @@ export class FakeYouFrontendEnvironment { environment: Environment; private constructor() { - switch (document.location.host) { + const domainWithoutPort = document.location.host.split(":")[0]; + + switch (domainWithoutPort) { case "fakeyou.com": this.environment = Environment.Production; break; case "staging.fakeyou.com": this.environment = Environment.Staging; break; - case "localhost:7000": - this.environment = Environment.Development; - break; - case "localhost:7001": - this.environment = Environment.Development; - break; + case "localhost": case "dev.fakeyou.com": this.environment = Environment.Development; break; diff --git a/packages/fakeyou.com/src/components/layout/SideNav/SideNav.tsx b/packages/fakeyou.com/src/components/layout/SideNav/SideNav.tsx index aec522ed57..bb9f7134fd 100644 --- a/packages/fakeyou.com/src/components/layout/SideNav/SideNav.tsx +++ b/packages/fakeyou.com/src/components/layout/SideNav/SideNav.tsx @@ -9,6 +9,7 @@ import { faSignOutAlt, faFaceViewfinder, faCloudUpload, + faWandMagicSparkles, } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Button from "components/common/Button/Button"; @@ -39,7 +40,7 @@ export default function SideNav(props: SideNavProps) { let history = useHistory(); const [windowWidth, setWindowWidth] = useState(window.innerWidth); const fakeYouFrontendEnv = FakeYouFrontendEnvironment.getInstance(); - + // eslint-disable-next-line @typescript-eslint/no-unused-vars const isDevelopmentEnv = fakeYouFrontendEnv.isDevelopment() const handleNavLinkClick = () => { const wrapper = document.getElementById("wrapper"); @@ -270,7 +271,6 @@ export default function SideNav(props: SideNavProps) { {t("speechVc")} - {isDevelopmentEnv && (
  • {"Voice Designer"}
  • - )}
  • {t("videoTitle")}
  • diff --git a/packages/fakeyou.com/src/hooks/useUploadedFiles/useUploadedFiles.tsx b/packages/fakeyou.com/src/hooks/useUploadedFiles/useUploadedFiles.tsx index 3289abe16a..8851d3aedc 100644 --- a/packages/fakeyou.com/src/hooks/useUploadedFiles/useUploadedFiles.tsx +++ b/packages/fakeyou.com/src/hooks/useUploadedFiles/useUploadedFiles.tsx @@ -10,6 +10,7 @@ const useUploadedFiles = create((set) => ({ setFiles: (files: any[]) => set({ files }), audioLinks: [], setAudioLinks: (audioLinks: string[]) => set({ audioLinks }), + deleteEverything:() => set({ files: [], audioLinks: [] }), })); export default useUploadedFiles; diff --git a/packages/fakeyou.com/src/v2/view/_common/SessionVoiceDesignerInferenceResultsList.tsx b/packages/fakeyou.com/src/v2/view/_common/SessionVoiceDesignerInferenceResultsList.tsx new file mode 100644 index 0000000000..239baf34cb --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/_common/SessionVoiceDesignerInferenceResultsList.tsx @@ -0,0 +1,341 @@ +import React, { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { TtsInferenceJob } from "../../../App"; +import { BucketConfig } from "@storyteller/components/src/api/BucketConfig"; +import { JobState } from "@storyteller/components/src/jobs/JobStates"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faClock, + faLink, + faHeadphonesSimple, +} from "@fortawesome/free-solid-svg-icons"; + +import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; +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 { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob"; +import { useLocalize } from "hooks"; + +interface Props { + inferenceJobs: Array; + ttsInferenceJobs: Array; + sessionSubscriptionsWrapper: SessionSubscriptionsWrapper; +} + +// 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 SessionVoiceDesignerInferenceResultsList(props: Props) { + const { t } = useLocalize("SessionTtsInferenceResultList"); + + 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, + }); + + 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 ============================= + + props.inferenceJobs.forEach((job) => { + if (!job.maybeResultToken) { + let cssStyle = "alert alert-secondary mb-0"; + let stateDescription = "Pending..."; + + switch (job.jobState) { + case JobState.PENDING: + case JobState.UNKNOWN: + stateDescription = + job.maybeExtraStatusDescription == null + ? t("resultsProgressPending") + : job.maybeExtraStatusDescription; + break; + case JobState.STARTED: + cssStyle = "alert alert-success mb-0"; + stateDescription = + job.maybeExtraStatusDescription == null + ? t("resultsProgressStarted") + : job.maybeExtraStatusDescription; + break; + case JobState.ATTEMPT_FAILED: + cssStyle = "alert alert-danger mb-0"; + stateDescription = `${t("resultsProgressFail", { + 0: job.attemptCount || "0", + })}}`; + 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( +
    +
    +
    +
    {stateDescription}
    +
    +
    +
    + ); + } else { + let audioLink = new BucketConfig().getGcsUrl( + job.maybeResultPublicBucketMediaPath + ); + let ttsPermalink = `/media/${job.maybeResultToken}`; + + let wavesurfers = ; + + results.push( +
    + {/*
    +

    {job.title}

    + +
    */} +
    +
    +
    +
    {job.maybeModelTitle}
    +

    {job.maybeRawInferenceText}

    +
    + + {/* */} + + {wavesurfers} + +
    + { + Analytics.ttsClickResultLink(); + }} + className="fw-semibold" + > + + {t("resultsAudioShareDownload")} + +
    +
    +
    +
    + ); + } + }); + + // TODO(bt,2023-04-08): Clean this utter garbage duplication up. + + // ============================= LEGACY TTS ============================= + + props.ttsInferenceJobs.forEach((job) => { + if (!job.maybeResultToken) { + let cssStyle = "alert alert-secondary mb-0"; + let stateDescription = "Pending..."; + + switch (job.jobState) { + case JobState.PENDING: + case JobState.UNKNOWN: + stateDescription = + job.maybeExtraStatusDescription == null + ? t("resultsProgressPending") + : job.maybeExtraStatusDescription; + break; + case JobState.STARTED: + cssStyle = "alert alert-success mb-0"; + stateDescription = + job.maybeExtraStatusDescription == null + ? t("resultsProgressStarted") + : job.maybeExtraStatusDescription; + break; + case JobState.ATTEMPT_FAILED: + cssStyle = "alert alert-danger mb-0"; + stateDescription = `${t("resultsProgressFail", { + 0: job.attemptCount || "0", + })}}`; + 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( +
    +
    +
    +
    {stateDescription}
    +
    +
    +
    + ); + } else { + let audioLink = new BucketConfig().getGcsUrl( + job.maybePublicBucketWavAudioPath + ); + let ttsPermalink = `/tts/result/${job.maybeResultToken}`; + + let wavesurfers = ; + + results.push( +
    + {/*
    +

    {job.title}

    + +
    */} +
    +
    +
    +
    {job.title}
    +

    {job.rawInferenceText}

    +
    + + {/* */} + + {wavesurfers} + +
    + { + Analytics.ttsClickResultLink(); + }} + className="fw-semibold" + > + + {t("resultsAudioShareDownload")} + +
    +
    +
    +
    + ); + } + }); + + // ============================= END LEGACY TTS ============================= + + let noResultsSection = ( +
    +
    + +
    {t("resultsBlankText")}
    +

    {t("resultsBlankSubText")}

    +
    +
    + ); + + if (results.length === 0) { + return <>{noResultsSection}; + } + + let upgradeNotice = <>; + + // Ask non-premium users to upgrade + if ( + results.length !== 0 && + !props.sessionSubscriptionsWrapper.hasPaidFeatures() + ) { + upgradeNotice = ( +
    +
    + + {t("resultsUpgradeNotice")}{" "} + { + Analytics.ttsTooSlowUpgradePremium(); + }} + className="alert-link fw-semibold" + > + {t("resultsUpgradeLinkText")} + {" "} + ({t("resultsTtsQueued")}{" "} + ~{pendingTtsJobs.pending_job_count}) +
    +
    + ); + } + + // Users have requested reverse chronological results + results.reverse(); + + return ( +
    +
    +
    + {upgradeNotice} +
    {results}
    +
    +
    +
    + ); +} + +export { SessionVoiceDesignerInferenceResultsList }; diff --git a/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx b/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx index ac4309b6b4..8ae4922ebc 100644 --- a/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx +++ b/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx @@ -10,6 +10,7 @@ import { faDiscord, faTwitter, faTiktok, + } from "@fortawesome/free-brands-svg-icons"; import { ThirdPartyLinks } from "@storyteller/components/src/constants/ThirdPartyLinks"; import { @@ -213,6 +214,10 @@ function FooterNav(props: Props) { {t("productVc")}
  • +
  • + Voice Designer +
  • +
  • {t("productFaceAnimator")}
  • diff --git a/packages/fakeyou.com/src/v2/view/nav/TopNav.tsx b/packages/fakeyou.com/src/v2/view/nav/TopNav.tsx index f69bb2c789..6312fd0fc9 100644 --- a/packages/fakeyou.com/src/v2/view/nav/TopNav.tsx +++ b/packages/fakeyou.com/src/v2/view/nav/TopNav.tsx @@ -12,6 +12,7 @@ import { faSignOutAlt, faBook, faStar, + faWandMagicSparkles } from "@fortawesome/free-solid-svg-icons"; import { faDiscord } from "@fortawesome/free-brands-svg-icons"; import { Logout } from "@storyteller/components/src/api/session/Logout"; @@ -244,6 +245,15 @@ function TopNav(props: Props) { {t("productVc")} +
  • + + + Voice Designer + +
  • +
    + +
    +
    +
    + + + {t("productNewTag")} + + +

    + Voice Designer +

    +
    + +
    + Make yourself sound like anyone +
    +
    + + + +
    + + Voice Designer + +
    state.deleteEverything); const audioProps = useFile({}); // contains upload inout state and controls, see docs const datasetInputs = [ @@ -72,6 +73,7 @@ function VoiceDesignerFormPage({ enqueueInferenceJob }: { enqueueInferenceJob: a const initialStep = history.location.pathname.includes("/upload") ? 1 : 0; const [currentStep, setCurrentStep] = useState(initialStep); + const [audioSamplesReady, setAudioSamplesReady] = useState(false); const steps = isEditMode ? ["Edit Details", "Edit Samples"] @@ -83,7 +85,7 @@ function VoiceDesignerFormPage({ enqueueInferenceJob }: { enqueueInferenceJob: a return ; case 1: return ( - + ); default: return null; @@ -111,6 +113,7 @@ function VoiceDesignerFormPage({ enqueueInferenceJob }: { enqueueInferenceJob: a idempotency_token: uuidv4(), }).then((res: any) => { if (res && res.success && res.token) { + deleteEverything(); history.push(`/voice-designer/dataset/${ res.token }/upload`); } }); @@ -140,6 +143,7 @@ function VoiceDesignerFormPage({ enqueueInferenceJob }: { enqueueInferenceJob: a voice_dataset_token: dataset_token || "", }) .then((res: any) => { + deleteEverything(); if (res && res.success) { enqueueInferenceJob( res.inference_job_token, @@ -194,6 +198,7 @@ function VoiceDesignerFormPage({ enqueueInferenceJob }: { enqueueInferenceJob: a onBack={handleBack} onNext={handleNext} onCreate={handleCreateVoice} + createDisabled={!audioSamplesReady} /> diff --git a/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerMainPage.tsx b/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerMainPage.tsx index 395fda1c90..007662764d 100644 --- a/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerMainPage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerMainPage.tsx @@ -20,28 +20,43 @@ import { function VoiceDesignerMainPage({ inferenceJobsByCategory }: { inferenceJobsByCategory: any }) { const { pathname } = useLocation(); const { t } = useLocalize("FaceAnimator"); - const { datasets, voices } = useVoiceRequests({ requestDatasets: true, requestVoices: true }); - const [isDeleteVoiceModalOpen, setIsDeleteVoiceModalOpen] = useState(false); - const [isDeleteDatasetModalOpen, setIsDeleteDatasetModalOpen] = - useState(false); + const { datasets, voices } = useVoiceRequests({ + requestDatasets: true, + requestVoices: true, + }); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const view = ["/voice-designer/datasets", "/voice-designer/voices"].indexOf( pathname ); + const [deleteItem, setDeleteItem] = useState(""); + const [deleteType, setDeleteType] = useState(""); + const [deleteText, setDeleteText] = useState({ + title: "", + text: "", + }); - // const openDeleteVoiceModal = () => { - // setIsDeleteVoiceModalOpen(true); - // }; - - const closeDeleteVoiceModal = () => { - setIsDeleteVoiceModalOpen(false); + const openDeleteModal = (token: string, type: string) => { + setDeleteItem(token); + setDeleteType(type); + setDeleteText({ + title: `Delete ${type}`, + text: `Are you sure you want to delete this ${type}?`, + }); + setIsDeleteModalOpen(true); }; - // const openDeleteDatasetModal = () => { - // setIsDeleteDatasetModalOpen(true); - // }; + const handleDelete = () => { + if (deleteType === "voice") { + voices.delete(deleteItem); + } else if (deleteType === "dataset") datasets.delete(deleteItem); + setDeleteItem(""); + setDeleteType(""); + }; - const closeDeleteDatasetModal = () => { - setIsDeleteDatasetModalOpen(false); + const closeDeleteModal = () => { + setIsDeleteModalOpen(false); + setDeleteItem(""); + setDeleteType(""); }; const history = useHistory(); @@ -57,15 +72,16 @@ function VoiceDesignerMainPage({ inferenceJobsByCategory }: { inferenceJobsByCat const voiceClick = (todo: any, type: string) => ({ target }: { target: any }) => { - let voiceToken = voices.list[target.name.split(",")[0].split(":")[1]].voice_token; + let voiceToken = + voices.list[target.name.split(",")[0].split(":")[1]].voice_token; todo(voiceToken, type); }; - const datasetClick = (todo: any, type: string) => ({ target }: { target: any }) => { - let datasetToken = datasets.list[target.name.split(",")[0].split(":")[1]].dataset_token; + let datasetToken = + datasets.list[target.name.split(",")[0].split(":")[1]].dataset_token; todo(datasetToken, type); }; @@ -84,7 +100,7 @@ function VoiceDesignerMainPage({ inferenceJobsByCategory }: { inferenceJobsByCat label: "Delete", small: true, variant: "danger", - onClick: datasetClick(datasets.delete, "dataset"), + onClick: datasetClick(openDeleteModal, "dataset"), }, ], name: dataset.title, @@ -106,7 +122,7 @@ function VoiceDesignerMainPage({ inferenceJobsByCategory }: { inferenceJobsByCat label: "Delete", small: true, variant: "danger", - onClick: voiceClick(voices.delete, "voice"), + onClick: voiceClick(openDeleteModal, "voice"), }, { label: "Use Voice", @@ -181,31 +197,13 @@ function VoiceDesignerMainPage({ inferenceJobsByCategory }: { inferenceJobsByCat - {/* Delete Voice Modal */} + {/* Delete Modal */} - Are you sure you want to delete "[Voice Name]"? -
    - This cannot be undone. -

    - } - /> - {/* Delete Dataset Modal */} - - Are you sure you want to delete "[Dataset Name]"? -
    - This cannot be undone. -

    - } + show={isDeleteModalOpen} + handleClose={closeDeleteModal} + title={deleteText.title} + content={

    {deleteText.text}

    } + onConfirm={handleDelete} /> ); diff --git a/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerUseVoicePage.tsx b/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerUseVoicePage.tsx index 9d8bec8723..f66555a637 100644 --- a/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerUseVoicePage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/voice_designer/VoiceDesignerUseVoicePage.tsx @@ -19,7 +19,7 @@ import { SessionWrapper } from "@storyteller/components/src/session/SessionWrapp import Container from "components/common/Container/Container"; import TextArea from "components/common/TextArea"; import { Button } from "components/common"; -import { SessionTtsInferenceResultList } from "v2/view/_common/SessionTtsInferenceResultsList"; +import { SessionVoiceDesignerInferenceResultsList } from "v2/view/_common/SessionVoiceDesignerInferenceResultsList"; import { FrontendInferenceJobType, InferenceJob, @@ -230,7 +230,7 @@ export default function VoiceDesignerUseVoicePage( Session TTS Results
    - void; onNext: () => void; onCreate: () => void; + createDisabled: boolean; steps: string[]; currentStep: number; } @@ -14,6 +15,7 @@ const StepperControls: React.FC = ({ steps, currentStep, onCreate, + createDisabled }) => { return (
    @@ -30,7 +32,7 @@ const StepperControls: React.FC = ({ )} {currentStep === 1 && ( - )} diff --git a/packages/fakeyou.com/src/v2/view/pages/voice_designer/components/VoiceDesignerUploadComponent.tsx b/packages/fakeyou.com/src/v2/view/pages/voice_designer/components/VoiceDesignerUploadComponent.tsx index bffe393ecb..2d5a2e175c 100644 --- a/packages/fakeyou.com/src/v2/view/pages/voice_designer/components/VoiceDesignerUploadComponent.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/voice_designer/components/VoiceDesignerUploadComponent.tsx @@ -27,10 +27,11 @@ interface Props { formIsCleared: boolean; setFormIsCleared: (cleared: boolean) => void; setCanConvert: (canConvert: boolean) => void; + setAudioSamplesReady: (ready: boolean) => void; changeConvertIdempotencyToken: () => void; } -function VoiceDesignerUploadComponent({ changeConvertIdempotencyToken, datasetToken, setCanConvert, setFormIsCleared }: Props) { +function VoiceDesignerUploadComponent({ changeConvertIdempotencyToken, datasetToken, setCanConvert, setFormIsCleared, setAudioSamplesReady }: Props) { const [isUploadDisabled, setIsUploadDisabled] = useState(false); const files = useUploadedFiles((state: any) => state.files); @@ -38,6 +39,7 @@ function VoiceDesignerUploadComponent({ changeConvertIdempotencyToken, datasetTo const audioLinks = useUploadedFiles((state: any) => state.audioLinks); const setAudioLinks = useUploadedFiles((state: any) => state.setAudioLinks); + // Auto generated // eslint-disable-next-line @typescript-eslint/no-unused-vars // const [idempotencyToken, setIdempotencyToken] = useState(uuidv4()); @@ -53,7 +55,9 @@ function VoiceDesignerUploadComponent({ changeConvertIdempotencyToken, datasetTo file: file, uuid_idempotency_token: uuidv4(), }) - .then(res => {}); + .then(res => { + setAudioSamplesReady(true); + }); setFormIsCleared(false); setCanConvert(false); @@ -100,6 +104,7 @@ function VoiceDesignerUploadComponent({ changeConvertIdempotencyToken, datasetTo
    {/* Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files */}
    - +