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({ 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, } }