File size: 3,936 Bytes
96f2542
 
 
 
 
 
 
 
 
 
 
 
73c8079
96f2542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8122b04
 
 
 
 
 
96f2542
 
 
 
 
 
 
 
 
8d8e3d7
 
 
96f2542
8122b04
 
 
 
96f2542
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192ae87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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<ModelInfo[]> {
  const r = await fetch("/api/models");
  if (!r.ok) throw new Error(`listModels: ${r.status}`);
  return r.json();
}

export async function getActiveModel(): Promise<ActiveStatus> {
  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<void> {
  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<string, unknown>;
  reference?: Blob;
};

export type GenerateResult = {
  blob: Blob;
  seedUsed: number | null;
};

export async function generate(input: GenerateInput): Promise<GenerateResult> {
  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<string, unknown>;
  speakers: DialogSpeakerInput[];
};

export type DialogResult = {
  blob: Blob;
  seedUsed: number | null;
};

export async function generateDialog(input: DialogInput): Promise<DialogResult> {
  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 };
}