@ -85,6 +85,7 @@ interface ComposerAdapter<TData extends WorkflowNodeData> {
const ASPECT_RATIOS : AspectRatio [ ] = [ "1:1" , "3:4" , "4:3" , "9:16" , "16:9" , "21:9" ] ;
const ASPECT_RATIOS : AspectRatio [ ] = [ "1:1" , "3:4" , "4:3" , "9:16" , "16:9" , "21:9" ] ;
const RESOLUTIONS : Resolution [ ] = [ "1K" , "2K" , "4K" ] ;
const RESOLUTIONS : Resolution [ ] = [ "1K" , "2K" , "4K" ] ;
const VIDEO_DURATION_QUICK_OPTIONS = [ 4 , 5 , 8 , 10 , 15 ] ;
const IMAGE_COUNTS : ImageGenerationCount [ ] = [ 1 , 2 , 4 ] ;
const IMAGE_COUNTS : ImageGenerationCount [ ] = [ 1 , 2 , 4 ] ;
const MAX_REFERENCE_IMAGES = 4 ;
const MAX_REFERENCE_IMAGES = 4 ;
const MAX_REFERENCE_IMAGE_BYTES = 10 * 1024 * 1024 ;
const MAX_REFERENCE_IMAGE_BYTES = 10 * 1024 * 1024 ;
@ -645,6 +646,12 @@ export function GenerationComposer() {
? inferCapabilityFromModel ( draft . selectedModel ) ? ? "image"
? inferCapabilityFromModel ( draft . selectedModel ) ? ? "image"
: context . capability ;
: context . capability ;
const videoSoundSupported = activeCapability === "video" && modelSupportsVideoSound ( draft . selectedModel ) ;
const videoSoundSupported = activeCapability === "video" && modelSupportsVideoSound ( draft . selectedModel ) ;
const videoDurationOptions = useMemo ( ( ) = > {
const duration = normalizeVideoDurationSeconds ( draft . durationSeconds ) ;
return VIDEO_DURATION_QUICK_OPTIONS . includes ( duration )
? VIDEO_DURATION_QUICK_OPTIONS
: [ . . . VIDEO_DURATION_QUICK_OPTIONS , duration ] . sort ( ( a , b ) = > a - b ) ;
} , [ draft . durationSeconds ] ) ;
const canChooseModel = context . mode === "empty-create" || context . mode === "node-edit" ;
const canChooseModel = context . mode === "empty-create" || context . mode === "node-edit" ;
const modelIsValid = context . mode === "empty-create"
const modelIsValid = context . mode === "empty-create"
? Boolean ( inferNodeTypeFromModel ( draft . selectedModel ) )
? Boolean ( inferNodeTypeFromModel ( draft . selectedModel ) )
@ -759,6 +766,48 @@ export function GenerationComposer() {
} ) ;
} ) ;
} , [ ] ) ;
} , [ ] ) ;
const updateVideoQuickDraft = useCallback (
( patch : Partial < ComposerDraft > , fields : DraftField [ ] ) = > {
const activeContext = contextRef . current ;
setDraft ( ( current ) = > {
const next = { . . . current , . . . patch } ;
draftRef . current = next ;
return next ;
} ) ;
if ( activeContext . mode === "node-edit" && activeContext . node ? . type === "generateVideo" ) {
const nodePatch : Partial < GenerateVideoNodeData > = { } ;
if ( fields . includes ( "durationSeconds" ) && patch . durationSeconds !== undefined ) {
nodePatch . durationSeconds = normalizeVideoDurationSeconds ( patch . durationSeconds ) ;
}
if ( fields . includes ( "resolution" ) && patch . resolution !== undefined ) {
nodePatch . resolution = patch . resolution ;
}
if ( fields . includes ( "soundEnabled" ) && Object . prototype . hasOwnProperty . call ( patch , "soundEnabled" ) ) {
nodePatch . soundEnabled = patch . soundEnabled ;
}
if ( Object . keys ( nodePatch ) . length > 0 ) {
updateNodeData ( activeContext . node . id , nodePatch ) ;
}
setDirtyFields ( ( current ) = > {
const next = new Set ( current ) ;
fields . forEach ( ( field ) = > next . delete ( field ) ) ;
dirtyFieldsRef . current = next ;
return next ;
} ) ;
return ;
}
setDirtyFields ( ( current ) = > {
const next = new Set ( current ) ;
fields . forEach ( ( field ) = > next . add ( field ) ) ;
dirtyFieldsRef . current = next ;
return next ;
} ) ;
} ,
[ updateNodeData ]
) ;
const handleComposerBlur = useCallback (
const handleComposerBlur = useCallback (
( event : FocusEvent < HTMLElement > ) = > {
( event : FocusEvent < HTMLElement > ) = > {
const nextTarget = event . relatedTarget as Node | null ;
const nextTarget = event . relatedTarget as Node | null ;
@ -1553,11 +1602,11 @@ export function GenerationComposer() {
aria - label = { t ( "node.duration" ) }
aria - label = { t ( "node.duration" ) }
value = { draft . durationSeconds }
value = { draft . durationSeconds }
onChange = { ( event ) = >
onChange = { ( event ) = >
mar kDraft( { durationSeconds : normalizeVideoDurationSeconds ( event . target . value ) } , [ "durationSeconds" ] )
updateVideoQuic kDraft( { durationSeconds : normalizeVideoDurationSeconds ( event . target . value ) } , [ "durationSeconds" ] )
}
}
className = "rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60"
className = "rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60"
>
>
{ [ 4 , 5 , 8 , 10 , 15 ] . map ( ( seconds ) = > (
{ videoDurationOptions . map ( ( seconds ) = > (
< option key = { seconds } value = { seconds } className = "bg-neutral-900" >
< option key = { seconds } value = { seconds } className = "bg-neutral-900" >
{ t ( "node.seconds" , { count : seconds } ) }
{ t ( "node.seconds" , { count : seconds } ) }
< / option >
< / option >
@ -1567,7 +1616,7 @@ export function GenerationComposer() {
< select
< select
aria - label = { t ( "node.resolution" ) }
aria - label = { t ( "node.resolution" ) }
value = { draft . resolution }
value = { draft . resolution }
onChange = { ( event ) = > mar kDraft( { resolution : event.target.value } , [ "resolution" ] ) }
onChange = { ( event ) = > updateVideoQuic kDraft( { resolution : event.target.value } , [ "resolution" ] ) }
className = "rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60"
className = "rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60"
>
>
{ VIDEO_RESOLUTION_OPTIONS . map ( ( item ) = > (
{ VIDEO_RESOLUTION_OPTIONS . map ( ( item ) = > (
@ -1582,7 +1631,7 @@ export function GenerationComposer() {
type = "button"
type = "button"
role = "switch"
role = "switch"
aria - checked = { draft . soundEnabled ? ? true }
aria - checked = { draft . soundEnabled ? ? true }
onClick = { ( ) = > mar kDraft( { soundEnabled : ! ( draft . soundEnabled ? ? true ) } , [ "soundEnabled" ] ) }
onClick = { ( ) = > updateVideoQuic kDraft( { soundEnabled : ! ( draft . soundEnabled ? ? true ) } , [ "soundEnabled" ] ) }
className = { ` rounded-md px-1.5 py-1 transition-colors ${
className = { ` rounded-md px-1.5 py-1 transition-colors ${
( draft . soundEnabled ? ? true )
( draft . soundEnabled ? ? true )
? "bg-cyan-500/15 text-cyan-200"
? "bg-cyan-500/15 text-cyan-200"