Create standalone browser port of LTX Athena with authenticated native API transport

This commit is contained in:
Mikei386
2026-09-29 17:59:23 +02:00
commit 1333b0c36b
202 changed files with 63054 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
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,
}
}