Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
commit
1333b0c36b
202 files changed
+63054
No files matched your search
@@ -0,0 +1,315 @@
|
||||
import type { components } from '../generated/backend-openapi'
|
||||
|
||||
export type VideoGenerationModelSpecsResponse = components['schemas']['GenerateVideoModelsSpecsResponse']
|
||||
export type VideoGenerationModelSpecItem = components['schemas']['LTXVideoGenerationModelSpecItem']
|
||||
export type VideoGenerationResolutionSpec = components['schemas']['LTXVideoGenerationResolutionSpec']
|
||||
export type VideoGenerationOfferingCapabilities = NonNullable<
|
||||
VideoGenerationModelSpecItem['spec']['capabilities']
|
||||
>
|
||||
export type VideoGenerationPipeline = components['schemas']['GenerateVideoRequest']['model']
|
||||
export type VideoGenerationResolution = components['schemas']['GenerateVideoRequest']['resolution']
|
||||
export type VideoGenerationDuration = Exclude<
|
||||
components['schemas']['GenerateVideoRequest']['duration'],
|
||||
null
|
||||
>
|
||||
export type VideoGenerationFps = components['schemas']['GenerateVideoRequest']['fps']
|
||||
export type VideoGenerationAspectRatio = components['schemas']['GenerateVideoRequest']['aspectRatio']
|
||||
|
||||
export interface VideoGenerationSettingsShape {
|
||||
model: string
|
||||
duration: number | null
|
||||
videoResolution: string
|
||||
fps: number
|
||||
aspectRatio?: string
|
||||
audio?: boolean
|
||||
}
|
||||
|
||||
export interface ResolvedVideoGenerationOptions {
|
||||
modelOptions: VideoGenerationModelSpecItem[]
|
||||
resolutionOptions: VideoGenerationResolution[]
|
||||
fpsOptions: VideoGenerationFps[]
|
||||
durationOptions: VideoGenerationDuration[]
|
||||
selectedModel: VideoGenerationPipeline | null
|
||||
selectedResolution: VideoGenerationResolution | null
|
||||
selectedFps: VideoGenerationFps | null
|
||||
selectedDuration: VideoGenerationDuration | null
|
||||
autoDurationAvailable: boolean
|
||||
hasCompatibleOptions: boolean
|
||||
}
|
||||
|
||||
type DurationSelectionMode = 'preserve' | 'smallest_valid'
|
||||
|
||||
/** GenSpace picker floor. The API envelope includes 2–5s so gap fill can request shorts. */
|
||||
export const GENSPACE_MIN_SELECTABLE_DURATION_S = 6
|
||||
|
||||
interface ResolveVideoGenerationOptionsParams<T extends VideoGenerationSettingsShape> {
|
||||
settings: T
|
||||
modelSpecs: VideoGenerationModelSpecItem[]
|
||||
hasAudio?: boolean
|
||||
minimumDuration?: number
|
||||
durationSelection?: DurationSelectionMode
|
||||
}
|
||||
|
||||
function getResolutionMap(
|
||||
item: VideoGenerationModelSpecItem,
|
||||
options: { hasAudio: boolean },
|
||||
): Record<string, VideoGenerationResolutionSpec> {
|
||||
const { hasAudio } = options
|
||||
if (!hasAudio) {
|
||||
return item.spec.supported_resolutions_durations
|
||||
}
|
||||
// A model with no a2v spec doesn't support audio-conditioned generation at all —
|
||||
// must not fall back to the plain (non-a2v) matrix, or it looks compatible when it isn't.
|
||||
return item.spec.a2v_supported_resolutions_durations ?? {}
|
||||
}
|
||||
|
||||
function getResolutionEntries(
|
||||
item: VideoGenerationModelSpecItem,
|
||||
options: { hasAudio: boolean },
|
||||
): Array<[VideoGenerationResolution, VideoGenerationResolutionSpec]> {
|
||||
return Object.entries(getResolutionMap(item, options)).map(([resolution, spec]) => [
|
||||
resolution as VideoGenerationResolution,
|
||||
spec,
|
||||
])
|
||||
}
|
||||
|
||||
function getDurationsForFps(
|
||||
resolutionSpec: VideoGenerationResolutionSpec,
|
||||
fps: VideoGenerationFps,
|
||||
): VideoGenerationDuration[] {
|
||||
return (resolutionSpec.fps_to_durations[String(fps)] ?? []) as VideoGenerationDuration[]
|
||||
}
|
||||
|
||||
function filterDurationsByMinimum(
|
||||
durations: VideoGenerationDuration[],
|
||||
minimumDuration: number | undefined,
|
||||
): VideoGenerationDuration[] {
|
||||
if (minimumDuration === undefined) return durations
|
||||
return durations.filter((duration) => duration >= minimumDuration)
|
||||
}
|
||||
|
||||
function getCompatibleFps(
|
||||
resolutionSpec: VideoGenerationResolutionSpec,
|
||||
options: { minimumDuration: number | undefined },
|
||||
): VideoGenerationFps[] {
|
||||
const { minimumDuration } = options
|
||||
return Object.keys(resolutionSpec.fps_to_durations).map((fps) => Number(fps) as VideoGenerationFps).filter((fps) => (
|
||||
filterDurationsByMinimum(getDurationsForFps(resolutionSpec, fps), minimumDuration).length > 0
|
||||
))
|
||||
}
|
||||
|
||||
function getCompatibleResolutionEntries(
|
||||
item: VideoGenerationModelSpecItem,
|
||||
options: { hasAudio: boolean; minimumDuration: number | undefined },
|
||||
): Array<[VideoGenerationResolution, VideoGenerationResolutionSpec]> {
|
||||
return getResolutionEntries(item, { hasAudio: options.hasAudio }).filter(([, resolutionSpec]) => (
|
||||
getCompatibleFps(resolutionSpec, { minimumDuration: options.minimumDuration }).length > 0
|
||||
))
|
||||
}
|
||||
|
||||
function getCompatibleModelOptions(
|
||||
modelSpecs: VideoGenerationModelSpecItem[],
|
||||
options: { hasAudio: boolean; minimumDuration: number | undefined },
|
||||
): VideoGenerationModelSpecItem[] {
|
||||
const { hasAudio, minimumDuration } = options
|
||||
// Always filter by resolution compatibility — hasAudio alone (independent of any
|
||||
// minimumDuration constraint) can exclude a model, e.g. a fast-tier pipeline with no
|
||||
// a2v spec. Skipping this whenever minimumDuration is unset used to let incompatible
|
||||
// (audio-unsupported) models stay selectable and get stuck with no valid resolution.
|
||||
return modelSpecs.filter((item) => (
|
||||
getCompatibleResolutionEntries(item, { hasAudio, minimumDuration }).length > 0
|
||||
))
|
||||
}
|
||||
|
||||
function emptyResolvedOptions(
|
||||
modelOptions: VideoGenerationModelSpecItem[],
|
||||
extras: Partial<ResolvedVideoGenerationOptions> = {},
|
||||
): ResolvedVideoGenerationOptions {
|
||||
return {
|
||||
modelOptions,
|
||||
resolutionOptions: [],
|
||||
fpsOptions: [],
|
||||
durationOptions: [],
|
||||
selectedModel: null,
|
||||
selectedResolution: null,
|
||||
selectedFps: null,
|
||||
selectedDuration: null,
|
||||
autoDurationAvailable: false,
|
||||
hasCompatibleOptions: false,
|
||||
...extras,
|
||||
}
|
||||
}
|
||||
|
||||
function chooseOption<T>(current: string | number | null, options: T[]): T | null {
|
||||
return options.find((option) => option === current) ?? options[0] ?? null
|
||||
}
|
||||
|
||||
export function getVideoGenerationModelSpecs(
|
||||
specs: VideoGenerationModelSpecsResponse | null | undefined,
|
||||
options: { useApiSpecs: boolean },
|
||||
): VideoGenerationModelSpecItem[] {
|
||||
const { useApiSpecs } = options
|
||||
if (!specs) return []
|
||||
return useApiSpecs ? specs.api_models : specs.local_models
|
||||
}
|
||||
|
||||
export function getLocalOfferingCapabilities(
|
||||
specs: VideoGenerationModelSpecsResponse | null | undefined,
|
||||
): VideoGenerationOfferingCapabilities | null {
|
||||
return specs?.local_models[0]?.spec.capabilities ?? null
|
||||
}
|
||||
|
||||
export function getApiOfferingCapabilities(
|
||||
specs: VideoGenerationModelSpecsResponse | null | undefined,
|
||||
pipeline: string | null | undefined,
|
||||
): VideoGenerationOfferingCapabilities | null {
|
||||
if (!specs || !pipeline) return null
|
||||
return specs.api_models.find((item) => item.pipeline === pipeline)?.spec.capabilities ?? null
|
||||
}
|
||||
|
||||
export function resolveVideoGenerationOptions<T extends VideoGenerationSettingsShape>({
|
||||
settings,
|
||||
modelSpecs,
|
||||
hasAudio = false,
|
||||
minimumDuration,
|
||||
durationSelection = 'preserve',
|
||||
}: ResolveVideoGenerationOptionsParams<T>): ResolvedVideoGenerationOptions {
|
||||
const modelOptions = getCompatibleModelOptions(modelSpecs, { hasAudio, minimumDuration })
|
||||
const selectedModelItem = modelOptions.find((item) => item.pipeline === settings.model) ?? modelOptions[0] ?? null
|
||||
if (!selectedModelItem) {
|
||||
return emptyResolvedOptions(modelOptions)
|
||||
}
|
||||
|
||||
const resolutionEntries = getCompatibleResolutionEntries(selectedModelItem, { hasAudio, minimumDuration })
|
||||
const resolutionOptions = resolutionEntries.map(([resolution]) => resolution)
|
||||
const selectedResolution = chooseOption(settings.videoResolution, resolutionOptions)
|
||||
if (!selectedResolution) {
|
||||
return emptyResolvedOptions(modelOptions, { selectedModel: selectedModelItem.pipeline, resolutionOptions })
|
||||
}
|
||||
|
||||
const selectedResolutionSpec = resolutionEntries.find(([resolution]) => resolution === selectedResolution)?.[1] ?? null
|
||||
if (!selectedResolutionSpec) {
|
||||
return emptyResolvedOptions(modelOptions, {
|
||||
selectedModel: selectedModelItem.pipeline,
|
||||
resolutionOptions,
|
||||
selectedResolution,
|
||||
})
|
||||
}
|
||||
|
||||
const fpsOptions = getCompatibleFps(selectedResolutionSpec, { minimumDuration })
|
||||
const selectedFps = chooseOption(settings.fps, fpsOptions)
|
||||
if (!selectedFps) {
|
||||
return emptyResolvedOptions(modelOptions, {
|
||||
selectedModel: selectedModelItem.pipeline,
|
||||
resolutionOptions,
|
||||
selectedResolution,
|
||||
fpsOptions,
|
||||
})
|
||||
}
|
||||
|
||||
const durationOptions = filterDurationsByMinimum(
|
||||
getDurationsForFps(selectedResolutionSpec, selectedFps),
|
||||
minimumDuration,
|
||||
)
|
||||
const autoDurationAvailable = !hasAudio && Boolean(selectedModelItem.spec.capabilities?.auto_duration)
|
||||
const selectedDuration = durationSelection === 'smallest_valid'
|
||||
? durationOptions[0] ?? null
|
||||
: autoDurationAvailable && settings.duration === null
|
||||
? null
|
||||
: chooseOption(settings.duration, durationOptions)
|
||||
|
||||
return {
|
||||
modelOptions,
|
||||
resolutionOptions,
|
||||
fpsOptions,
|
||||
durationOptions,
|
||||
selectedModel: selectedModelItem.pipeline,
|
||||
selectedResolution,
|
||||
selectedFps,
|
||||
selectedDuration,
|
||||
autoDurationAvailable,
|
||||
hasCompatibleOptions: selectedDuration !== null || autoDurationAvailable,
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeVideoGenerationSettings<T extends VideoGenerationSettingsShape>(
|
||||
settings: T,
|
||||
modelSpecs: VideoGenerationModelSpecItem[],
|
||||
options: {
|
||||
hasAudio?: boolean
|
||||
minimumDuration?: number
|
||||
durationSelection?: DurationSelectionMode
|
||||
} = {},
|
||||
): T | null {
|
||||
const resolved = resolveVideoGenerationOptions({
|
||||
settings,
|
||||
modelSpecs,
|
||||
hasAudio: options.hasAudio,
|
||||
minimumDuration: options.minimumDuration,
|
||||
durationSelection: options.durationSelection,
|
||||
})
|
||||
if (
|
||||
!resolved.hasCompatibleOptions
|
||||
|| !resolved.selectedModel
|
||||
|| !resolved.selectedResolution
|
||||
|| !resolved.selectedFps
|
||||
|| (resolved.selectedDuration === null && !resolved.autoDurationAvailable)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...settings,
|
||||
model: resolved.selectedModel,
|
||||
videoResolution: resolved.selectedResolution,
|
||||
fps: resolved.selectedFps,
|
||||
duration: resolved.selectedDuration,
|
||||
aspectRatio: (settings.aspectRatio === '9:16' ? '9:16' : '16:9') as VideoGenerationAspectRatio,
|
||||
}
|
||||
}
|
||||
|
||||
export function areVideoGenerationSettingsEquivalent<T extends VideoGenerationSettingsShape>(
|
||||
left: T,
|
||||
right: T,
|
||||
): boolean {
|
||||
return (
|
||||
left.model === right.model
|
||||
&& left.duration === right.duration
|
||||
&& left.videoResolution === right.videoResolution
|
||||
&& left.fps === right.fps
|
||||
&& (left.aspectRatio ?? '16:9') === (right.aspectRatio ?? '16:9')
|
||||
&& (left.audio ?? false) === (right.audio ?? false)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback labels for persisted `generationParams.model` / picker pipeline ids.
|
||||
*
|
||||
* Prefer `resolvePipelineDisplayName` (backend spec) at generation time. Local ids like
|
||||
* "fast"/"pro" are shared across LTX versions, so the fallback here stays version-agnostic —
|
||||
* only API ids (`fast-2.5`, …) encode their version in the id itself.
|
||||
*/
|
||||
const PIPELINE_DISPLAY_NAMES: Record<string, string> = {
|
||||
fast: 'LTX Fast',
|
||||
pro: 'LTX Pro',
|
||||
'fast-2.5': 'LTX-2.5 Fast',
|
||||
'pro-2.5': 'LTX-2.5 Pro',
|
||||
}
|
||||
|
||||
/** Returns a display label for a known video pipeline, or null if unknown/absent. */
|
||||
export function formatPipelineDisplayName(model: string | undefined | null): string | null {
|
||||
if (!model) return null
|
||||
return PIPELINE_DISPLAY_NAMES[model] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Version-correct label for `pipeline` taken from the backend specs currently in effect.
|
||||
* Returns null when the pipeline isn't in `modelSpecs`, so callers can fall back.
|
||||
*/
|
||||
export function resolvePipelineDisplayName(
|
||||
modelSpecs: VideoGenerationModelSpecItem[],
|
||||
pipeline: string | undefined | null,
|
||||
): string | null {
|
||||
if (!pipeline) return null
|
||||
return modelSpecs.find((item) => item.pipeline === pipeline)?.spec.display_name ?? null
|
||||
}
|
||||
Reference in new issue
Block a user