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
+329
View File
@@ -0,0 +1,329 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
import { resetBackendCredentials } from '../lib/backend'
import { ApiClient, type ApiSuccessOf } from '../lib/api-client'
export interface AppSettings {
useTorchCompile: boolean
diffusionStageCacheEnabled: boolean
hasLtxApiKey: boolean
userPrefersLtxApiVideoGenerations: boolean
hasFalApiKey: boolean
userPrefersFalApiImageGenerations: boolean
hasGeminiApiKey: boolean
geminiModel: string
useLocalTextEncoder: boolean
promptCacheSize: number
promptEnhancerEnabledT2V: boolean
promptEnhancerEnabledI2V: boolean
// The user's explicit prompt-enhancer provider choice, persisted so it survives restarts.
// null means no active choice yet — the enhancer defaults to whichever provider is available
// without writing that default back here; only an explicit pick (never an automatic fallback
// when the preferred provider is temporarily unavailable) sets this.
promptEnhancerProviderPreference: 'local' | 'api' | null
seedLocked: boolean
lockedSeed: number
modelsDir: string
useConvVae: boolean
}
export const DEFAULT_GEMINI_MODEL = 'gemini-3.5-flash-lite'
export const DEFAULT_APP_SETTINGS: AppSettings = {
useTorchCompile: false,
diffusionStageCacheEnabled: false,
hasLtxApiKey: false,
userPrefersLtxApiVideoGenerations: false,
hasFalApiKey: false,
userPrefersFalApiImageGenerations: false,
hasGeminiApiKey: false,
geminiModel: '',
useLocalTextEncoder: false,
promptCacheSize: 1,
promptEnhancerEnabledT2V: false,
promptEnhancerEnabledI2V: false,
promptEnhancerProviderPreference: null,
seedLocked: false,
lockedSeed: 42,
modelsDir: '',
useConvVae: false,
}
type BackendProcessStatus = 'alive' | 'restarting' | 'dead'
interface AppSettingsContextValue {
settings: AppSettings
isLoaded: boolean
runtimePolicyLoaded: boolean
updateSettings: (patch: Partial<AppSettings> | ((prev: AppSettings) => AppSettings)) => void
refreshSettings: () => Promise<void>
saveLtxApiKey: (value: string) => Promise<void>
saveFalApiKey: (value: string) => Promise<void>
saveGeminiApiKey: (value: string) => Promise<void>
forceApiGenerations: boolean
shouldVideoGenerateWithLtxApi: boolean
shouldImageGenerateWithFalApi: boolean
cudaAvailable: boolean
// Bumped whenever installed models change (download / delete / activate a version). Generation
// model specs are derived from the *active* local model, so anything reading them must refetch;
// without this they stay pinned to whatever was installed at app start.
modelsVersion: number
notifyModelsChanged: () => void
}
const AppSettingsContext = createContext<AppSettingsContextValue | null>(null)
function toBackendProcessStatus(value: unknown): BackendProcessStatus | null {
if (!value || typeof value !== 'object') {
return null
}
const record = value as { status?: unknown }
if (record.status === 'alive' || record.status === 'restarting' || record.status === 'dead') {
return record.status
}
return null
}
function normalizeAppSettings(data: Partial<AppSettings>): AppSettings {
return {
useTorchCompile: data.useTorchCompile ?? DEFAULT_APP_SETTINGS.useTorchCompile,
diffusionStageCacheEnabled: data.diffusionStageCacheEnabled ?? DEFAULT_APP_SETTINGS.diffusionStageCacheEnabled,
hasLtxApiKey: data.hasLtxApiKey ?? DEFAULT_APP_SETTINGS.hasLtxApiKey,
userPrefersLtxApiVideoGenerations: data.userPrefersLtxApiVideoGenerations ?? DEFAULT_APP_SETTINGS.userPrefersLtxApiVideoGenerations,
hasFalApiKey: data.hasFalApiKey ?? DEFAULT_APP_SETTINGS.hasFalApiKey,
userPrefersFalApiImageGenerations: data.userPrefersFalApiImageGenerations ?? DEFAULT_APP_SETTINGS.userPrefersFalApiImageGenerations,
hasGeminiApiKey: data.hasGeminiApiKey ?? DEFAULT_APP_SETTINGS.hasGeminiApiKey,
geminiModel: data.geminiModel ?? DEFAULT_APP_SETTINGS.geminiModel,
useLocalTextEncoder: data.useLocalTextEncoder ?? DEFAULT_APP_SETTINGS.useLocalTextEncoder,
promptCacheSize: data.promptCacheSize ?? DEFAULT_APP_SETTINGS.promptCacheSize,
promptEnhancerEnabledT2V: data.promptEnhancerEnabledT2V ?? DEFAULT_APP_SETTINGS.promptEnhancerEnabledT2V,
promptEnhancerEnabledI2V: data.promptEnhancerEnabledI2V ?? DEFAULT_APP_SETTINGS.promptEnhancerEnabledI2V,
promptEnhancerProviderPreference: data.promptEnhancerProviderPreference ?? DEFAULT_APP_SETTINGS.promptEnhancerProviderPreference,
seedLocked: data.seedLocked ?? DEFAULT_APP_SETTINGS.seedLocked,
lockedSeed: data.lockedSeed ?? DEFAULT_APP_SETTINGS.lockedSeed,
modelsDir: data.modelsDir ?? DEFAULT_APP_SETTINGS.modelsDir,
useConvVae: data.useConvVae ?? DEFAULT_APP_SETTINGS.useConvVae,
}
}
type RuntimePolicyPayload = ApiSuccessOf<'getRuntimePolicy'>
type GpuInfoPayload = ApiSuccessOf<'getGpuInfo'>
export function AppSettingsProvider({ children }: { children: ReactNode }) {
const [settings, setSettings] = useState<AppSettings>(DEFAULT_APP_SETTINGS)
const [isLoaded, setIsLoaded] = useState(false)
const [runtimePolicyLoaded, setRuntimePolicyLoaded] = useState(false)
const [forceApiGenerations, setForceApiGenerations] = useState(true)
const [cudaAvailable, setCudaAvailable] = useState(false)
const [backendProcessStatus, setBackendProcessStatus] = useState<BackendProcessStatus | null>(null)
const [modelsVersion, setModelsVersion] = useState(0)
const [settingsRevision, setSettingsRevision] = useState(0)
const notifyModelsChanged = useCallback(() => {
setModelsVersion((current) => current + 1)
}, [])
useEffect(() => {
if (backendProcessStatus !== 'alive') return
let cancelled = false
setRuntimePolicyLoaded(false)
const fetchRuntimePolicy = async () => {
const result = await ApiClient.getRuntimePolicy()
if (!result.ok) {
if (!cancelled) {
// Fail closed until policy can be read.
setForceApiGenerations(true)
setRuntimePolicyLoaded(true)
}
return
}
const payload = result.data as RuntimePolicyPayload
if (typeof payload.force_api_generations !== 'boolean') {
if (!cancelled) {
setForceApiGenerations(true)
}
} else if (!cancelled) {
setForceApiGenerations(payload.force_api_generations)
}
if (!cancelled) {
setRuntimePolicyLoaded(true)
}
}
void fetchRuntimePolicy()
return () => {
cancelled = true
}
}, [backendProcessStatus])
useEffect(() => {
if (backendProcessStatus !== 'alive') return
let cancelled = false
const fetchGpuInfo = async () => {
const result = await ApiClient.getGpuInfo()
if (!result.ok || cancelled) return
const payload = result.data as GpuInfoPayload
setCudaAvailable(Boolean(payload.cuda_available))
}
void fetchGpuInfo()
return () => {
cancelled = true
}
}, [backendProcessStatus, modelsVersion])
useEffect(() => {
let cancelled = false
const applyStatus = (value: unknown) => {
const nextStatus = toBackendProcessStatus(value)
if (!nextStatus || cancelled) {
return
}
if (nextStatus === 'alive') {
resetBackendCredentials()
}
setBackendProcessStatus(nextStatus)
}
const unsubscribe = window.electronAPI.onBackendHealthStatus((data) => {
applyStatus(data)
})
void window.electronAPI.getBackendHealthStatus()
.then((snapshot) => {
applyStatus(snapshot)
})
.catch(() => {
// Snapshot is optional at startup; subscription continues to listen for pushes.
})
return () => {
cancelled = true
unsubscribe()
}
}, [])
const refreshSettings = useCallback(async () => {
const result = await ApiClient.getSettings()
if (!result.ok) {
throw new Error(result.error.message)
}
setSettings(normalizeAppSettings(result.data))
setIsLoaded(true)
}, [])
useEffect(() => {
if (isLoaded || backendProcessStatus !== 'alive') return
let cancelled = false
let retryTimer: ReturnType<typeof setTimeout> | null = null
const fetchSettings = async () => {
try {
await refreshSettings()
if (cancelled) return
} catch {
if (!cancelled) {
retryTimer = setTimeout(fetchSettings, 1000)
}
}
}
fetchSettings()
return () => {
cancelled = true
if (retryTimer) clearTimeout(retryTimer)
}
}, [backendProcessStatus, isLoaded, refreshSettings])
useEffect(() => {
if (!isLoaded || backendProcessStatus !== 'alive' || settingsRevision === 0) return
const syncTimer = setTimeout(async () => {
const { hasLtxApiKey: _a, hasFalApiKey: _b, hasGeminiApiKey: _c, modelsDir: _d, ...syncPayload } = settings
const result = await ApiClient.updateSettings(syncPayload)
if (!result.ok) {
// Best-effort settings sync.
}
}, 150)
return () => clearTimeout(syncTimer)
}, [backendProcessStatus, isLoaded, settings, settingsRevision])
const updateSettings = useCallback((patch: Partial<AppSettings> | ((prev: AppSettings) => AppSettings)) => {
setSettingsRevision(value => value + 1)
if (typeof patch === 'function') {
setSettings((prev) => patch(prev))
return
}
setSettings((prev) => ({ ...prev, ...patch }))
}, [])
const saveLtxApiKey = useCallback(async (value: string) => {
const result = await ApiClient.updateSettings({ ltxApiKey: value })
if (!result.ok) {
throw new Error(result.error.message)
}
await refreshSettings()
}, [refreshSettings])
const saveGeminiApiKey = useCallback(async (value: string) => {
const result = await ApiClient.updateSettings({ geminiApiKey: value })
if (!result.ok) {
throw new Error(result.error.message)
}
await refreshSettings()
}, [refreshSettings])
const saveFalApiKey = useCallback(async (value: string) => {
const result = await ApiClient.updateSettings({ falApiKey: value })
if (!result.ok) {
throw new Error(result.error.message)
}
await refreshSettings()
}, [refreshSettings])
const shouldVideoGenerateWithLtxApi =
forceApiGenerations || (settings.userPrefersLtxApiVideoGenerations && settings.hasLtxApiKey)
const shouldImageGenerateWithFalApi =
forceApiGenerations || (settings.userPrefersFalApiImageGenerations && settings.hasFalApiKey)
const contextValue = useMemo<AppSettingsContextValue>(
() => ({
settings,
isLoaded,
runtimePolicyLoaded,
updateSettings,
refreshSettings,
saveLtxApiKey,
saveFalApiKey,
saveGeminiApiKey,
forceApiGenerations,
shouldVideoGenerateWithLtxApi,
shouldImageGenerateWithFalApi,
cudaAvailable,
modelsVersion,
notifyModelsChanged,
}),
[cudaAvailable, forceApiGenerations, isLoaded, modelsVersion, notifyModelsChanged, refreshSettings, runtimePolicyLoaded, saveFalApiKey, saveGeminiApiKey, saveLtxApiKey, settings, shouldVideoGenerateWithLtxApi, shouldImageGenerateWithFalApi, updateSettings],
)
return <AppSettingsContext.Provider value={contextValue}>{children}</AppSettingsContext.Provider>
}
export function useAppSettings() {
const context = useContext(AppSettingsContext)
if (!context) {
throw new Error('useAppSettings must be used within AppSettingsProvider')
}
return context
}