seedance multi generations at a time (#1185)

This commit is contained in:
BFlat authored and GitHub committed 2026-03-20 20:36:45 +07:00
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 -----