diff --git a/src/website/packages/components/src/api/media_files/GetMedia.ts b/src/website/packages/components/src/api/media_files/GetMedia.ts
index d267ed1828..920b8694c6 100644
--- a/src/website/packages/components/src/api/media_files/GetMedia.ts
+++ b/src/website/packages/components/src/api/media_files/GetMedia.ts
@@ -46,6 +46,7 @@ export interface MediaFile {
maybe_moderator_fields?: MediaFileModeratorFields;
created_at: Date;
updated_at: Date;
+ maybe_text_transcript: string | undefined;
}
export interface MediaFileModeratorFields {
diff --git a/src/website/packages/components/src/api/workflows/EnqueueLipsync.ts b/src/website/packages/components/src/api/workflows/EnqueueLipsync.ts
new file mode 100644
index 0000000000..0055621208
--- /dev/null
+++ b/src/website/packages/components/src/api/workflows/EnqueueLipsync.ts
@@ -0,0 +1,31 @@
+import MakeRequest from "../MakeRequest";
+
+export interface MediaFileCropArea {
+ height: number;
+ width: number;
+ x: number;
+ y: number;
+}
+export interface EnqueueLipsyncRequest {
+ creator_set_visibility: "public" | "private";
+ audio_media_file_token: string;
+ image_or_video_media_file_token: string;
+ maybe_crop?: MediaFileCropArea;
+ remove_watermark: boolean;
+ uuid_idempotency_token: string;
+}
+
+export interface EnqueueLipsyncResponse {
+ inference_job_token?: string;
+ success: boolean;
+}
+
+export const EnqueueLipsync = MakeRequest<
+ string,
+ EnqueueLipsyncRequest,
+ EnqueueLipsyncResponse,
+ {}
+>({
+ method: "POST",
+ routingFunction: () => `/v1/workflows/enqueue_lipsync`,
+});
diff --git a/src/website/packages/components/src/jobs/InferenceJob.ts b/src/website/packages/components/src/jobs/InferenceJob.ts
index f61359ba17..533cbc8340 100644
--- a/src/website/packages/components/src/jobs/InferenceJob.ts
+++ b/src/website/packages/components/src/jobs/InferenceJob.ts
@@ -19,6 +19,7 @@ export enum FrontendInferenceJobType {
VideoWorkflow, // workflow ?
VideoStyleTransfer, // video_filter
LivePortrait, // live_portrait
+ Lipsync, // lip_sync
}
export enum AllInferenceJobs {
diff --git a/src/website/packages/fakeyou.com/src/components/common/TextArea/TextArea.tsx b/src/website/packages/fakeyou.com/src/components/common/TextArea/TextArea.tsx
index 8d11021de7..d882aa5eb0 100644
--- a/src/website/packages/fakeyou.com/src/components/common/TextArea/TextArea.tsx
+++ b/src/website/packages/fakeyou.com/src/components/common/TextArea/TextArea.tsx
@@ -7,6 +7,7 @@ interface TextAreaProps
textArea?: boolean;
required?: boolean;
resize?: boolean;
+ className?: string;
}
export default function TextArea({
@@ -14,17 +15,20 @@ export default function TextArea({
textArea,
required,
resize = true,
+ className,
...rest
}: TextAreaProps) {
return (
-
+
{label && (
)}
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 83a9913daa..2bb30ec12f 100644
--- a/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx
+++ b/src/website/packages/fakeyou.com/src/v2/view/PageContainer.tsx
@@ -142,6 +142,7 @@ import { BetaWebcamActingForm } from "./pages/beta_products/BetaWebcamActingForm
import { BetaWebcamActingPage } from "./pages/beta_products/BetaWebcamActingPage";
import { Beta3DVideoCompositorForm } from "./pages/beta_products/Beta3DVideoCompositorForm";
import { Beta3DVideoCompositorPage } from "./pages/beta_products/Beta3DVideoCompositorPage";
+import Lipsync from "./pages/lipsync/Lipsync";
interface Props {
sessionWrapper: SessionWrapper;
@@ -763,6 +764,14 @@ class PageContainer extends React.Component<
/>
+
+
+
+
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/GenerateTts.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/GenerateTts.tsx
new file mode 100644
index 0000000000..98f580228a
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/GenerateTts.tsx
@@ -0,0 +1,306 @@
+import React, { useEffect, useState } from "react";
+import { v4 as uuidv4 } from "uuid";
+import {
+ faPause,
+ faPlay,
+ faSquareQuote,
+ faWaveformLines,
+ faXmark,
+} from "@fortawesome/pro-solid-svg-icons";
+import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
+import {
+ GenerateTtsAudio,
+ GenerateTtsAudioErrorType,
+ GenerateTtsAudioIsError,
+ GenerateTtsAudioIsOk,
+} from "@storyteller/components/src/api/tts/GenerateTtsAudio";
+import {
+ FrontendInferenceJobType,
+ InferenceJob,
+} from "@storyteller/components/src/jobs/InferenceJob";
+import { Button, Panel, TextArea } from "components/common";
+import { useInferenceJobs } from "hooks";
+import LipsyncAudioPlayer from "./LipsyncAudioPlayer";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
+
+interface GenerateTtsProps {
+ weightToken?: string | null;
+ onResultToken?: (token: string | null) => void;
+}
+
+export const GenerateTts = ({
+ weightToken,
+ onResultToken,
+}: GenerateTtsProps) => {
+ const [textBuffer, setTextBuffer] = useState("");
+ const [maybeTtsError, setMaybeTtsError] = useState<
+ GenerateTtsAudioErrorType | undefined
+ >(undefined);
+ const [currentAudioUrl, setCurrentAudioUrl] = useState
(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [jobToken, setJobToken] = useState(null);
+ const [isAudioLoading, setIsAudioLoading] = useState(false);
+ const [voiceToken] = useState(
+ weightToken || "weight_hz7g8f1j4psrsw2sv67e4y61q"
+ );
+ const [progress, setProgress] = useState(0);
+
+ const { enqueueInferenceJob, inferenceJobs } = useInferenceJobs();
+
+ const [transcript, setTranscript] = useState(null);
+
+ const handleChangeText = (ev: React.FormEvent) => {
+ const textValue = (ev.target as HTMLTextAreaElement).value;
+ setTextBuffer(textValue);
+ };
+
+ const handleEnqueueTts = async (ev: React.FormEvent) => {
+ ev.preventDefault();
+
+ if (!textBuffer) {
+ return false;
+ }
+
+ // Check if the text hasn't changed and the voice hasn't changed
+
+ setIsAudioLoading(true);
+
+ const modelToken = voiceToken;
+
+ const request = {
+ uuid_idempotency_token: uuidv4(),
+ tts_model_token: modelToken,
+ inference_text: textBuffer,
+ };
+
+ const response = await GenerateTtsAudio(request);
+
+ if (GenerateTtsAudioIsOk(response)) {
+ setMaybeTtsError(undefined);
+
+ enqueueInferenceJob(
+ response.inference_job_token,
+ FrontendInferenceJobType.TextToSpeech
+ );
+ setJobToken(response.inference_job_token);
+ setTranscript(textBuffer);
+ } else if (GenerateTtsAudioIsError(response)) {
+ setMaybeTtsError(response.error);
+ }
+
+ return false;
+ };
+
+ useEffect(() => {
+ if (!jobToken) return;
+
+ const fetch = async () => {
+ const job = inferenceJobs.find(
+ (job: InferenceJob) => job.jobToken === jobToken
+ );
+
+ if (job && job.progressPercentage) {
+ setProgress(job.progressPercentage);
+ }
+
+ if (job && job.maybeResultToken) {
+ const url = new URL(window.location.href);
+ url.searchParams.set("audio", job.maybeResultToken);
+ window.history.replaceState({}, "", url.toString());
+
+ if (onResultToken) {
+ onResultToken(job.maybeResultToken);
+ }
+ }
+
+ if (job && job.maybeResultPublicBucketMediaPath) {
+ const audioLink = new BucketConfig().getGcsUrl(
+ job.maybeResultPublicBucketMediaPath
+ );
+ setCurrentAudioUrl(audioLink);
+ setProgress(0);
+
+ if (audioLink !== currentAudioUrl) {
+ setIsAudioLoading(false);
+ setIsPlaying(true);
+ }
+ }
+ };
+
+ fetch();
+ }, [currentAudioUrl, jobToken, inferenceJobs, onResultToken]);
+
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const maybeResultToken = urlParams.get("audio");
+
+ if (maybeResultToken) {
+ const fetchMedia = async () => {
+ try {
+ const response = await GetMedia(maybeResultToken, {});
+ if (
+ response &&
+ response.media_file &&
+ response.media_file.public_bucket_path &&
+ response.media_file.maybe_text_transcript
+ ) {
+ const audioLink = new BucketConfig().getGcsUrl(
+ response.media_file.public_bucket_path
+ );
+ setCurrentAudioUrl(audioLink);
+ setTranscript(response.media_file.maybe_text_transcript || "");
+ } else {
+ console.error(
+ "Failed to retrieve media or media has no public bucket path",
+ response
+ );
+ }
+ } catch (error) {
+ console.error("Error fetching media:", error);
+ }
+ };
+
+ fetchMedia();
+ }
+ }, []);
+
+ const handleAudioFinish = () => {
+ setIsPlaying(false);
+ };
+
+ const handleClearAudio = () => {
+ setCurrentAudioUrl(null);
+ setJobToken(null);
+ const url = new URL(window.location.href);
+ url.searchParams.delete("audio");
+ window.history.replaceState({}, "", url.toString());
+
+ if (onResultToken) {
+ onResultToken(null);
+ }
+ };
+
+ let maybeError = <>>;
+ if (!!maybeTtsError) {
+ let hasMessage = false;
+ let message = <>>;
+ switch (maybeTtsError) {
+ case GenerateTtsAudioErrorType.TooManyRequests:
+ hasMessage = true;
+ message = (
+ <>Too many requests! Please wait a few minutes then try again.>
+ );
+ break;
+ case GenerateTtsAudioErrorType.ServerError |
+ GenerateTtsAudioErrorType.BadRequest |
+ GenerateTtsAudioErrorType.NotFound:
+ break;
+ }
+
+ if (hasMessage) {
+ maybeError = (
+
+
+ {message}
+
+ );
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+ What do you want your character to say?
+
+
+
+
+
+ {currentAudioUrl ? (
+
+
+
+
+
+
+
+ Audio Transcript
+
+
{transcript}
+
+
+
+
+
+
+ ) : (
+ <>
+
+ {maybeError}
+
+ >
+ )}
+
+
+ >
+ );
+};
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/Lipsync.scss b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/Lipsync.scss
new file mode 100644
index 0000000000..4782acc793
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/Lipsync.scss
@@ -0,0 +1,28 @@
+@import "scss/colours.scss";
+
+.ls-remove-audio-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ background-color: $action;
+ border: 2.5px solid;
+ border-color: $action;
+ transition: 0.2s;
+ color: $white !important;
+ &:hover {
+ background-color: lighten($action, 5%);
+ border-color: lighten($action, 5%);
+ color: $white !important;
+ }
+ border-radius: 0.5rem;
+}
+
+.ls-section-between {
+ padding-top: 16%;
+
+ @media (max-width: 992px) {
+ padding-top: 0;
+ }
+}
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/Lipsync.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/Lipsync.tsx
new file mode 100644
index 0000000000..bec25a3c1c
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/Lipsync.tsx
@@ -0,0 +1,998 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+ Badge,
+ Button,
+ Checkbox,
+ Container,
+ Label,
+ Panel,
+} from "components/common";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ faArrowDown,
+ faArrowDownToLine,
+ faEquals,
+ faLips,
+ faLock,
+ faMicrophone,
+ faPlus,
+ faSparkles,
+ faWaveformLines,
+} from "@fortawesome/pro-solid-svg-icons";
+import "../live_portrait/LivePortrait.scss";
+import "./Lipsync.scss";
+import {
+ EnqueueLipsync,
+ EnqueueLipsyncResponse,
+ MediaFileCropArea,
+} from "@storyteller/components/src/api/workflows/EnqueueLipsync";
+import Tippy from "@tippyjs/react";
+import "tippy.js/dist/tippy.css";
+import { v4 as uuidv4 } from "uuid";
+import { useInferenceJobs, useSession } from "hooks";
+import {
+ FrontendInferenceJobType,
+ InferenceJob,
+} from "@storyteller/components/src/jobs/InferenceJob";
+import { SessionSubscriptionsWrapper } from "@storyteller/components/src/session/SessionSubscriptionsWrapper";
+import { AITools } from "components/marketing";
+import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
+import LoadingSpinner from "components/common/LoadingSpinner";
+import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
+import { useLocation } from "react-router-dom";
+import { LivePortraitDetails } from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
+import { useDocumentTitle } from "@storyteller/components/src/hooks/UseDocumentTitle";
+import { useHistory } from "react-router-dom";
+import { JobState } from "@storyteller/components/src/jobs/JobStates";
+import PremiumLock from "components/PremiumLock";
+import OutputThumbnailImage from "../live_portrait/OutputThumbnailImage";
+import SessionLsInferenceResultsList from "./SessionLsInferenceResultsList";
+import ThumbnailMediaPicker from "../live_portrait/ThumbnailMediaPicker";
+import { GenerateTts } from "./GenerateTts";
+import { GetWeight } from "@storyteller/components/src/api/weights/GetWeight";
+import { LipsyncTokenMap } from "./LipsyncTokens";
+
+interface LivePortraitProps {
+ sessionSubscriptionsWrapper: SessionSubscriptionsWrapper;
+}
+
+interface GeneratedVideo {
+ sourceIndex: number;
+ motionIndex: number;
+ sourceToken: string;
+ motionToken: string;
+ videoSrc: string;
+ jobToken: string;
+ createdAt: Date;
+}
+
+interface CurrentlyGenerating {
+ sourceIndex: number;
+ motionIndex: number;
+ jobState?: JobState;
+}
+
+interface JobProgress {
+ [key: string]: number | null;
+}
+
+const PRECOMPUTED_SOURCE_TOKENS: string[] = [
+ "m_2xrse9799wvy8hkv8tbxqxct8089t7", // Mona Lisa
+];
+
+export default function LivePortrait({
+ sessionSubscriptionsWrapper,
+}: LivePortraitProps) {
+ useDocumentTitle("Lip Sync AI. Free Video Animation");
+ const { enqueueInferenceJob } = useInferenceJobs();
+ const { loggedIn, sessionFetched } = useSession();
+ // const { open, close } = useModal();
+ const [isEnqueuing, setIsEnqueuing] = useState(false);
+ const [selectedSourceIndex, setSelectedSourceIndex] = useState(0);
+ const [selectedMotionIndex, setSelectedMotionIndex] = useState(0);
+ const [removeWatermark, setRemoveWatermark] = useState(false);
+ const [visibility, setVisibility] = useState<"private" | "public">("public");
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [cropArea, setCropArea] = useState({
+ height: 0,
+ width: 0,
+ x: 0,
+ y: 0,
+ });
+ const [voiceModelTitle, setVoiceModelTitle] = useState(null);
+ const [generatedVideoSrc, setGeneratedVideoSrc] = useState("");
+ const [sourceTokens, setSourceTokens] = useState([
+ ...PRECOMPUTED_SOURCE_TOKENS,
+ ]);
+ const [audioTokens, setAudioTokens] = useState([]);
+ const numberOfInitialSourceTokensRef = useRef(sourceTokens.length);
+ const numberOfInitialSourceTokens = numberOfInitialSourceTokensRef.current;
+ const numberOfInitialAudioTokensRef = useRef(audioTokens.length);
+ const numberOfInitialAudioTokens = numberOfInitialAudioTokensRef.current;
+ const [userSourceToken, setUserSourceToken] = useState("");
+ const [userMotionToken, setUserMotionToken] = useState("");
+ const [isUserUploaded, setIsUserUploaded] = useState(false);
+ const [isGenerating, setIsGenerating] = useState(false);
+ const [generatedVideos, setGeneratedVideos] = useState([]);
+ const [jobProcessedTokens, setJobProcessedTokens] = useState([]);
+ const [currentlyGeneratingList, setCurrentlyGeneratingList] = useState<
+ CurrentlyGenerating[]
+ >([]);
+ const [jobProgress, setJobProgress] = useState({});
+ const [currentCombinationKey, setCurrentCombinationKey] = useState("");
+ const getCombinationKey = (sourceIndex: number, motionIndex: number) =>
+ `s${sourceIndex}_m${motionIndex}`;
+ const [voiceToken, setVoiceToken] = useState(null);
+ const [audioToken, setAudioToken] = useState(null);
+ const location = useLocation();
+ const history = useHistory();
+
+ const handleAudioResultToken = (token: string | null) => {
+ setAudioToken(token);
+ };
+
+ const handleJobClick = (job: InferenceJob) => {
+ const livePortraitDetails = job.maybeLivePortraitDetails;
+ if (livePortraitDetails) {
+ const { source_media_file_token, face_driver_media_file_token } =
+ livePortraitDetails;
+
+ const sourceIndex = sourceTokens.indexOf(source_media_file_token);
+ const motionIndex = audioTokens.indexOf(face_driver_media_file_token);
+ setSelectedSourceIndex(sourceIndex);
+ setSelectedMotionIndex(motionIndex);
+ setCurrentCombinationKey(getCombinationKey(sourceIndex, motionIndex));
+
+ // Find the video for the clicked job
+ const videoFromJob = generatedVideos.find(
+ video => video.jobToken === job.jobToken
+ );
+ if (videoFromJob) {
+ setGeneratedVideoSrc(videoFromJob.videoSrc);
+ setIsGenerating(false);
+ } else {
+ // If there's no video and the job is still in the generating list, show progress
+ const isGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === sourceIndex && gen.motionIndex === motionIndex
+ );
+ if (isGenerating) {
+ const progress =
+ jobProgress[getCombinationKey(sourceIndex, motionIndex)] || 0;
+ setJobProgress({
+ ...jobProgress,
+ [getCombinationKey(sourceIndex, motionIndex)]: progress,
+ });
+ setIsGenerating(true);
+ } else {
+ setIsGenerating(false);
+ setGeneratedVideoSrc("");
+ }
+ }
+ }
+ };
+
+ const handleJobStateChange = useCallback(
+ (jobToken: string, jobState: JobState) => {
+ const currentCombinationKey = getCombinationKey(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+
+ if (jobState === JobState.COMPLETE_FAILURE) {
+ setCurrentlyGeneratingList(prevList =>
+ prevList.filter(
+ gen =>
+ !(
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ )
+ )
+ );
+
+ setJobProgress(prevProgress => {
+ const updatedProgress = { ...prevProgress };
+ delete updatedProgress[currentCombinationKey];
+ return updatedProgress;
+ });
+
+ setIsGenerating(false);
+ }
+ },
+ [selectedSourceIndex, selectedMotionIndex]
+ );
+
+ const handleJobProgress = (progress: number | null) => {
+ setJobProgress(prevProgress => ({
+ ...prevProgress,
+ [currentCombinationKey]: progress,
+ }));
+ };
+
+ // const handleSourceSelect = (index: number) => {
+ // setIsUserUploaded(index >= numberOfInitialSourceTokens);
+ // setSelectedSourceIndex(index);
+ // setCurrentCombinationKey(getCombinationKey(index, selectedMotionIndex));
+ // };
+
+ // const handleMotionSelect = (index: number) => {
+ // setIsUserUploaded(index >= numberOfInitialAudioTokens);
+ // setSelectedMotionIndex(index);
+ // setCurrentCombinationKey(getCombinationKey(selectedSourceIndex, index));
+ // };
+
+ const enqueueClick = () => {
+ // Clear the generated video when reanimating
+ setGeneratedVideoSrc("");
+ setIsGenerating(true);
+
+ // Add the current source and motion combination to the generating list
+ setCurrentlyGeneratingList(prevList => [
+ ...prevList,
+ { sourceIndex: selectedSourceIndex, motionIndex: selectedMotionIndex },
+ ]);
+
+ setIsEnqueuing(true);
+
+ const combinationKey = getCombinationKey(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+ setCurrentCombinationKey(combinationKey);
+
+ EnqueueLipsync("", {
+ creator_set_visibility: visibility,
+ audio_media_file_token: "",
+ maybe_crop: cropArea,
+ remove_watermark: removeWatermark,
+ image_or_video_media_file_token: "",
+ uuid_idempotency_token: uuidv4(),
+ }).then((res: EnqueueLipsyncResponse) => {
+ if (res.success && res.inference_job_token) {
+ enqueueInferenceJob(
+ res.inference_job_token,
+ FrontendInferenceJobType.Lipsync,
+ false
+ );
+ } else {
+ // @ts-ignore
+ window.dataLayer.push({
+ event: "enqueue_failure",
+ page: "/live-portrait",
+ user_id: "$user_id",
+ });
+ console.error("Failed to enqueue job", res);
+ setIsGenerating(false);
+ // Remove the combination from currentlyGeneratingList if fail
+ setCurrentlyGeneratingList(prevList =>
+ prevList.filter(
+ gen =>
+ gen.sourceIndex !== selectedSourceIndex ||
+ gen.motionIndex !== selectedMotionIndex
+ )
+ );
+ }
+ setIsEnqueuing(false);
+ });
+ };
+
+ const renderVideoOrPlaceholder = () => {
+ const isCurrentlyGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ );
+
+ const currentProgress =
+ jobProgress[
+ getCombinationKey(selectedSourceIndex, selectedMotionIndex)
+ ] || null;
+
+ const latestVideoSrc = getLatestVideoForCombination(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+
+ if (latestVideoSrc && !isCurrentlyGenerating) {
+ return (
+
+ );
+ } else if (isCurrentlyGenerating) {
+ return (
+
+
+
+
+
+ {currentProgress !== null
+ ? `Generating video... ${currentProgress}%`
+ : "Generating video..."}
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
+ {audioToken ? (
+
+ Click{" "}
+
+
+ Animate
+ {" "}
+ to start generating
+
+ ) : (
+
+
+
+ Generate Audio
+ {" "}
+ then click animate
+
+ )}
+
+
+
+ );
+ }
+ };
+
+ useEffect(() => {
+ if (userSourceToken) {
+ setSourceTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(userSourceToken);
+ if (tokenIndex !== -1) {
+ setSelectedSourceIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialSourceTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, userSourceToken];
+ setSelectedSourceIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ setUserSourceToken("");
+ }
+ }, [userSourceToken, numberOfInitialSourceTokens]);
+
+ useEffect(() => {
+ if (userMotionToken) {
+ setAudioTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(userMotionToken);
+ if (tokenIndex !== -1) {
+ setSelectedMotionIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialAudioTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, userMotionToken];
+ setSelectedMotionIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ setUserMotionToken("");
+ }
+ }, [userMotionToken, numberOfInitialAudioTokens]);
+
+ const isUserContent =
+ selectedSourceIndex >= numberOfInitialSourceTokens ||
+ selectedMotionIndex >= numberOfInitialAudioTokens;
+
+ const [selectedSourceMedia, setSelectedSourceMedia] = useState<{
+ [key: string]: any;
+ }>({});
+
+ const handleSelectedMediaChange = (media: any) => {
+ setSelectedSourceMedia(media);
+ };
+
+ const selectedSourceMediaLink = selectedSourceMedia?.public_bucket_path
+ ? new BucketConfig().getGcsUrl(selectedSourceMedia.public_bucket_path)
+ : null;
+
+ const handleJobTokens = async (
+ maybeResultToken: string,
+ jobToken: string,
+ createdAt: Date,
+ livePortraitDetails?: LivePortraitDetails
+ ) => {
+ if (!livePortraitDetails) {
+ return;
+ }
+
+ // makes sure that it only processes each job once and exactly when needed
+ if (jobProcessedTokens.includes(jobToken)) {
+ return;
+ }
+
+ const response = await GetMedia(maybeResultToken, {});
+
+ if (
+ response &&
+ response.media_file &&
+ response.media_file.public_bucket_path
+ ) {
+ const mediaLink = new BucketConfig().getGcsUrl(
+ response.media_file.public_bucket_path
+ );
+
+ const sourceIndex = sourceTokens.indexOf(
+ livePortraitDetails.source_media_file_token
+ );
+ const motionIndex = audioTokens.indexOf(
+ livePortraitDetails.face_driver_media_file_token
+ );
+
+ const newGeneratedVideo = {
+ sourceIndex,
+ motionIndex,
+ sourceToken: livePortraitDetails.source_media_file_token,
+ motionToken: livePortraitDetails.face_driver_media_file_token,
+ videoSrc: mediaLink,
+ jobToken,
+ createdAt,
+ };
+
+ setGeneratedVideos(prevGeneratedVideos => {
+ return [
+ ...prevGeneratedVideos.filter(v => v.jobToken !== jobToken),
+ newGeneratedVideo,
+ ];
+ });
+
+ // Set the video source from the clicked job, regardless of its timestamp relative to others
+ if (
+ selectedSourceIndex === newGeneratedVideo.sourceIndex &&
+ selectedMotionIndex === newGeneratedVideo.motionIndex
+ ) {
+ setGeneratedVideoSrc(mediaLink);
+ setIsGenerating(false);
+ }
+
+ setJobProcessedTokens(prevTokens => [...prevTokens, jobToken]);
+ } else {
+ console.error(
+ "Failed to retrieve media or media has no public bucket path",
+ response
+ );
+ setIsGenerating(false);
+ setGeneratedVideoSrc("");
+ setIsEnqueuing(false);
+ }
+ };
+
+ const getLatestVideoForCombination = useCallback(
+ (sourceIndex: number, motionIndex: number) => {
+ const matchingVideos = generatedVideos.filter(
+ video =>
+ video.sourceIndex === sourceIndex && video.motionIndex === motionIndex
+ );
+ const sortedVideos = matchingVideos.sort(
+ (a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ );
+ const latestVideo =
+ sortedVideos.length > 0 ? sortedVideos[0].videoSrc : null;
+
+ return latestVideo;
+ },
+ [generatedVideos]
+ );
+
+ useEffect(() => {
+ const latestVideoSrc = getLatestVideoForCombination(
+ selectedSourceIndex,
+ selectedMotionIndex
+ );
+
+ if (latestVideoSrc) {
+ setGeneratedVideoSrc(latestVideoSrc);
+ setIsGenerating(false);
+ } else if (!isUserUploaded) {
+ setGeneratedVideoSrc("");
+ setIsGenerating(false);
+ } else {
+ setGeneratedVideoSrc("");
+ }
+ }, [
+ selectedSourceIndex,
+ selectedMotionIndex,
+ generatedVideos,
+ isUserUploaded,
+ getLatestVideoForCombination,
+ ]);
+
+ useEffect(() => {
+ const queryParams = new URLSearchParams(location.search);
+ const sourceToken = queryParams.get("source");
+ const motionToken = queryParams.get("motion");
+
+ if (sourceToken) {
+ setSourceTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(sourceToken);
+ if (tokenIndex !== -1) {
+ setSelectedSourceIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialSourceTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, sourceToken];
+ setSelectedSourceIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ }
+
+ if (motionToken) {
+ setAudioTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(motionToken);
+ if (tokenIndex !== -1) {
+ setSelectedMotionIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialAudioTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, motionToken];
+ setSelectedMotionIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ }
+ }, [
+ location.search,
+ numberOfInitialSourceTokens,
+ numberOfInitialAudioTokens,
+ ]);
+
+ // const uploadFocusPointSource = useMemo(() => {
+ // const queryParams = new URLSearchParams(location.search);
+ // const sourceToken = queryParams.get("source");
+ // const motionToken = queryParams.get("motion");
+
+ // if (motionToken && !sourceToken) {
+ // return true;
+ // } else if (!motionToken && sourceToken) {
+ // return false;
+ // } else {
+ // return false;
+ // }
+ // }, [location.search]);
+
+ // const uploadFocusPointMotion = useMemo(() => {
+ // const queryParams = new URLSearchParams(location.search);
+ // const sourceToken = queryParams.get("source");
+ // const motionToken = queryParams.get("motion");
+
+ // if (sourceToken && !motionToken) {
+ // return true;
+ // } else if (!sourceToken && motionToken) {
+ // return false;
+ // } else {
+ // return false;
+ // }
+ // }, [location.search]);
+
+ useEffect(() => {
+ // When switching source or motion indexes, check if the current combination is generating
+ const isCurrentlyGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ );
+
+ if (isCurrentlyGenerating) {
+ setIsGenerating(true);
+ } else if (generatedVideoSrc) {
+ setIsGenerating(false);
+ } else if (!isUserUploaded) {
+ setIsGenerating(false);
+ } else {
+ setIsGenerating(false);
+ }
+ }, [
+ selectedSourceIndex,
+ selectedMotionIndex,
+ currentlyGeneratingList,
+ generatedVideoSrc,
+ isUserUploaded,
+ ]);
+
+ useEffect(() => {
+ if (generatedVideos.length > 0) {
+ const relevantVideos = generatedVideos.filter(video =>
+ currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === video.sourceIndex &&
+ gen.motionIndex === video.motionIndex
+ )
+ );
+
+ relevantVideos.forEach(video => {
+ if (video.jobToken) {
+ const jobIndex = currentlyGeneratingList.findIndex(
+ gen =>
+ gen.sourceIndex === video.sourceIndex &&
+ gen.motionIndex === video.motionIndex
+ );
+ if (jobIndex !== -1) {
+ const updatedGeneratingList = [...currentlyGeneratingList];
+ updatedGeneratingList.splice(jobIndex, 1);
+ setCurrentlyGeneratingList(updatedGeneratingList);
+
+ const isCurrentJobGenerating = currentlyGeneratingList.some(
+ gen =>
+ gen.sourceIndex === selectedSourceIndex &&
+ gen.motionIndex === selectedMotionIndex
+ );
+
+ if (!isCurrentJobGenerating) {
+ setGeneratedVideoSrc(video.videoSrc);
+ setIsGenerating(false);
+ }
+ }
+ }
+ });
+ }
+ }, [
+ generatedVideos,
+ currentlyGeneratingList,
+ selectedSourceIndex,
+ selectedMotionIndex,
+ ]);
+
+ const handleDownloadClick = () => {
+ if (generatedVideoSrc) {
+ const link = document.createElement("a");
+ link.href = generatedVideoSrc;
+ link.download = "output_video.mp4";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ } else {
+ console.error("No video source available for download");
+ }
+ };
+
+ useEffect(() => {
+ const queryParams = new URLSearchParams(location.search);
+ setVoiceToken(queryParams.get("voice"));
+
+ if (voiceToken) {
+ const precomputedSourceToken = LipsyncTokenMap[voiceToken];
+
+ if (precomputedSourceToken) {
+ // Set the precomputed source token as the media
+ setSourceTokens(prevTokens => {
+ const tokenIndex = prevTokens.indexOf(precomputedSourceToken);
+ if (tokenIndex !== -1) {
+ setSelectedSourceIndex(tokenIndex);
+ setIsUserUploaded(tokenIndex >= numberOfInitialSourceTokens);
+ return prevTokens;
+ } else {
+ const updatedTokens = [...prevTokens, precomputedSourceToken];
+ setSelectedSourceIndex(updatedTokens.length - 1);
+ setIsUserUploaded(true);
+ return updatedTokens;
+ }
+ });
+ }
+
+ GetWeight(voiceToken, {})
+ .then(response => {
+ if (response && response.success) {
+ const title = response.title || null;
+ setVoiceModelTitle(title);
+ } else {
+ console.error(
+ "Failed to retrieve media or media has no title",
+ response
+ );
+ }
+ })
+ .catch(error => {
+ console.error("Error fetching media:", error);
+ });
+ }
+ }, [location.search, numberOfInitialSourceTokens, voiceToken]);
+
+ const signupCTA = (
+ <>
+ {!sessionFetched ? null : (
+
+
+
+ You need to be logged in to use Lip Sync
+
+
+ Please login or sign up to continue.
+
+
+
+
+ )}
+ >
+ );
+
+ return (
+ <>
+
+
+
+
+ Lip Sync
+
+
+
+ Make your characters really speak with lip sync and text to speech
+
+
+ {voiceModelTitle ? (
+
+
+
+
+ {voiceModelTitle ? `Current Voice: ${voiceModelTitle}` : null}
+
+
+
+ ) : null}
+ {/*
*/}
+
+ {!loggedIn && (
+ {signupCTA}
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {renderVideoOrPlaceholder()}
+
+
+
+
+ {!isUserContent && (
+
+ )}
+
+
+
+
+
+
+
history.push("/signup?redirect=/lipsync")
+ }
+ className="flex-grow-1"
+ // disabled={!isUserContent}
+ isLoading={isEnqueuing || isGenerating}
+ disabled={(!isUserContent && loggedIn) || !audioToken}
+ />
+
+
+
+
+
+
+
+
+
+
+ {
+ setVisibility(prevVisibility =>
+ prevVisibility === "private"
+ ? "public"
+ : "private"
+ );
+ }}
+ checked={visibility === "private"}
+ className="mb-0"
+ />
+
+ {
+ setRemoveWatermark(
+ prevRemoveWatermark => !prevRemoveWatermark
+ );
+ }}
+ checked={removeWatermark}
+ className="mb-0"
+ />
+
+
+
+ {/* {!hasPremium && (
+
+
+
+ )} */}
+
+
+
+
+ {loggedIn && (
+
+
+
+
+ setSourceTokens(prevTokens =>
+ prevTokens.includes(newToken)
+ ? prevTokens
+ : [...prevTokens, newToken]
+ )
+ }
+ addMotionToken={(newToken: string) =>
+ setAudioTokens(prevTokens =>
+ prevTokens.includes(newToken)
+ ? prevTokens
+ : [...prevTokens, newToken]
+ )
+ }
+ onJobClick={handleJobClick}
+ onJobProgress={handleJobProgress}
+ onJobStateChange={handleJobStateChange}
+ />
+
+
+ )}
+
+
+
+
+
+
+
+ Try other AI video tools
+
+
+
+ >
+ );
+}
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/LipsyncAudioPlayer.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/LipsyncAudioPlayer.tsx
new file mode 100644
index 0000000000..031e8e31a6
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/LipsyncAudioPlayer.tsx
@@ -0,0 +1,36 @@
+import React, { useCallback, useState } from "react";
+import Wavesurfer from "react-wavesurfer.js";
+
+interface Props {
+ filename: string;
+ play: boolean;
+ onFinish?: () => void;
+}
+
+export default function LipsyncAudioPlayer(props: Props) {
+ const [position, setPosition] = useState(0);
+
+ const handleFinish = useCallback(() => {
+ setPosition(0);
+ props.onFinish && props.onFinish();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.onFinish]);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/LipsyncTokens.ts b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/LipsyncTokens.ts
new file mode 100644
index 0000000000..cd03245e45
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/LipsyncTokens.ts
@@ -0,0 +1,3 @@
+export const LipsyncTokenMap: { [key: string]: string } = {
+ weight_hz7g8f1j4psrsw2sv67e4y61q: "m_zx1fyp6t7n8b46swz087xkapa7s2vs", // Mariano Closs
+};
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/SessionLsInferenceResultsList.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/SessionLsInferenceResultsList.tsx
new file mode 100644
index 0000000000..cb5b978e26
--- /dev/null
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/lipsync/SessionLsInferenceResultsList.tsx
@@ -0,0 +1,244 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import {
+ FrontendInferenceJobType,
+ InferenceJob,
+} from "@storyteller/components/src/jobs/InferenceJob";
+import { useInferenceJobs } from "hooks";
+import { LivePortraitDetails } from "@storyteller/components/src/api/model_inference/GetModelInferenceJobStatus";
+import { GetMedia } from "@storyteller/components/src/api/media_files/GetMedia";
+import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
+import { JobState } from "@storyteller/components/src/jobs/JobStates";
+import moment from "moment";
+import { Link } from "react-router-dom";
+import LoadingSpinner from "components/common/LoadingSpinner";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowTurnUp } from "@fortawesome/pro-solid-svg-icons";
+
+interface SessionLpInferenceResultsListProps {
+ sessionSubscriptionsWrapper: any;
+ onJobTokens: (
+ maybeResultToken: string,
+ jobToken: string,
+ createdAt: Date,
+ maybeLivePortraitDetails?: LivePortraitDetails
+ ) => void;
+ addSourceToken: (token: string) => void;
+ addMotionToken: (token: string) => void;
+ onJobClick: (job: InferenceJob) => void;
+ onJobProgress: (progress: number | null) => void;
+ onJobStateChange: (jobToken: string, jobState: JobState) => void;
+}
+
+export default function SessionLpInferenceResultsList({
+ sessionSubscriptionsWrapper,
+ onJobTokens,
+ addMotionToken,
+ addSourceToken,
+ onJobClick,
+ onJobProgress,
+ onJobStateChange,
+}: SessionLpInferenceResultsListProps) {
+ const { inferenceJobsByCategory } = useInferenceJobs();
+ const hasInitialized = useRef(false);
+
+ const lastProgressRef = useRef<{ [key: string]: number | null }>({});
+
+ const livePortraitJobs = useMemo(() => {
+ return (
+ inferenceJobsByCategory.get(FrontendInferenceJobType.LivePortrait) || []
+ );
+ }, [inferenceJobsByCategory]);
+
+ const lastProcessedJobToken = useRef(null);
+
+ useEffect(() => {
+ if (!hasInitialized.current) {
+ hasInitialized.current = true;
+ } else {
+ livePortraitJobs.forEach((job: InferenceJob) => {
+ if (
+ job.maybeResultToken &&
+ job.jobToken !== lastProcessedJobToken.current
+ ) {
+ const livePortraitDetails = job.maybeLivePortraitDetails;
+
+ if (livePortraitDetails) {
+ const { source_media_file_token, face_driver_media_file_token } =
+ livePortraitDetails;
+
+ addSourceToken(source_media_file_token);
+
+ addMotionToken(face_driver_media_file_token);
+
+ lastProcessedJobToken.current = job.jobToken;
+ onJobTokens(
+ job.maybeResultToken,
+ job.jobToken,
+ job.createdAt,
+ livePortraitDetails
+ );
+ }
+ }
+ });
+ }
+ }, [livePortraitJobs, onJobTokens, addSourceToken, addMotionToken]);
+
+ useEffect(() => {
+ livePortraitJobs.forEach((job: InferenceJob) => {
+ const currentProgress = job.progressPercentage;
+
+ // Early return if nothing has changed
+ if (
+ (job.jobState === JobState.STARTED && currentProgress === null) ||
+ (job.jobState !== JobState.STARTED &&
+ job.jobState !== JobState.COMPLETE_SUCCESS &&
+ job.jobState !== JobState.COMPLETE_FAILURE)
+ ) {
+ return;
+ }
+
+ // Progress handling for STARTED jobs
+ if (job.jobState === JobState.STARTED) {
+ if (lastProgressRef.current[job.jobToken] !== currentProgress) {
+ lastProgressRef.current[job.jobToken] = currentProgress;
+ onJobProgress(currentProgress);
+ }
+ return;
+ }
+
+ // Handling COMPLETE_SUCCESS or COMPLETE_FAILURE states
+ if (
+ (job.jobState === JobState.COMPLETE_SUCCESS ||
+ job.jobState === JobState.COMPLETE_FAILURE) &&
+ lastProgressRef.current[job.jobToken] !== null
+ ) {
+ lastProgressRef.current[job.jobToken] = null;
+ onJobProgress(null);
+ onJobStateChange(job.jobToken, job.jobState);
+ }
+ });
+ }, [livePortraitJobs, onJobProgress, onJobStateChange]);
+
+ const [mediaSrc, setMediaSrc] = useState<{ [key: string]: string }>({});
+
+ useEffect(() => {
+ const fetchMedia = async (token: string) => {
+ try {
+ const response = await GetMedia(token, {});
+ const publicBucketPath = response.media_file?.public_bucket_path || "";
+ setMediaSrc(prev => ({ ...prev, [token]: publicBucketPath }));
+ } catch (error) {
+ console.error("Error fetching media:", error);
+ }
+ };
+
+ livePortraitJobs.forEach((job: InferenceJob) => {
+ const token = job.maybeLivePortraitDetails?.source_media_file_token;
+ if (token && !mediaSrc[token]) {
+ fetchMedia(token);
+ }
+ });
+ }, [livePortraitJobs, mediaSrc]);
+
+ const jobStateTextMap: { [key in JobState]: string } = {
+ [JobState.UNKNOWN]: "Unknown",
+ [JobState.PENDING]: "Pending",
+ [JobState.STARTED]: "Generating",
+ [JobState.COMPLETE_SUCCESS]: "Completed",
+ [JobState.COMPLETE_FAILURE]: "Completed (Failure)",
+ [JobState.ATTEMPT_FAILED]: "Attempt Failed",
+ [JobState.DEAD]: "Dead",
+ [JobState.CANCELED_BY_USER]: "Canceled by User",
+ };
+
+ const jobContent = (
+
+ {livePortraitJobs.length > 0 ? (
+
+ {livePortraitJobs
+ .slice(0, 4)
+ .map((job: InferenceJob, key: number) => (
+
{
+ onJobClick(job);
+ }}
+ className="col-12 col-lg-3"
+ >
+
+
+

{
+ const mediaPath =
+ mediaSrc[
+ job.maybeLivePortraitDetails
+ .source_media_file_token
+ ] || "";
+ const isVideo = mediaPath.endsWith(".mp4");
+ const finalPath = isVideo
+ ? `${mediaPath}-thumb.jpg`
+ : mediaPath;
+ return new BucketConfig().getGcsUrl(finalPath);
+ })()
+ : ""
+ }
+ alt="Job Thumbnail"
+ className="object-fit-cover w-100 h-100 rounded"
+ />
+
+
+
+ {(job.jobState === JobState.PENDING ||
+ job.jobState === JobState.STARTED) && (
+
+ )}
+
+ {jobStateTextMap[job.jobState as JobState]}
+
+
+
+ {moment(job.createdAt).fromNow()}
+
+
+ {job.maybeResultToken ? (
+
+ More Details
+
+ ) : (
+
+ {job.progressPercentage}% complete
+
+ )}
+
+
+
+
+
+ ))}
+
+ ) : (
+
+
+ Your latest lip sync generations will appear here.
+
+
+ )}
+
+ );
+
+ return <>{jobContent}>;
+}
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailItem.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailItem.tsx
index e7e8f14bf6..375c1fed6e 100644
--- a/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailItem.tsx
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailItem.tsx
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from "react";
interface ThumbnailItemProps {
index: number;
selectedIndex: number;
- handleThumbnailClick: (index: number) => void;
+ handleThumbnailClick?: (index: number) => void;
poster?: string;
mediaType?: string;
}
@@ -48,7 +48,7 @@ const ThumbnailItem: React.FC = ({
handleThumbnailClick(index)}
+ onClick={() => handleThumbnailClick && handleThumbnailClick(index)}
>
{poster ? (
mediaType === "image" ? (
diff --git a/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailMediaPicker.tsx b/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailMediaPicker.tsx
index d6344dd1da..cd7b71cd94 100644
--- a/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailMediaPicker.tsx
+++ b/src/website/packages/fakeyou.com/src/v2/view/pages/live_portrait/ThumbnailMediaPicker.tsx
@@ -21,7 +21,7 @@ import { useSession } from "hooks";
interface ThumbnailMediaPickerProps {
mediaTokens: string[];
selectedIndex: number;
- handleThumbnailClick: (index: number) => void;
+ handleThumbnailClick?: (index: number) => void;
title?: string;
description?: string;
badgeLabel?: string;
@@ -39,6 +39,10 @@ interface ThumbnailMediaPickerProps {
onSelectedMediaChange?: (media: any) => void;
uploadFocusPoint?: boolean;
uploadButtonText?: string;
+ showThumbnails?: boolean;
+ showUploadButton?: boolean;
+ showStep?: boolean;
+ stepAlwaysOnTop?: boolean;
}
interface MediaData {
@@ -62,6 +66,10 @@ const ThumbnailMediaPicker: React.FC
= React.memo(
onSelectedMediaChange,
uploadFocusPoint,
uploadButtonText = "Upload your own media",
+ showThumbnails = true,
+ showUploadButton = true,
+ showStep = true,
+ stepAlwaysOnTop = false,
}) => {
const [isCropping, setIsCropping] = useState(false);
const [mediaData, setMediaData] = useState<{ [key: string]: any }>({});
@@ -146,8 +154,10 @@ const ThumbnailMediaPicker: React.FC = React.memo(
);
return (
-
-
+
+
{!isCropping ? (
@@ -232,59 +242,67 @@ const ThumbnailMediaPicker: React.FC = React.memo(
)}
-
-
- {stepNumber &&
{stepNumber}
}
-
{title}
-
-
-
{description}
-
-
-
- {paginatedMediaTokens.map((token, index) => {
- const media = mediaData[token];
- const mediaLink = media?.public_bucket_path
- ? new BucketConfig().getGcsUrl(media.public_bucket_path)
- : null;
-
- return (
-
- );
- })}
-
- {mediaTokens.length > itemsPerPage && (
-
-
-
+ {showStep && (
+
+
+ {stepNumber &&
{stepNumber}
}
+
{title}
- )}
-
-
+
{description}
+
+ )}
+
+ {showThumbnails && (
+
+ {paginatedMediaTokens.map((token, index) => {
+ const media = mediaData[token];
+ const mediaLink = media?.public_bucket_path
+ ? new BucketConfig().getGcsUrl(media.public_bucket_path)
+ : null;
+
+ return (
+
+ );
+ })}
+
+ {mediaTokens.length > itemsPerPage && (
+
+
+
+
+ )}
+
+ )}
+
+ {showUploadButton && (
+
+ )}
);
}