mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
live portrait cta to lipsync as source media (#699)
* live portrait cta to lipsync as source media * lip icon
This commit is contained in:
1 parent
6407f30d1a
commit
6fef9167ce
5 files changed
+183
-69
No files matched your search
-1
@@ -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
|
||||
|
||||
+95
-55
@@ -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>
|
||||
))}
|
||||
|
||||
+20
@@ -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 ? (
|
||||
|
||||
Reference in new issue
Block a user