mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 23:47:51 +00:00
seedance multi generations at a time (#1185)
This commit is contained in:
1 parent
4905cdcd3d
commit
9d380decf9
5 files changed
+163
-69
No files matched your search
@@ -210,9 +210,11 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
||||
uploadVideo={UploadVideoMedia}
|
||||
uploadAudio={UploadAudioMedia}
|
||||
credits={videoCredits}
|
||||
onEnqueuePressed={async (prompt, subscriberId) => {
|
||||
onEnqueuePressed={async (prompt, subscriberIds) => {
|
||||
const modelLabel = selectedVideoModel?.fullName ?? "";
|
||||
startBatch(prompt, modelLabel, subscriberId);
|
||||
for (const subscriberId of subscriberIds) {
|
||||
startBatch(prompt, modelLabel, subscriberId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -169,7 +169,7 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
|
||||
case ModelPage.ImageToVideo:
|
||||
return {
|
||||
resolution: promptVideo.resolution,
|
||||
generationCount: 1, // Video doesn't have generation count
|
||||
generationCount: promptVideo.generationCount,
|
||||
label: "Videos",
|
||||
};
|
||||
case ModelPage.ImageEditor:
|
||||
|
||||
@@ -35,6 +35,7 @@ import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { ImagePromptRow } from "./ImagePromptRow";
|
||||
import type { UploadImageFn } from "./ImagePromptRow";
|
||||
import { AspectRatioIcon } from "./common/AspectRatioIcon";
|
||||
import { VideoGenerationCountPicker } from "./common/VideoGenerationCountPicker";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import { GenerationProvider } from "@storyteller/api-enums";
|
||||
@@ -58,7 +59,7 @@ interface PromptBoxVideoProps {
|
||||
useJobContext: () => JobContextType;
|
||||
onEnqueuePressed?: (
|
||||
prompt: string,
|
||||
subscriberId: string,
|
||||
subscriberIds: string[],
|
||||
) => void | Promise<void>;
|
||||
selectedModel?: VideoModel;
|
||||
selectedProvider?: GenerationProvider;
|
||||
@@ -117,6 +118,8 @@ export const PromptBoxVideo = ({
|
||||
const setDuration = usePromptVideoStore((s) => s.setDuration);
|
||||
const inputMode = usePromptVideoStore((s) => s.inputMode);
|
||||
const setInputMode = usePromptVideoStore((s) => s.setInputMode);
|
||||
const generationCount = usePromptVideoStore((s) => s.generationCount);
|
||||
const setGenerationCount = usePromptVideoStore((s) => s.setGenerationCount);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
@@ -232,6 +235,13 @@ export const PromptBoxVideo = ({
|
||||
}
|
||||
}, [selectedModel]);
|
||||
|
||||
// Reset generation count when switching away from seedance 2.0
|
||||
useEffect(() => {
|
||||
if (selectedModel?.id !== "seedance_2p0" && generationCount > 1) {
|
||||
setGenerationCount(1);
|
||||
}
|
||||
}, [selectedModel]);
|
||||
|
||||
const durationRange = selectedModel?.durationOptions?.length
|
||||
? {
|
||||
min: selectedModel.durationOptions[0]!,
|
||||
@@ -495,9 +505,8 @@ export const PromptBoxVideo = ({
|
||||
|
||||
gtagEvent("enqueue_video");
|
||||
|
||||
const subscriberId = crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: Math.random().toString(36).slice(2);
|
||||
const isSeedance2 = selectedModel.id === "seedance_2p0";
|
||||
const count = isSeedance2 ? generationCount : 1;
|
||||
|
||||
const isRefMode =
|
||||
inputMode === "reference" && !!selectedModel.supportsReferenceMode;
|
||||
@@ -515,83 +524,104 @@ export const PromptBoxVideo = ({
|
||||
setIsEnqueueing(false);
|
||||
}, 10000);
|
||||
|
||||
let request: EnqueueImageToVideoRequest = {
|
||||
model: selectedModel,
|
||||
image_media_token: imageMediaToken,
|
||||
prompt: prompt,
|
||||
end_frame_image_media_token: isRefMode
|
||||
? undefined
|
||||
: endFrameImage?.mediaToken,
|
||||
frontend_caller: "image_to_video",
|
||||
frontend_subscriber_id: subscriberId,
|
||||
};
|
||||
const buildRequest = (subscriberId: string): EnqueueImageToVideoRequest => {
|
||||
let request: EnqueueImageToVideoRequest = {
|
||||
model: selectedModel,
|
||||
image_media_token: imageMediaToken,
|
||||
prompt: prompt,
|
||||
end_frame_image_media_token: isRefMode
|
||||
? undefined
|
||||
: endFrameImage?.mediaToken,
|
||||
frontend_caller: "image_to_video",
|
||||
frontend_subscriber_id: subscriberId,
|
||||
};
|
||||
|
||||
if (!!selectedProvider) {
|
||||
request.provider = selectedProvider;
|
||||
}
|
||||
if (!!selectedProvider) {
|
||||
request.provider = selectedProvider;
|
||||
}
|
||||
|
||||
if (selectedModel.generateWithSound) {
|
||||
request.generate_audio = !!generateWithSound;
|
||||
}
|
||||
if (selectedModel.generateWithSound) {
|
||||
request.generate_audio = !!generateWithSound;
|
||||
}
|
||||
|
||||
// Pass reference image tokens in reference mode
|
||||
if (isRefMode && referenceImages.length > 0) {
|
||||
request.reference_image_media_tokens = referenceImages.map(
|
||||
(img) => img.mediaToken,
|
||||
);
|
||||
}
|
||||
// Pass reference image tokens in reference mode
|
||||
if (isRefMode && referenceImages.length > 0) {
|
||||
request.reference_image_media_tokens = referenceImages.map(
|
||||
(img) => img.mediaToken,
|
||||
);
|
||||
}
|
||||
|
||||
// Pass reference video tokens in reference mode
|
||||
if (isRefMode && referenceVideos.length > 0) {
|
||||
request.reference_video_media_tokens = referenceVideos.map(
|
||||
(v) => v.mediaToken,
|
||||
);
|
||||
}
|
||||
// Pass reference video tokens in reference mode
|
||||
if (isRefMode && referenceVideos.length > 0) {
|
||||
request.reference_video_media_tokens = referenceVideos.map(
|
||||
(v) => v.mediaToken,
|
||||
);
|
||||
}
|
||||
|
||||
// Pass reference audio tokens in reference mode
|
||||
if (isRefMode && referenceAudios.length > 0) {
|
||||
request.reference_audio_media_tokens = referenceAudios.map(
|
||||
(a) => a.mediaToken,
|
||||
);
|
||||
}
|
||||
// Pass reference audio tokens in reference mode
|
||||
if (isRefMode && referenceAudios.length > 0) {
|
||||
request.reference_audio_media_tokens = referenceAudios.map(
|
||||
(a) => a.mediaToken,
|
||||
);
|
||||
}
|
||||
|
||||
// Pass duration if model supports it
|
||||
if (selectedModel.durationOptions && duration !== null) {
|
||||
request.duration_seconds = duration;
|
||||
}
|
||||
// Pass duration if model supports it
|
||||
if (selectedModel.durationOptions && duration !== null) {
|
||||
request.duration_seconds = duration;
|
||||
}
|
||||
|
||||
switch (selectedModel?.tauriId) {
|
||||
case "grok_video":
|
||||
request.grok_aspect_ratio = getGrokAspectRatio();
|
||||
break;
|
||||
switch (selectedModel?.tauriId) {
|
||||
case "grok_video":
|
||||
request.grok_aspect_ratio = getGrokAspectRatio();
|
||||
break;
|
||||
|
||||
case "sora_2":
|
||||
request.sora_orientation =
|
||||
resolution === "720p" ? "landscape" : "portrait";
|
||||
break;
|
||||
}
|
||||
case "sora_2":
|
||||
request.sora_orientation =
|
||||
resolution === "720p" ? "landscape" : "portrait";
|
||||
break;
|
||||
}
|
||||
|
||||
if (selectedModel.supportsCommonAspectRatio) {
|
||||
// NB: This should be the *new* aspect ratio behavior for all models.
|
||||
const selectedOption = selectedModel.sizeOptions?.find(
|
||||
(option) => option.textLabel === aspectRatio,
|
||||
);
|
||||
if (selectedModel.supportsCommonAspectRatio) {
|
||||
const selectedOption = selectedModel.sizeOptions?.find(
|
||||
(option) => option.textLabel === aspectRatio,
|
||||
);
|
||||
|
||||
if (selectedOption) {
|
||||
request.aspect_ratio =
|
||||
selectedOption.tauriValue as typeof request.aspect_ratio;
|
||||
} else {
|
||||
const maybeDefault = selectedModel.sizeOptions[0];
|
||||
if (!!maybeDefault) {
|
||||
if (selectedOption) {
|
||||
request.aspect_ratio =
|
||||
maybeDefault.tauriValue as typeof request.aspect_ratio;
|
||||
selectedOption.tauriValue as typeof request.aspect_ratio;
|
||||
} else {
|
||||
const maybeDefault = selectedModel.sizeOptions[0];
|
||||
if (!!maybeDefault) {
|
||||
request.aspect_ratio =
|
||||
maybeDefault.tauriValue as typeof request.aspect_ratio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
const subscriberIds: string[] = [];
|
||||
const enqueuePromises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const subscriberId = crypto.randomUUID
|
||||
? crypto.randomUUID()
|
||||
: Math.random().toString(36).slice(2);
|
||||
subscriberIds.push(subscriberId);
|
||||
enqueuePromises.push(
|
||||
EnqueueImageToVideo(buildRequest(subscriberId)) as Promise<void>,
|
||||
);
|
||||
}
|
||||
|
||||
await EnqueueImageToVideo(request);
|
||||
try {
|
||||
await Promise.all(enqueuePromises);
|
||||
} catch (err) {
|
||||
console.error("PromptBoxVideo - enqueue failed", err);
|
||||
toast.error("Failed to start video generation. Please try again.");
|
||||
}
|
||||
|
||||
onEnqueuePressed?.(prompt, subscriberId);
|
||||
onEnqueuePressed?.(prompt, subscriberIds);
|
||||
|
||||
setIsEnqueueing(false);
|
||||
};
|
||||
@@ -972,6 +1002,7 @@ export const PromptBoxVideo = ({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{modelNeedsAnImageButNoneAreSelected && (
|
||||
@@ -980,6 +1011,13 @@ export const PromptBoxVideo = ({
|
||||
Starting frame required
|
||||
</span>
|
||||
)}
|
||||
{selectedModel?.id === "seedance_2p0" && (
|
||||
<VideoGenerationCountPicker
|
||||
maxCount={4}
|
||||
currentCount={generationCount}
|
||||
handleCountChange={setGenerationCount}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
content="Add a starting image before generating"
|
||||
position="top"
|
||||
@@ -994,7 +1032,7 @@ export const PromptBoxVideo = ({
|
||||
onClick={handleEnqueue}
|
||||
disabled={!prompt.trim()}
|
||||
loading={isEnqueueing}
|
||||
credits={credits}
|
||||
credits={credits != null ? credits * generationCount : credits}
|
||||
>
|
||||
Generate
|
||||
</GenerateButton>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { faCopy } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
|
||||
interface VideoGenerationCountPickerProps {
|
||||
maxCount: number;
|
||||
currentCount: number;
|
||||
handleCountChange: (count: number) => void;
|
||||
}
|
||||
|
||||
export const VideoGenerationCountPicker = ({
|
||||
maxCount,
|
||||
currentCount,
|
||||
handleCountChange,
|
||||
}: VideoGenerationCountPickerProps) => {
|
||||
const options: PopoverItem[] = [];
|
||||
for (let i = 1; i <= maxCount; i++) {
|
||||
options.push({
|
||||
label: String(i),
|
||||
selected: i === currentCount,
|
||||
});
|
||||
}
|
||||
|
||||
const onSelect = (item: PopoverItem) => {
|
||||
const count = parseInt(item.label, 10);
|
||||
if (!isNaN(count) && count >= 1 && count <= maxCount) {
|
||||
handleCountChange(count);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content="Number of generations"
|
||||
position="top"
|
||||
className="z-50"
|
||||
closeOnClick={true}
|
||||
delay={0}
|
||||
>
|
||||
<PopoverMenu
|
||||
items={options}
|
||||
onSelect={onSelect}
|
||||
mode="toggle"
|
||||
panelTitle="No. of videos"
|
||||
triggerIcon={<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />}
|
||||
buttonClassName="h-9"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -139,6 +139,7 @@ interface PromptVideoStore {
|
||||
generateWithSound: boolean;
|
||||
duration: number | null;
|
||||
inputMode: VideoInputMode;
|
||||
generationCount: number;
|
||||
setPrompt: (prompt: string) => void;
|
||||
setResolution: (resolution: Resolution | string) => void;
|
||||
setAspectRatio: (aspectRatio: string | null) => void;
|
||||
@@ -150,6 +151,7 @@ interface PromptVideoStore {
|
||||
setGenerateWithSound: (value: boolean) => void;
|
||||
setDuration: (duration: number | null) => void;
|
||||
setInputMode: (mode: VideoInputMode) => void;
|
||||
setGenerationCount: (count: number) => void;
|
||||
}
|
||||
|
||||
export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
||||
@@ -164,6 +166,7 @@ export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
||||
generateWithSound: true,
|
||||
duration: null,
|
||||
inputMode: "keyframe",
|
||||
generationCount: 1,
|
||||
setPrompt: (prompt) => set({ prompt }),
|
||||
setResolution: (resolution) => set({ resolution }),
|
||||
setAspectRatio: (aspectRatio) => set({ aspectRatio }),
|
||||
@@ -175,6 +178,7 @@ export const usePromptVideoStore = create<PromptVideoStore>()((set) => ({
|
||||
setGenerateWithSound: (generateWithSound) => set({ generateWithSound }),
|
||||
setDuration: (duration) => set({ duration }),
|
||||
setInputMode: (inputMode) => set({ inputMode }),
|
||||
setGenerationCount: (generationCount) => set({ generationCount }),
|
||||
}));
|
||||
|
||||
// ----- Edit Prompt Box Store -----
|
||||
|
||||
Reference in new issue
Block a user