diff --git a/_redirects b/_redirects index 3fc6f640fa..f137d71658 100644 --- a/_redirects +++ b/_redirects @@ -10,6 +10,7 @@ /coming-soon* /index.html 200 /commissions* /index.html 200 /contribute* /index.html 200 +/face-animation* /index.html 200 /firehose* /index.html 200 /guide* /index.html 200 /jobs* /index.html 200 diff --git a/packages/components/src/api/stats/queues/GetQueueStats.ts b/packages/components/src/api/stats/queues/GetQueueStats.ts index 6181ffb81c..a6d5847d30 100644 --- a/packages/components/src/api/stats/queues/GetQueueStats.ts +++ b/packages/components/src/api/stats/queues/GetQueueStats.ts @@ -17,6 +17,7 @@ export interface InferenceInfo { export interface ByQueueStats { pending_svc_jobs: number, pending_rvc_jobs: number, + pending_face_animation_jobs: number, } export interface LegacyTtsInfo { diff --git a/packages/fakeyou.com/src/scss/site-refresh.scss b/packages/fakeyou.com/src/scss/site-refresh.scss index 65f8940ed3..409986a865 100644 --- a/packages/fakeyou.com/src/scss/site-refresh.scss +++ b/packages/fakeyou.com/src/scss/site-refresh.scss @@ -689,6 +689,18 @@ styles with important tags: } } + .badge-new { + font-weight: 600; + font-size: 12px; + padding: 2px 6px; + border: none; + border-radius: 0.3rem; + opacity: 0.85; + max-height: 18px; + background-color: #fff3d0; + color: #833c00; + } + .rounded-circle { border-radius: 50px; } diff --git a/packages/fakeyou.com/src/v2/view/PageContainer.tsx b/packages/fakeyou.com/src/v2/view/PageContainer.tsx index 5f5c50c7bf..0d43d2b8ed 100644 --- a/packages/fakeyou.com/src/v2/view/PageContainer.tsx +++ b/packages/fakeyou.com/src/v2/view/PageContainer.tsx @@ -80,6 +80,7 @@ import { VoiceConversionModelListItem } from "@storyteller/components/src/api/vo import { CommunityCommissionsPage } from "./pages/contest/CommunityCommissionsPage"; import { ProductUsageInfoPage } from "./pages/product_usage_info/ProductUsageInfoPage"; import { TestingPage } from "./pages/testing/TestingPage"; +import { FaceAnimationPage } from "./pages/face-animation/FaceAnimationPage"; interface Props { sessionWrapper: SessionWrapper; @@ -542,6 +543,17 @@ class PageContainer extends React.Component { /> + + + + diff --git a/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx b/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx index ead95d3111..2ecb293a54 100644 --- a/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx +++ b/packages/fakeyou.com/src/v2/view/nav/FooterNav.tsx @@ -57,7 +57,8 @@ function FooterNav(props: Props) { by_queue: { pending_svc_jobs: 0, pending_rvc_jobs: 0, - }, + pending_face_animation_jobs: 0, + } }, legacy_tts: { pending_job_count: 0, @@ -145,6 +146,12 @@ function FooterNav(props: Props) { {queueStats.inference.by_queue.pending_svc_jobs} +
+ Animations Queued:{" "} + + {queueStats.inference.by_queue.pending_face_animation_jobs} + +
diff --git a/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationPage.tsx b/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationPage.tsx new file mode 100644 index 0000000000..5587e6bb3f --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationPage.tsx @@ -0,0 +1,151 @@ +import React, { useState } from "react"; + +import { usePrefixedDocumentTitle } from "../../../../common/UsePrefixedDocumentTitle"; +import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient"; +import { + EnqueueFaceAnimation, + EnqueueFaceAnimationIsSuccess, + EnqueueFaceAnimationRequest, +} from "@storyteller/components/src/api/face_animation/EnqueueFaceAnimation"; +import { v4 as uuidv4 } from "uuid"; +import { FrontendInferenceJobType, InferenceJob } from "@storyteller/components/src/jobs/InferenceJob"; +import FaceAnimationUploadComponent from "./FaceAnimationUploadComponent"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faBarsStaggered } from "@fortawesome/free-solid-svg-icons"; +import { FaceAnimationPageResultList } from "./FaceAnimationPageResultList"; +import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; + +interface Props { + sessionSubscriptionsWrapper: SessionSubscriptionsWrapper; + enqueueInferenceJob: ( + jobToken: string, + frontendInferenceJobType: FrontendInferenceJobType + ) => void; + inferenceJobs: Array; + inferenceJobsByCategory: Map>; +} + +function FaceAnimationPage(props: Props) { + PosthogClient.recordPageview(); + usePrefixedDocumentTitle("Face Animation"); + + const [audioToken, setAudioToken] = useState(undefined); + const [imageToken, setImageToken] = useState(undefined); + + const doTest = async () => { + let request: EnqueueFaceAnimationRequest = { + uuid_idempotency_token: uuidv4(), + audio_source: { + //maybe_voice_conversion_result_token: "todo", + //maybe_media_file_token: "audio_media_token", + maybe_media_upload_token: audioToken, + }, + image_source: { + //maybe_media_file_token: "image_media_token", + maybe_media_upload_token: imageToken, + }, + }; + + let result = await EnqueueFaceAnimation(request); + + if (EnqueueFaceAnimationIsSuccess(result)) { + props.enqueueInferenceJob( + result.inference_job_token, + FrontendInferenceJobType.FaceAnimation + ); + } + }; + + const generateButtonDisabled = !!!audioToken || !!!imageToken; + const generateButtonClass = generateButtonDisabled + ? "btn btn-uploaded w-100 disabled" + : "btn btn-primary w-100"; + + return ( +
+
+

Face Animation (Alpha)

+ +
+
+ +

Audio

+ { setAudioToken(token) }} + formIsCleared={false} + setFormIsCleared={() => {}} + setCanConvert={() => {}} + changeConvertIdempotencyToken={() => {}} + /> + + {audioToken ? ( + <> +
+
Audio upload token: {audioToken}
+ + ) : (<>)} + +
+
+ +

