174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import { ApiClient, type ApiRequestBodyOf } from '../lib/api-client'
|
|
import { withGenerationActive } from '../lib/generation-active'
|
|
import { logger } from '../lib/logger'
|
|
|
|
export type IcLoraConditioningType = 'canny' | 'depth' | 'custom'
|
|
export type IcLoraAudioMode = 'source' | 'generated' | 'off'
|
|
|
|
export interface IcLoraSubmitParams {
|
|
videoPath: string
|
|
conditioningType: IcLoraConditioningType
|
|
conditioningStrength: number
|
|
prompt: string
|
|
// "custom": the user's own IC-LoRA weights + a pre-rendered control video.
|
|
customLoraRef?: string
|
|
controlVideoPath?: string
|
|
// Skip Stage 2 refine — transformation IC-LoRAs need this; default off.
|
|
skipStage2?: boolean
|
|
// Keep the IC-LoRA active during Stage 2 refine (only when Stage 2 runs). Default off.
|
|
// On: the transformation survives refinement and the output lands at the chosen resolution.
|
|
useLoraInStage2?: boolean
|
|
// Target output resolution for the useLoraInStage2 path (undefined = source). Backend
|
|
// snaps it down to a valid size and never upscales above source.
|
|
resolution?: { width: number; height: number }
|
|
// Stage-1-only canvas multiplier: 2.0 = native, 1.0 = half. Only used when skipStage2.
|
|
resolutionFactor?: number
|
|
// Soundtrack: source (input clip) / generated (prompt) / off.
|
|
audioMode?: IcLoraAudioMode
|
|
// LoRA adapter merge weight (load-time). Default 1.0.
|
|
loraStrength?: number
|
|
// Override control-video fps (decimate to fewer frames). undefined = source fps.
|
|
fpsOverride?: number
|
|
// IC-LoRA mode: catalog IC-LoRA id + the user's input media path + output duration.
|
|
// When icLoraId is set, the backend resolves catalog weights and builds the
|
|
// control video via the IC-LoRA's preprocessing pipeline.
|
|
icLoraId?: string
|
|
// Catalog download.variants[].id when the entry has multiple checkpoints.
|
|
variantId?: string
|
|
inputPath?: string
|
|
// Values for the IC-LoRA's declared controls, keyed by control id (e.g. duration).
|
|
// Sent verbatim; the backend validates + maps them to behaviour.
|
|
controlValues?: Record<string, number | string>
|
|
// Outpainting (position_canvas control): per-edge pixels to add around the source.
|
|
outpaintPads?: { left: number; right: number; top: number; bottom: number }
|
|
// When true, an empty prompt is allowed (set from the IC-LoRA's allows_empty_prompt).
|
|
allowEmptyPrompt?: boolean
|
|
// Optional reference image for catalog IC-LoRAs that opt in (allows_reference_image).
|
|
// Sent as image conditioning at frame 0, strength 1.0.
|
|
referenceImagePath?: string
|
|
}
|
|
|
|
export interface IcLoraResult {
|
|
videoPath: string
|
|
}
|
|
|
|
interface UseIcLoraState {
|
|
isGenerating: boolean
|
|
canCancel: boolean
|
|
status: string
|
|
error: string | null
|
|
result: IcLoraResult | null
|
|
}
|
|
|
|
type GenerateIcLoraBody = ApiRequestBodyOf<'generateIcLora'>
|
|
|
|
export function useIcLora() {
|
|
const [state, setState] = useState<UseIcLoraState>({
|
|
isGenerating: false,
|
|
canCancel: false,
|
|
status: '',
|
|
error: null,
|
|
result: null,
|
|
})
|
|
|
|
const submitIcLora = useCallback(async (params: IcLoraSubmitParams) => {
|
|
if (!params.prompt.trim() && !params.allowEmptyPrompt) return
|
|
// IC-LoRA mode needs inputPath; canny/depth/custom need videoPath.
|
|
if (!params.icLoraId && !params.videoPath) return
|
|
if (params.icLoraId && !params.inputPath) return
|
|
|
|
setState({
|
|
isGenerating: true,
|
|
// IC-LoRA is always local GPU (the tab is hidden when forceApiGenerations).
|
|
// Do not gate on shouldVideoGenerateWithLtxApi — that flag is for t2v/i2v.
|
|
canCancel: true,
|
|
status: 'Generating',
|
|
error: null,
|
|
result: null,
|
|
})
|
|
|
|
await withGenerationActive(async () => {
|
|
const result = await ApiClient.generateIcLora({
|
|
video_path: params.videoPath,
|
|
conditioning_type: params.conditioningType,
|
|
conditioning_strength: params.conditioningStrength,
|
|
prompt: params.prompt,
|
|
custom_lora_ref: params.customLoraRef,
|
|
control_video_path: params.controlVideoPath,
|
|
skip_stage_2: params.skipStage2,
|
|
use_lora_in_stage_2: params.useLoraInStage2,
|
|
resolution: params.resolution,
|
|
resolution_factor: params.resolutionFactor,
|
|
audio_mode: params.audioMode,
|
|
lora_strength: params.loraStrength,
|
|
fps_override: params.fpsOverride,
|
|
ic_lora_id: params.icLoraId,
|
|
variant_id: params.variantId,
|
|
input_path: params.inputPath,
|
|
control_values: params.controlValues,
|
|
outpaint_pads: params.outpaintPads,
|
|
images: params.referenceImagePath
|
|
? [{ path: params.referenceImagePath, frame: 0, strength: 1.0 }]
|
|
: [],
|
|
} as GenerateIcLoraBody)
|
|
if (!result.ok) {
|
|
logger.error(`IC-LoRA error: ${result.error.message}`)
|
|
setState({
|
|
isGenerating: false,
|
|
canCancel: false,
|
|
status: '',
|
|
error: result.error.message,
|
|
result: null,
|
|
})
|
|
return
|
|
}
|
|
|
|
const payload = result.data
|
|
if (payload.status === 'cancelled') {
|
|
setState({
|
|
isGenerating: false,
|
|
canCancel: false,
|
|
status: 'Cancelled',
|
|
error: null,
|
|
result: null,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (payload.status === 'complete') {
|
|
setState({
|
|
isGenerating: false,
|
|
canCancel: false,
|
|
status: 'Generation complete!',
|
|
error: null,
|
|
result: {
|
|
videoPath: payload.video_path,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
})
|
|
}, [])
|
|
|
|
const reset = useCallback(() => {
|
|
setState({
|
|
isGenerating: false,
|
|
canCancel: false,
|
|
status: '',
|
|
error: null,
|
|
result: null,
|
|
})
|
|
}, [])
|
|
|
|
return {
|
|
submitIcLora,
|
|
resetIcLora: reset,
|
|
isIcLoraGenerating: state.isGenerating,
|
|
canCancel: state.canCancel,
|
|
icLoraStatus: state.status,
|
|
icLoraError: state.error,
|
|
icLoraResult: state.result,
|
|
}
|
|
}
|