diff --git a/frontend/apps/artcraft-webapp/src/components/prompt-box/MediaReferenceRow.tsx b/frontend/apps/artcraft-webapp/src/components/prompt-box/MediaReferenceRow.tsx index 86926fc2c..73917bbfb 100644 --- a/frontend/apps/artcraft-webapp/src/components/prompt-box/MediaReferenceRow.tsx +++ b/frontend/apps/artcraft-webapp/src/components/prompt-box/MediaReferenceRow.tsx @@ -21,6 +21,8 @@ import { } from "./upload-media"; interface MediaReferenceRowProps { + videoSupported: boolean; + audioSupported: boolean; referenceVideos: RefVideo[]; onReferenceVideosChange: (videos: RefVideo[]) => void; maxVideoCount: number; @@ -33,6 +35,8 @@ interface MediaReferenceRowProps { } export const MediaReferenceRow = ({ + videoSupported, + audioSupported, referenceVideos, onReferenceVideosChange, maxVideoCount, @@ -66,9 +70,7 @@ export const MediaReferenceRow = ({ totalAudioDuration < maxAudioRefDuration && !uploadingAudio; - const handleVideoUpload = async ( - e: React.ChangeEvent, - ) => { + const handleVideoUpload = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (videoInputRef.current) videoInputRef.current.value = ""; if (files.length === 0) return; @@ -123,9 +125,7 @@ export const MediaReferenceRow = ({ } }; - const handleAudioUpload = async ( - e: React.ChangeEvent, - ) => { + const handleAudioUpload = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (audioInputRef.current) audioInputRef.current.value = ""; if (files.length === 0) return; @@ -214,101 +214,107 @@ export const MediaReferenceRow = ({ onPointerDown={(e) => e.stopPropagation()} > {/* Video section */} -
-
-
- - - Video Ref - - ({referenceVideos.length}/{maxVideoCount}) + {videoSupported && ( +
+
+
+ + + Video Ref + + ({referenceVideos.length}/{maxVideoCount}) + +
+ + {totalVideoDuration}/{maxVideoRefDuration}s
- - {totalVideoDuration}/{maxVideoRefDuration}s - -
-
- {referenceVideos.map((video) => ( - - ))} - {uploadingVideo && ( -
- + {referenceVideos.map((video) => ( + -
- )} - {canAddVideo && ( - - )} + ))} + {uploadingVideo && ( +
+ +
+ )} + {canAddVideo && ( + + )} +
-
+ )} -
+ {videoSupported && audioSupported && ( +
+ )} {/* Audio section */} -
-
-
- - - Audio Ref - - ({referenceAudios.length}/{maxAudioCount}) + {audioSupported && ( +
+
+
+ + + Audio Ref + + ({referenceAudios.length}/{maxAudioCount}) + +
+ + {totalAudioDuration}/{maxAudioRefDuration}s
- - {totalAudioDuration}/{maxAudioRefDuration}s - -
-
- {referenceAudios.map((audio, i) => ( - - ))} - {uploadingAudio && ( -
- + {referenceAudios.map((audio, i) => ( + -
- )} - {canAddAudio && ( - - )} + ))} + {uploadingAudio && ( +
+ +
+ )} + {canAddAudio && ( + + )} +
-
+ )}
); diff --git a/frontend/apps/artcraft-webapp/src/lib/omni-gen-hooks.ts b/frontend/apps/artcraft-webapp/src/lib/omni-gen-hooks.ts index 8fda34aa8..d65fd5b05 100644 --- a/frontend/apps/artcraft-webapp/src/lib/omni-gen-hooks.ts +++ b/frontend/apps/artcraft-webapp/src/lib/omni-gen-hooks.ts @@ -176,6 +176,7 @@ const MODEL_DISPLAY_NAMES: Record = { seedream_5_lite: "Seedream 5 Lite", // Video models grok_video: "Grok Video", + grok_imagine_video: "Grok Imagine", kling_1p6_pro: "Kling 1.6 Pro", kling_2p1_pro: "Kling 2.1 Pro", kling_2p1_master: "Kling 2.1 Master", 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 e70a32430..1be5ac3b8 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 @@ -159,22 +159,33 @@ function buildSizePopoverItems( })); } +// Effective max duration for the active input mode. Some models (e.g. Grok) +// cap duration lower when image references are used — that's reference mode here. +function maxDurationForMode( + model: OmniGenVideoModelInfo, + isReferenceMode: boolean, +): number | null { + const imageRefMax = model.duration_seconds_max_with_image_references; + return isReferenceMode && imageRefMax != null + ? imageRefMax + : model.duration_seconds_max; +} + function resolveDurationForModel( model: OmniGenVideoModelInfo, current: number | null, + isReferenceMode: boolean, ): number | null { if (current == null) return model.duration_seconds_default ?? null; - if ( - model.duration_seconds_min != null && - model.duration_seconds_max != null - ) { - if ( - current >= model.duration_seconds_min && - current <= model.duration_seconds_max - ) { + const max = maxDurationForMode(model, isReferenceMode); + if (model.duration_seconds_min != null && max != null) { + if (current >= model.duration_seconds_min && current <= max) { return current; } - return model.duration_seconds_default ?? model.duration_seconds_min; + return Math.min( + model.duration_seconds_default ?? model.duration_seconds_min, + max, + ); } if (model.duration_seconds_options?.length) { if (model.duration_seconds_options.includes(current)) return current; @@ -316,10 +327,12 @@ export default function CreateVideo() { !!selectedModel?.starting_keyframe_supported || !!selectedModel?.starting_keyframe_required || !!selectedModel?.image_references_supported; + const supportsVideoRefs = !!selectedModel?.video_references_supported; + const supportsAudioRefs = !!selectedModel?.audio_references_supported; const supportsRefMode = !!selectedModel?.image_references_supported || - !!selectedModel?.video_references_supported || - !!selectedModel?.audio_references_supported; + supportsVideoRefs || + supportsAudioRefs; const inputMode = ui.inputMode; const isReferenceMode = supportsRefMode && inputMode === "reference"; const hasEndFrame = !!( @@ -441,15 +454,13 @@ export default function CreateVideo() { ); const durationRange = useMemo((): { min: number; max: number } | null => { if (!selectedModel) return null; + const max = maxDurationForMode(selectedModel, isReferenceMode); if ( selectedModel.duration_seconds_min != null && - selectedModel.duration_seconds_max != null && - selectedModel.duration_seconds_max > selectedModel.duration_seconds_min + max != null && + max > selectedModel.duration_seconds_min ) { - return { - min: selectedModel.duration_seconds_min, - max: selectedModel.duration_seconds_max, - }; + return { min: selectedModel.duration_seconds_min, max }; } if ( selectedModel.duration_seconds_options && @@ -461,7 +472,7 @@ export default function CreateVideo() { return { min: opts[0]!, max: opts[opts.length - 1]! }; } return null; - }, [selectedModel]); + }, [selectedModel, isReferenceMode]); const effectiveDuration = duration ?? selectedModel?.duration_seconds_default ?? 5; const [localDuration, setLocalDuration] = useState(effectiveDuration); @@ -594,10 +605,6 @@ export default function CreateVideo() { const model = item.action ? _modelLookup.get(item.action) : undefined; if (!model) return; const currentState = useCreateVideoStore.getState().ui; - const nextDuration = resolveDurationForModel( - model, - currentState.duration, - ); const newSupportsKeyframe = !!model.starting_keyframe_supported || @@ -612,6 +619,12 @@ export default function CreateVideo() { ? "reference" : "keyframe"; + const nextDuration = resolveDurationForModel( + model, + currentState.duration, + nextInputMode === "reference", + ); + setUi({ selectedModelId: model.model, selectedSize: model.aspect_ratio_default ?? "wide_sixteen_by_nine", @@ -659,15 +672,22 @@ export default function CreateVideo() { (item: PopoverItem) => { const mode = item.label === "Reference" ? "reference" : "keyframe"; if (mode === inputMode) return; - setUi({ inputMode: mode }); if (mode === "reference") { + // Some models (e.g. Grok) cap duration lower in image-reference mode. + const cap = selectedModel?.duration_seconds_max_with_image_references; + const clamped = cap != null && (duration ?? 0) > cap ? cap : undefined; + setUi({ + inputMode: mode, + ...(clamped != null ? { duration: clamped } : {}), + }); setEndFrameImage(undefined); } else { + setUi({ inputMode: mode }); setReferenceVideos([]); setReferenceAudios([]); } }, - [inputMode, setUi], + [inputMode, duration, selectedModel, setUi], ); const imagePickerMax = Math.max( @@ -1030,8 +1050,10 @@ export default function CreateVideo() { } mentionItems={mentionItems.length > 0 ? mentionItems : undefined} mediaReferenceRow={ - isReferenceMode ? ( + isReferenceMode && (supportsVideoRefs || supportsAudioRefs) ? ( { - return Object.fromEntries( - Object.entries(obj).filter(([, v]) => v != null), - ); + return Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null)); } // ── API class ──────────────────────────────────────────────────────────── diff --git a/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts b/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts index 4e6f268f4..b7bb56783 100644 --- a/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts +++ b/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts @@ -68,6 +68,7 @@ export const MODEL_TYPE_TO_CREATOR: Record = { marble_0p1_plus: ModelCreator.WorldLabs, grok_image: ModelCreator.Grok, grok_video: ModelCreator.Grok, + grok_imagine_video: ModelCreator.Grok, midjourney: ModelCreator.Midjourney, midjourney_v6: ModelCreator.Midjourney, midjourney_v6p1: ModelCreator.Midjourney, @@ -155,6 +156,7 @@ export const getModelDisplayName = (modelType: string): string => { // Grok grok_image: "Grok Image", grok_video: "Grok Video", + grok_imagine_video: "Grok Imagine", // Flux — Black Forest Labs flux_1_dev: "Flux 1 Dev",