Files

129 lines
4.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useState } from 'react'
import { ApiClient } from '../lib/api-client'
import { canCancelLocalJob, withGenerationActive } from '../lib/generation-active'
import { logger } from '../lib/logger'
import { prepareGenerationInput } from '../lib/generation-input'
import { useAppSettings } from '../contexts/AppSettingsContext'
import type { RetakeExtendModel } from './use-retake'
export type ExtendDirection = 'start' | 'end'
// Seconds-to-add presets (matches LTX Studio). API allows 2–20s.
export const EXTEND_SECONDS = [4, 6, 8, 10, 12] as const
export const DEFAULT_EXTEND_SECONDS = 4
export interface ExtendSubmitParams {
videoPath: string
duration: number
prompt: string
mode: ExtendDirection
resolution?: { width: number; height: number }
model: RetakeExtendModel
}
export interface ExtendResult {
videoPath: string
}
interface UseExtendState {
isExtending: boolean
canCancel: boolean
extendStatus: string
extendError: string | null
result: ExtendResult | null
}
export function useExtend() {
const { shouldVideoGenerateWithLtxApi, shouldImageGenerateWithFalApi } = useAppSettings()
const [state, setState] = useState<UseExtendState>({
isExtending: false,
canCancel: false,
extendStatus: '',
extendError: null,
result: null,
})
const submitExtend = useCallback(async (params: ExtendSubmitParams) => {
if (!params.videoPath) return
setState({
isExtending: true,
canCancel: canCancelLocalJob('video', shouldVideoGenerateWithLtxApi, shouldImageGenerateWithFalApi),
extendStatus: 'Generating',
extendError: null,
result: null,
})
await withGenerationActive(async () => {
let preparedVideoPath: string
try {
preparedVideoPath = (await prepareGenerationInput(params.videoPath)) ?? params.videoPath
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to prepare source video'
logger.error(`Extend input error: ${message}`)
setState({ isExtending: false, canCancel: false, extendStatus: '', extendError: message, result: null })
return
}
const result = await ApiClient.extend({
video_path: preparedVideoPath,
duration: params.duration,
prompt: params.prompt,
mode: params.mode,
resolution: params.resolution,
model: params.model,
})
if (!result.ok) {
logger.error(`Extend error: ${result.error.message}`)
setState({ isExtending: false, canCancel: false, extendStatus: '', extendError: result.error.message, result: null })
return
}
const payload = result.data
if (payload.status === 'cancelled') {
setState({ isExtending: false, canCancel: false, extendStatus: 'Cancelled', extendError: null, result: null })
return
}
if ('video_path' in payload) {
setState({
isExtending: false,
canCancel: false,
extendStatus: 'Extend complete!',
extendError: null,
result: { videoPath: payload.video_path },
})
return
}
// A 200 with a remote payload and no local file is a legitimate success (the backend
// completed the generation); there's just no local artifact to import. Don't surface
// it as an error.
logger.warn(`Extend completed with a remote payload and no local file: ${JSON.stringify(payload.result)}`)
setState({
isExtending: false,
canCancel: false,
extendStatus: 'Extend complete!',
extendError: null,
result: null,
})
})
}, [shouldImageGenerateWithFalApi, shouldVideoGenerateWithLtxApi])
const resetExtend = useCallback(() => {
setState({ isExtending: false, canCancel: false, extendStatus: '', extendError: null, result: null })
}, [])
return {
submitExtend,
resetExtend,
isExtending: state.isExtending,
canCancel: state.canCancel,
extendStatus: state.extendStatus,
extendError: state.extendError,
extendResult: state.result,
}
}