const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:4291" export type JobStatus = | "created" | "downloading" | "downloaded" | "splitting" | "frames_extracted" | "transcribing" | "transcribed" | "failed" export interface FrameObject { name: string position?: string color?: string extract_prompt?: string } export interface FrameDescription { scene?: string objects?: FrameObject[] style?: string suggested_prompt?: string } export interface GeneratedImage { id: string prompt: string model: string mode: string url: string selected: boolean created_at: number } export interface KeyFrame { index: number timestamp: number url: string description?: FrameDescription | null generated_images?: GeneratedImage[] } export interface TranscriptSegment { index: number start: number end: number en: string zh: string } export interface Job { id: string url: string status: JobStatus progress: number message?: string video_url?: string duration?: number width?: number height?: number frames: KeyFrame[] transcript: TranscriptSegment[] error?: string } export async function createJob(tkUrl: string): Promise { const res = await fetch(`${API_BASE}/jobs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: tkUrl }), }) if (!res.ok) throw new Error(`createJob ${res.status}`) return res.json() } export async function uploadJob(file: File): Promise { const fd = new FormData() fd.append("file", file) const res = await fetch(`${API_BASE}/jobs/upload`, { method: "POST", body: fd, }) if (!res.ok) { const text = await res.text().catch(() => "") throw new Error(`uploadJob ${res.status} ${text.slice(0, 200)}`) } return res.json() } export async function getJob(id: string): Promise { const res = await fetch(`${API_BASE}/jobs/${id}`) if (!res.ok) throw new Error(`getJob ${res.status}`) return res.json() } export async function triggerTranscribe(id: string): Promise { const res = await fetch(`${API_BASE}/jobs/${id}/transcribe`, { method: "POST" }) if (!res.ok) throw new Error(`transcribe ${res.status}`) return res.json() } export async function analyzeJob(id: string, frames = 5): Promise { const res = await fetch(`${API_BASE}/jobs/${id}/analyze?frames=${frames}`, { method: "POST" }) if (!res.ok) { const t = await res.text().catch(() => "") throw new Error(`analyze ${res.status} ${t.slice(0, 200)}`) } return res.json() } export async function addManualFrame(id: string, t: number): Promise { const res = await fetch(`${API_BASE}/jobs/${id}/frames?t=${encodeURIComponent(t.toFixed(2))}`, { method: "POST" }) if (!res.ok) { const txt = await res.text().catch(() => "") throw new Error(`addFrame ${res.status} ${txt.slice(0, 200)}`) } return res.json() } export async function describeFrame(jobId: string, frameIdx: number): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/describe`, { method: "POST" }) if (!res.ok) { const txt = await res.text().catch(() => "") throw new Error(`describe ${res.status} ${txt.slice(0, 200)}`) } return res.json() } export async function translateText(text: string, target: "en" | "zh" = "en"): Promise { const res = await fetch(`${API_BASE}/translate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text, target }), }) if (!res.ok) { const txt = await res.text().catch(() => "") throw new Error(`translate ${res.status} ${txt.slice(0, 200)}`) } const j = await res.json() return (j.text || "").toString() } export async function generateImage( jobId: string, frameIdx: number, body: { prompt: string; extra_prompt?: string; negative_prompt?: string; model?: string; mode?: "edit" | "text"; from_selected?: boolean }, ): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }) if (!res.ok) { const txt = await res.text().catch(() => "") throw new Error(`generate ${res.status} ${txt.slice(0, 300)}`) } return res.json() } export async function selectGenerated( jobId: string, frameIdx: number, genId: string, selected: boolean, ): Promise { const res = await fetch(`${API_BASE}/jobs/${jobId}/frames/${frameIdx}/gen/${genId}/select`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ selected }), }) if (!res.ok) throw new Error(`select ${res.status}`) return res.json() } export function generatedImageUrl(jobId: string, frameIdx: number, genId: string): string { return `${API_BASE}/jobs/${jobId}/frames/${frameIdx}/gen/${genId}.jpg` } export function frameUrl(jobId: string, frameIndex: number): string { return `${API_BASE}/jobs/${jobId}/frames/${frameIndex}.jpg` } export function videoUrl(jobId: string): string { return `${API_BASE}/jobs/${jobId}/video.mp4` }