live portrait cta to lipsync as source media (#699)

* live portrait cta to lipsync as source media

* lip icon
This commit is contained in:
BFlat authored and GitHub committed 2024-09-11 18:22:50 +07:00
1 parent 6407f30d1a
commit 6fef9167ce
5 files changed
+183 -69

No files matched your search

@@ -64,7 +64,6 @@ function SessionTtsInferenceResultList(props: Props) {
const fetchMedia = async (token: string) => {
try {
const response = await GetWeight(token, {});
console.log("GetWeight response:", response);
const publicBucketPath =
response.cover_image.maybe_cover_image_public_bucket_path || "";
setMediaSrc(prev => ({ ...prev, [token]: publicBucketPath }));
@@ -301,10 +301,21 @@ export const GenerateTts = ({
modelToken: string | undefined,
resultToken: string | undefined | null
) => {
if (modelToken && resultToken && isAudioLoading === false) {
if (modelToken && resultToken && !isAudioLoading) {
handleClearAudio();
setLoadingSelectedAudioResult(true);
const newUrl = `/ai-lip-sync?voice=${modelToken}&audio=${resultToken}`;
const url = new URL(window.location.href);
const searchParams = new URLSearchParams(url.search);
const sourceToken = searchParams.get("source");
searchParams.set("voice", modelToken);
searchParams.set("audio", resultToken || "");
if (sourceToken) {
searchParams.set("source", sourceToken);
}
const newUrl = `${url.pathname}?${searchParams.toString()}`;
history.push(newUrl);
try {
@@ -347,8 +358,18 @@ export const GenerateTts = ({
const handleVoiceSelect = async (data: any) => {
if (data.weight_token) {
const newUrl = `/ai-lip-sync?voice=${data.weight_token}`;
const url = new URL(window.location.href);
const searchParams = new URLSearchParams(url.search);
const sourceToken = searchParams.get("source");
searchParams.set("voice", data.weight_token);
if (sourceToken) {
searchParams.set("source", sourceToken);
}
const newUrl = `${url.pathname}?${searchParams.toString()}`;
history.push(newUrl);
close();
}
};
@@ -411,6 +432,7 @@ export const GenerateTts = ({
<Panel
padding={true}
className="panel-inner h-100 position-relative rounded"
key={currentAudioUrl}
>
<div className="d-flex flex-column justify-content-center h-100">
<div className="d-flex gap-3 align-items-center justify-content-center">
@@ -423,7 +445,6 @@ export const GenerateTts = ({
/>
<div className="w-100">
<LipsyncAudioPlayer
key={currentAudioUrl}
filename={currentAudioUrl || ""}
play={isPlaying}
onFinish={handleAudioFinish}
@@ -433,7 +454,7 @@ export const GenerateTts = ({
<div className="pt-4">
<h6 className="fw-bold">
<FontAwesomeIcon icon={faSquareQuote} className="me-2" />
Audio Transcript
{t("label.audioTranscript")}
</h6>
<p className="fs-7">{transcript}</p>
</div>
@@ -80,8 +80,11 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
const [sourceTokens, setSourceTokens] = useState<string[]>([
...PRECOMPUTED_SOURCE_TOKENS,
]);
const numberOfInitialSourceTokensRef = useRef(sourceTokens.length);
const numberOfInitialSourceTokens = numberOfInitialSourceTokensRef.current;
const [sourceTokenFromQuery, setSourceTokenFromQuery] = useState<
string | null
>(null);
// const numberOfInitialSourceTokensRef = useRef(sourceTokens.length);
// const numberOfInitialSourceTokens = numberOfInitialSourceTokensRef.current;
const [isGenerating, setIsGenerating] = useState(false);
const [jobProcessedTokens, setJobProcessedTokens] = useState<string[]>([]);
const [voiceToken, setVoiceToken] = useState<string | null>(null);
@@ -319,13 +322,26 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
setVoiceToken(queryParams.get("voice"));
}, [location.search]);
const voiceTokenFromQuery = queryParams.get("voice");
const sourceTokenFromQuery = queryParams.get("source");
useEffect(() => {
if (voiceToken) {
const precomputedSourceToken = LipsyncTokenMap[voiceToken];
setVoiceToken(voiceTokenFromQuery);
setSourceTokenFromQuery(sourceTokenFromQuery);
if (sourceTokenFromQuery) {
setSourceTokens(prevTokens => {
const tokenIndex = prevTokens.indexOf(sourceTokenFromQuery);
if (tokenIndex !== -1) {
setSelectedSourceIndex(tokenIndex);
return prevTokens;
} else {
const updatedTokens = [...prevTokens, sourceTokenFromQuery];
setSelectedSourceIndex(updatedTokens.length - 1);
return updatedTokens;
}
});
} else if (voiceTokenFromQuery) {
const precomputedSourceToken = LipsyncTokenMap[voiceTokenFromQuery];
if (precomputedSourceToken) {
setSourceTokens(prevTokens => {
const tokenIndex = prevTokens.indexOf(precomputedSourceToken);
@@ -340,7 +356,7 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
});
}
}
}, [voiceToken, numberOfInitialSourceTokens]);
}, [location.search]);
const signupCTA = (
<>
@@ -373,6 +389,22 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
</>
);
const handleRemoveSourceMedia = () => {
if (sourceTokenFromQuery) {
setSourceTokenFromQuery(null);
setGeneratedVideoSrc("");
setJobProcessedTokens([]);
setJobPercentage(null);
setLastEnqueuedJobToken(null);
setIsGenerating(false);
const queryParams = new URLSearchParams(location.search);
queryParams.delete("source");
history.replace({ search: queryParams.toString() });
setSelectedSourceIndex(0);
}
};
return (
<>
<Container type="panel" className="mt-3 mt-lg-5">
@@ -422,6 +454,8 @@ export default function Lipsync({ sessionSubscriptionsWrapper }: LipsyncProps) {
showUploadButton={false}
showThumbnails={false}
stepAlwaysOnTop={true}
showRemoveButton={!!sourceTokenFromQuery}
onRemoveMedia={handleRemoveSourceMedia}
/>
</div>
<Button
@@ -12,7 +12,9 @@ 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";
import { faArrowTurnUp, faLips } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "components/common";
import { useHistory } from "react-router-dom";
interface SessionLpInferenceResultsListProps {
sessionSubscriptionsWrapper: any;
@@ -38,6 +40,8 @@ export default function SessionLpInferenceResultsList({
onJobProgress,
onJobStateChange,
}: SessionLpInferenceResultsListProps) {
const history = useHistory();
const { inferenceJobsByCategory } = useInferenceJobs();
const hasInitialized = useRef(false);
@@ -151,6 +155,19 @@ export default function SessionLpInferenceResultsList({
[JobState.CANCELED_BY_USER]: "Canceled by User",
};
const handleLipsyncCTA = (
e: React.MouseEvent<HTMLButtonElement>,
resultToken: string | undefined
) => {
e.preventDefault();
e.stopPropagation();
if (resultToken) {
history.push(`/ai-lip-sync?source=${resultToken}`);
} else {
console.error("No result token available for this job.");
}
};
const jobContent = (
<div>
{livePortraitJobs.length > 0 ? (
@@ -166,63 +183,86 @@ export default function SessionLpInferenceResultsList({
className="col-12 col-lg-3"
>
<div className="lp-jobs-list">
<div
className="ratio ratio-1x1 overflow-hidden rounded"
style={{ width: "70px" }}
>
<img
src={
job.maybeLivePortraitDetails?.source_media_file_token
? (() => {
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"
/>
</div>
<div className="d-flex flex-column flex-grow-1">
<div className="d-flex gap-2 align-items-center">
{(job.jobState === JobState.PENDING ||
job.jobState === JobState.STARTED) && (
<LoadingSpinner thin={true} size={14} padding={false} />
)}
<span className="fw-semibold">
{jobStateTextMap[job.jobState as JobState]}
</span>
</div>
<span className="fw-normal opacity-75 fs-7">
{moment(job.createdAt).fromNow()}
</span>
<div className="d-flex">
{job.maybeResultToken ? (
<Link
className="fs-7 d-flex align-items-center gap-1 mt-1"
to={`/media/${job.maybeResultToken}`}
>
More Details
</Link>
) : (
<div className="fs-7 opacity-50 fw-medium mt-1">
{job.progressPercentage}% complete
<div className="d-flex flex-column gap-2 w-100">
<div className="d-flex gap-2 align-items-center w-100">
<div
className="ratio ratio-1x1 overflow-hidden rounded"
style={{ width: "70px" }}
>
<img
src={
job.maybeLivePortraitDetails
?.source_media_file_token
? (() => {
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"
/>
</div>
<div className="d-flex flex-column flex-grow-1">
<div className="d-flex gap-2 align-items-center">
{(job.jobState === JobState.PENDING ||
job.jobState === JobState.STARTED) && (
<LoadingSpinner
thin={true}
size={14}
padding={false}
/>
)}
<span className="fw-semibold">
{jobStateTextMap[job.jobState as JobState]}
</span>
</div>
)}
<span className="fw-normal opacity-75 fs-7">
{moment(job.createdAt).fromNow()}
</span>
<div className="d-flex">
{job.maybeResultToken ? (
<Link
className="fs-7 d-flex align-items-center gap-1 mt-1"
to={`/media/${job.maybeResultToken}`}
>
More Details
</Link>
) : (
<div className="fs-7 opacity-50 fw-medium mt-1">
{job.progressPercentage}% complete
</div>
)}
</div>
</div>
<FontAwesomeIcon
icon={faArrowTurnUp}
className="pe-2 fs-5 opacity-75"
/>
</div>
{job.maybeResultToken && (
<Button
icon={faLips}
label="Use with Lip Sync"
small={true}
onClick={e => {
if (job.maybeResultToken !== null) {
handleLipsyncCTA(e, job.maybeResultToken);
}
}}
/>
)}
</div>
<FontAwesomeIcon
icon={faArrowTurnUp}
className="pe-2 fs-5 opacity-75"
/>
</div>
</div>
))}
@@ -12,6 +12,7 @@ import {
faUpload,
faChevronLeft,
faChevronRight,
faXmark,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { isMobile } from "react-device-detect";
@@ -44,6 +45,8 @@ interface ThumbnailMediaPickerProps {
showStep?: boolean;
stepAlwaysOnTop?: boolean;
videoRef?: React.RefObject<HTMLVideoElement>;
showRemoveButton?: boolean;
onRemoveMedia?: () => void;
}
interface MediaData {
@@ -72,6 +75,8 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
showStep = true,
stepAlwaysOnTop = false,
videoRef,
showRemoveButton = false,
onRemoveMedia,
}) => {
const [isCropping, setIsCropping] = useState(false);
const [mediaData, setMediaData] = useState<{ [key: string]: any }>({});
@@ -159,6 +164,21 @@ const ThumbnailMediaPicker: React.FC<ThumbnailMediaPickerProps> = React.memo(
<div
className={`lp-media order-4 ${stepAlwaysOnTop ? "" : "order-lg-1"}`}
>
{showRemoveButton && (
<div
style={{
position: "absolute",
top: "10px",
right: "10px",
zIndex: 10,
}}
>
<button onClick={onRemoveMedia} className="ls-remove-audio-btn">
<FontAwesomeIcon icon={faXmark} />
</button>
</div>
)}
<div className="lp-tag">
<div>
{!isCropping ? (