export type Lang = { code: string; label: string }; export type ParamSpec = { name: string; label: string; type: "float" | "int" | "bool" | "enum"; default: number | string | boolean; min?: number; max?: number; step?: number; choices?: string[]; help?: string; group?: "basic" | "advanced"; }; export type ModelInfo = { id: string; label: string; description: string; languages: Lang[]; paralinguistic_tags: string[]; supports_voice_clone: boolean; params: ParamSpec[]; }; export type ActiveStatus = { id: string | null; status: "idle" | "loading" | "loaded" | "error"; last_error: string | null; }; export async function listModels(): Promise { const r = await fetch("/api/models"); if (!r.ok) throw new Error(`listModels: ${r.status}`); return r.json(); } export async function getActiveModel(): Promise { const r = await fetch("/api/models/active"); if (!r.ok) throw new Error(`getActiveModel: ${r.status}`); return r.json(); } export async function activateModel(id: string): Promise { const r = await fetch(`/api/models/${encodeURIComponent(id)}/activate`, { method: "POST" }); if (!r.ok) { const err = await r.json().catch(() => ({})); throw new Error(err?.error?.code ?? `activateModel: ${r.status}`); } } export type GenerateInput = { modelId: string; text: string; language?: string; params: Record; reference?: Blob; }; export type GenerateResult = { blob: Blob; seedUsed: number | null; }; export async function generate(input: GenerateInput): Promise { const fd = new FormData(); fd.set("text", input.text); fd.set("model_id", input.modelId); fd.set("params", JSON.stringify(input.params ?? {})); if (input.language) fd.set("language", input.language); if (input.reference) fd.set("reference_wav", input.reference, "ref.wav"); const r = await fetch("/api/generate", { method: "POST", body: fd }); if (!r.ok) { const err = await r.json().catch(() => ({})); const code = err?.error?.code ?? `generate: ${r.status}`; const msg = err?.error?.message; throw new Error(msg ? `${code}: ${msg}` : code); } const seedHeader = r.headers.get("x-seed-used"); const seedUsed = seedHeader != null ? Number(seedHeader) : null; const blob = await r.blob(); return { blob, seedUsed }; } export function streamActiveEvents( onEvent: (e: { id: string | null; status: string; error?: string }) => void, ) { const es = new EventSource("/api/models/active/events"); es.onmessage = (m) => { try { onEvent(JSON.parse(m.data)); } catch { /* ignore malformed */ } }; return () => es.close(); } export type DialogSpeakerInput = { letter: "A" | "B" | "C" | "D"; reference: Blob; }; export type DialogInput = { engineId: string; text: string; language?: string; params: Record; speakers: DialogSpeakerInput[]; }; export type DialogResult = { blob: Blob; seedUsed: number | null; }; export async function generateDialog(input: DialogInput): Promise { const fd = new FormData(); fd.set("text", input.text); fd.set("engine_id", input.engineId); fd.set("params", JSON.stringify(input.params ?? {})); if (input.language) fd.set("language", input.language); for (const s of input.speakers) { fd.set(`reference_wav_${s.letter.toLowerCase()}`, s.reference, `${s.letter}.wav`); } const r = await fetch("/api/generate/dialog", { method: "POST", body: fd }); if (!r.ok) { const err = await r.json().catch(() => ({})); const code = err?.error?.code ?? `dialog: ${r.status}`; const msg = err?.error?.message; throw new Error(msg ? `${code}: ${msg}` : code); } const seedHeader = r.headers.get("x-seed-used"); const seedUsed = seedHeader != null ? Number(seedHeader) : null; const blob = await r.blob(); return { blob, seedUsed }; }