diff --git a/frontend/apps/artcraft-webapp/src/lib/cost-estimate-api.ts b/frontend/apps/artcraft-webapp/src/lib/cost-estimate-api.ts index 4a8c91adb1..663c605b11 100644 --- a/frontend/apps/artcraft-webapp/src/lib/cost-estimate-api.ts +++ b/frontend/apps/artcraft-webapp/src/lib/cost-estimate-api.ts @@ -117,6 +117,7 @@ export interface VideoCostParams { model: string; aspectRatio?: string; resolution?: string | null; + bitrate?: string | null; duration?: number | null; numVideos?: number; hasStartFrame: boolean; @@ -142,6 +143,7 @@ export function useVideoCostEstimate(params: VideoCostParams): number | null { model: params.model, aspect_ratio: params.aspectRatio ?? null, resolution: params.resolution ?? null, + bitrate: params.bitrate ?? null, duration_seconds: params.duration ?? null, generate_audio: params.generateAudio ?? null, video_batch_count: params.numVideos ?? 1, @@ -178,6 +180,7 @@ export function useVideoCostEstimate(params: VideoCostParams): number | null { params.model, params.aspectRatio, params.resolution, + params.bitrate, params.duration, params.numVideos, params.hasStartFrame, diff --git a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts index f0721ebf4e..8ae2c08af4 100644 --- a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts +++ b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video-store.ts @@ -33,6 +33,7 @@ export type VideoUiState = { selectedSize: string; duration: number | null; resolution: string | null; + bitrate: string | null; generateWithSound: boolean; inputMode: VideoInputMode; numVideos: number; @@ -69,6 +70,7 @@ const DEFAULT_UI: VideoUiState = { selectedSize: "wide_sixteen_by_nine", duration: null, resolution: null, + bitrate: null, generateWithSound: false, inputMode: "keyframe", numVideos: 1, diff --git a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx index 48cbc026e8..515b8f524d 100644 --- a/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/create-video/create-video.tsx @@ -121,6 +121,15 @@ const LABEL_TO_RES: Record = Object.fromEntries( Object.entries(RES_LABELS).map(([k, v]) => [v, k]), ); +const BITRATE_LABELS: Record = { + normal: "Normal", + high: "High", +}; + +const LABEL_TO_BITRATE: Record = Object.fromEntries( + Object.entries(BITRATE_LABELS).map(([k, v]) => [v, k]), +); + // ── Model lookup ───────────────────────────────────────────────────────── // Placeholder entries shown in the picker as disabled "SOON" items. These are @@ -294,6 +303,16 @@ export default function CreateVideo() { (v: string | null) => setUi({ resolution: v }), [setUi], ); + const bitrate = + resolveModelOption( + ui.bitrate ?? undefined, + selectedModel?.bitrate_options, + selectedModel?.bitrate_default, + ) ?? null; + const setBitrate = useCallback( + (v: string | null) => setUi({ bitrate: v }), + [setUi], + ); const generateWithSound = ui.generateWithSound; const numVideos = resolveModelCount( ui.numVideos, @@ -386,6 +405,8 @@ export default function CreateVideo() { const hasSizeOptions = (selectedModel?.aspect_ratio_options?.length ?? 0) > 0; const hasResolutionOptions = (selectedModel?.resolution_options?.length ?? 0) > 0; + const hasBitrateOptions = + (selectedModel?.bitrate_options?.length ?? 0) > 0; const hasSound = !!selectedModel?.show_generate_with_sound_toggle; const supportsImagePrompts = !!selectedModel?.starting_keyframe_supported || @@ -463,6 +484,7 @@ export default function CreateVideo() { model: selectedModel?.model ?? "", aspectRatio: selectedSize, resolution, + bitrate, duration: effectiveDuration, numVideos, hasStartFrame: !isReferenceMode && referenceImages.length > 0, @@ -579,6 +601,16 @@ export default function CreateVideo() { : null, [selectedModel, resolution], ); + const bitrateItems = useMemo( + (): PopoverItem[] | null => + selectedModel?.bitrate_options + ? selectedModel.bitrate_options.map((b) => ({ + label: BITRATE_LABELS[b] ?? b, + selected: b === (bitrate ?? selectedModel.bitrate_default), + })) + : null, + [selectedModel, bitrate], + ); const inputModeItems = useMemo( (): PopoverItem[] | null => supportsRefMode @@ -736,6 +768,12 @@ export default function CreateVideo() { [setResolution], ); + const handleBitrateChange = useCallback( + (item: PopoverItem) => + setBitrate(LABEL_TO_BITRATE[item.label] ?? item.label), + [setBitrate], + ); + const handleInputModeChange = useCallback( (item: PopoverItem) => { const mode = item.label === "Reference" ? "reference" : "keyframe"; @@ -905,6 +943,9 @@ export default function CreateVideo() { resolution: hasResolutionOptions ? (resolution ?? selectedModel.resolution_default ?? undefined) : undefined, + bitrate: hasBitrateOptions + ? (bitrate ?? selectedModel.bitrate_default ?? undefined) + : undefined, generateAudio: hasSound ? generateWithSound : undefined, startFrameImageMediaToken: startFrameToken?.length ? startFrameToken @@ -998,8 +1039,10 @@ export default function CreateVideo() { numVideos, effectiveDuration, resolution, + bitrate, generateWithSound, hasResolutionOptions, + hasBitrateOptions, hasSound, supportsImagePrompts, hasEndFrame, @@ -1018,7 +1061,12 @@ export default function CreateVideo() { // ── Mobile form ─────────────────────────────────────────────────────── const activeResolutionLabel = resolutionItems?.find((i) => i.selected)?.label; - const outputSummary = [`${effectiveDuration}s`, activeResolutionLabel] + const activeBitrateLabel = bitrateItems?.find((i) => i.selected)?.label; + const outputSummary = [ + `${effectiveDuration}s`, + activeResolutionLabel, + activeBitrateLabel, + ] .filter(Boolean) .join(" · "); @@ -1147,7 +1195,7 @@ export default function CreateVideo() { /> )} - {(durationRange || resolutionItems) && ( + {(durationRange || resolutionItems || bitrateItems) && ( <> )} + {bitrateItems && ( + + + + )} )} @@ -1431,6 +1487,21 @@ export default function CreateVideo() { /> )} + {bitrateItems && ( + + + + )} {durationRange && (