Image

+ { setImageToken(token) }} + formIsCleared={false} + setFormIsCleared={() => {}} + setCanConvert={() => {}} + changeConvertIdempotencyToken={() => {}} + /> + + {imageToken ? ( + <> +
+
Image upload token: {imageToken}
+ + ) : (<>)} + +
+
+ +

Generate Result

+ + +
+
+ +

+ + Session Results +

+
+ +
+ + +
+
+ ); +} + +export { FaceAnimationPage }; diff --git a/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationPageResultList.tsx b/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationPageResultList.tsx new file mode 100644 index 0000000000..ea560a16db --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationPageResultList.tsx @@ -0,0 +1,215 @@ +import React, { useEffect, useState } from "react"; +import { t } from "i18next"; +import { BucketConfig } from "@storyteller/components/src/api/BucketConfig"; +import { JobState } from "@storyteller/components/src/jobs/JobStates"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faHeadphonesSimple, + faDownload, +} from "@fortawesome/free-solid-svg-icons"; + +import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper"; +import { Analytics } from "../../../../common/Analytics"; +import { + GetPendingTtsJobCount, + GetPendingTtsJobCountIsOk, + GetPendingTtsJobCountSuccessResponse, +} from "@storyteller/components/src/api/tts/GetPendingTtsJobCount"; +import { InferenceJob } from "@storyteller/components/src/jobs/InferenceJob"; + +interface Props { + inferenceJobs: 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 FaceAnimationPageResultList(props: Props) { + 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("common.SessionTtsInferenceResults.progress.pending") + : job.maybeExtraStatusDescription; + break; + case JobState.STARTED: + cssStyle = "alert alert-success mb-0"; + stateDescription = + job.maybeExtraStatusDescription == null + ? t("common.SessionTtsInferenceResults.progress.started") + : job.maybeExtraStatusDescription; + break; + case JobState.ATTEMPT_FAILED: + cssStyle = "alert alert-danger mb-0"; + stateDescription = `Failed ${job.attemptCount} attempt(s). Will retry...`; + 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( + "common.SessionTtsInferenceResults.progress.dead" + ); + break; + case JobState.COMPLETE_SUCCESS: + cssStyle = "message is-success mb-0"; + // Not sure why we're here instead of other branch! + stateDescription = t( + "common.SessionTtsInferenceResults.progress.success" + ); + break; + } + + results.push( +
+
+
+
{stateDescription}
+
+
+
+ ); + } else { + let audioLink = new BucketConfig().getGcsUrl( + job.maybeResultPublicBucketMediaPath + ); + //let ttsPermalink = `/tts/result/${job.maybeResultToken}`; + + //let wavesurfers = ; + let wavesurfers : string[] = []; + + let audioDownloadFilename = `fakeyou-${job.jobToken}.wav`; + + results.push( +
+ {/*
+

{job.title}

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

{job.maybeRawInferenceText}

+
+ + {/* */} + + {wavesurfers} + +
+ {/* { + Analytics.ttsClickResultLink(); + }} + className="fw-semibold" + > + + {t("common.SessionTtsInferenceResults.result.shareDownload")} + */} + + { + Analytics.voiceConversionClickDownload(); + }} + download={audioDownloadFilename} + > + + Download File{" "} + +
+
+
+
+ ); + } + }); + + let noResultsSection = ( +
+
+ +
+ {t("common.SessionTtsInferenceResults.noResults.title")} +
+

{t("common.SessionTtsInferenceResults.noResults.subtitle")}

+
+
+ ); + + if (results.length === 0) { + return <>{noResultsSection}; + } + + // Users have requested reverse chronological results + results.reverse(); + + return ( +
+
+
+
{results}
+
+
+
+ ); +} + +export { FaceAnimationPageResultList }; diff --git a/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationUploadComponent.tsx b/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationUploadComponent.tsx new file mode 100644 index 0000000000..9304d7528c --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/pages/face-animation/FaceAnimationUploadComponent.tsx @@ -0,0 +1,246 @@ +import { + faCheck, + faFileArrowUp, + faFileAudio, + faTrash, +} from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import React, { useState } from "react"; +import { FileUploader } from "react-drag-drop-files"; +import { v4 as uuidv4 } from "uuid"; +import { + UploadAudio, + UploadAudioIsOk, + UploadAudioRequest, +} from "@storyteller/components/src/api/upload/UploadAudio"; +import { + UploadImage, + UploadImageIsOk, + UploadImageRequest, +} from "@storyteller/components/src/api/upload/UploadImage"; + +interface Props { + uploadTypeLabel: string, + uploadTypesAllowed: string[], + + setMediaUploadToken: (token?: string) => void; + + formIsCleared: boolean; + setFormIsCleared: (cleared: boolean) => void; + + setCanConvert: (canConvert: boolean) => void; + changeConvertIdempotencyToken: () => void; +} + +function FaceAnimationUploadComponent(props: Props) { + const [file, setFile] = useState(undefined); + const [isUploadDisabled, setIsUploadDisabled] = useState(false); + const [uploadLoading, setUploadLoading] = useState(false); + + // Auto generated + const [idempotencyToken, setIdempotencyToken] = useState(uuidv4()); + + const handleChange = (file: any) => { + console.log("handle change"); + setFile(file); + setIdempotencyToken(uuidv4()); + props.setFormIsCleared(false); + props.setCanConvert(false); + props.changeConvertIdempotencyToken(); + setIsUploadDisabled(false); + }; + + const handleDragOver = (e: React.DragEvent): void => { + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.classList.add("upload-zone-drag"); + }; + + const handleDragLeave = (e: React.DragEvent): void => { + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.classList.remove("upload-zone-drag"); + }; + + const handleClear = () => { + setFile(null); + setIdempotencyToken(uuidv4()); + setIsUploadDisabled(false); + props.setMediaUploadToken(undefined); // clear + props.setFormIsCleared(true); + props.setCanConvert(false); + props.changeConvertIdempotencyToken(); + }; + + const handleUploadFile = async () => { + if (file === undefined) { + return false; + } + + setUploadLoading(true); + + // TODO/FIXME: Horrible way to control upload type. + switch (props.uploadTypeLabel.toLocaleLowerCase()) { + case "audio": + uploadAudio(); + break; + case "image": + uploadImage(); + break; + } + + + setUploadLoading(false); + }; + + const uploadAudio = async () => { + const request: UploadAudioRequest = { + uuid_idempotency_token: idempotencyToken, + file: file, + source: "file", + }; + + let result = await UploadAudio(request); + + if (UploadAudioIsOk(result)) { + setIsUploadDisabled(true); + props.setMediaUploadToken(result.upload_token); + props.setFormIsCleared(false); + props.setCanConvert(true); + } + } + + const uploadImage = async () => { + const request: UploadImageRequest = { + uuid_idempotency_token: idempotencyToken, + file: file, + source: "file", + }; + + let result = await UploadImage(request); + + if (UploadImageIsOk(result)) { + setIsUploadDisabled(true); + props.setMediaUploadToken(result.upload_token); + props.setFormIsCleared(false); + props.setCanConvert(true); + } + } + + const fileSize = + file && file.size >= 1024 * 1024 + ? (file.size / 1024 / 1024).toFixed(2) + " MB" + : file + ? `${Math.floor(file.size / 1024)} KB` + : null; + + const uploadBtnClass = isUploadDisabled + ? "btn btn-uploaded w-100 disabled" + : "btn btn-primary w-100"; + + return ( +
+ {/* Usage refer to https://github.com/KarimMokhtar/react-drag-drop-files */} + +
+ {file ? ( + + ) : ( + + )} +
+
+
+ {file ? ( + + {file.name.slice(0, file.name.lastIndexOf("."))} + + ) : ( + <> + Upload {props.uploadTypeLabel} file or drop it + here... + + )} +
+
+
+ {file ? ( +

+ + {file && `${file.name.split(".").pop().toUpperCase()}`}{" "} + file size: {fileSize} + {" "} + Change file +

+ ) : ( +

+ {props.uploadTypesAllowed.join(", ").toString()} supported +

+ )} +
+
+
+
+ } + /> + + {file ? ( +
+ + + +
+ ) : ( + <> + )} +
+ ); +} + +const LoadingIcon: React.FC = () => { + return ( + <> + + Loading... + + ); +}; + +export default FaceAnimationUploadComponent; diff --git a/packages/fakeyou.com/src/v2/view/pages/landing/LandingPage.tsx b/packages/fakeyou.com/src/v2/view/pages/landing/LandingPage.tsx index c17b448044..8caf0e9113 100644 --- a/packages/fakeyou.com/src/v2/view/pages/landing/LandingPage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/landing/LandingPage.tsx @@ -11,7 +11,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { faDiscord, faTwitter } from "@fortawesome/free-brands-svg-icons"; import { WebUrl } from "../../../../common/WebUrl"; -import { faFileArrowUp } from "@fortawesome/pro-solid-svg-icons"; +import { faFileArrowUp, faSparkles } from "@fortawesome/pro-solid-svg-icons"; import { PosthogClient } from "@storyteller/components/src/analytics/PosthogClient"; // import { // faFileArrowUp, @@ -250,7 +250,7 @@ function LandingPage(props: Props) { to="/tts" className="panel panel-select d-flex flex-column align-items-center" > -
+

{t("productTtsTitle")} @@ -259,7 +259,7 @@ function LandingPage(props: Props) { {t("productTtsText")}

- +
@@ -276,7 +276,7 @@ function LandingPage(props: Props) { to="/voice-conversion" className="panel panel-select d-flex flex-column align-items-center" > -
+

{t("productVcTitle")} @@ -285,7 +285,7 @@ function LandingPage(props: Props) { {t("productVcText")}

- +
@@ -299,19 +299,23 @@ function LandingPage(props: Props) {
-
+
-

- {t("productVideoTitle")} -

+

+ + + NEW + + Face Animation +

- {t("productVideoText")} + Create videos from audio
- +
@@ -323,6 +327,24 @@ function LandingPage(props: Props) { />
+
+ +
+
+

{t("productVideoTitle")}

+
+ {t("productVideoText")} +
+
+ + + +
+ +