@shogo-ai/voice 1.10.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +89 -0
  2. package/dist/{chunk-S3S2RNO6.js → chunk-BI6SL35K.js} +1 -184
  3. package/dist/chunk-IOFWML5L.js +1078 -0
  4. package/dist/chunk-KMSL5PWK.js +540 -0
  5. package/dist/chunk-ME3SIZZU.js +581 -0
  6. package/dist/{chunk-T3T4EBWY.js → chunk-RALZRMFL.js} +6 -2
  7. package/dist/chunk-TQOBJMEK.js +17 -0
  8. package/dist/chunk-U5UC7ZVS.js +988 -0
  9. package/dist/chunk-UW7VQ3S6.js +148 -0
  10. package/dist/chunk-ZAW6F3YT.js +172 -0
  11. package/dist/index.cjs +748 -25
  12. package/dist/index.d.cts +396 -3
  13. package/dist/index.d.ts +396 -3
  14. package/dist/index.js +63 -3
  15. package/dist/live/client.cjs +173 -0
  16. package/dist/live/client.d.cts +132 -0
  17. package/dist/live/client.d.ts +132 -0
  18. package/dist/live/client.js +9 -0
  19. package/dist/native/index.d.cts +5 -2
  20. package/dist/native/index.d.ts +5 -2
  21. package/dist/native/index.js +15 -11
  22. package/dist/{organicParticlesShaders-DIva0vBO.d.ts → organicParticlesShaders-seZq7jYX.d.cts} +1 -421
  23. package/dist/{organicParticlesShaders-DIva0vBO.d.cts → organicParticlesShaders-seZq7jYX.d.ts} +1 -421
  24. package/dist/particlesConfig-0YbdPOdY.d.cts +223 -0
  25. package/dist/particlesConfig-0YbdPOdY.d.ts +223 -0
  26. package/dist/react/conversation.cjs +789 -0
  27. package/dist/react/conversation.d.cts +134 -0
  28. package/dist/react/conversation.d.ts +134 -0
  29. package/dist/react/conversation.js +13 -0
  30. package/dist/react/index.cjs +374 -36
  31. package/dist/react/index.d.cts +10 -141
  32. package/dist/react/index.d.ts +10 -141
  33. package/dist/react/index.js +23 -770
  34. package/dist/react/visuals.cjs +1048 -0
  35. package/dist/react/visuals.d.cts +70 -0
  36. package/dist/react/visuals.d.ts +70 -0
  37. package/dist/react/visuals.js +10 -0
  38. package/dist/route/agent.cjs +1047 -85
  39. package/dist/route/agent.js +3 -3
  40. package/dist/route/audio-tags.cjs +1047 -85
  41. package/dist/route/audio-tags.js +3 -3
  42. package/dist/route/index.cjs +1051 -85
  43. package/dist/route/index.d.cts +19 -3
  44. package/dist/route/index.d.ts +19 -3
  45. package/dist/route/index.js +3 -3
  46. package/dist/route/music.cjs +1047 -85
  47. package/dist/route/music.js +3 -3
  48. package/dist/route/op.cjs +2127 -0
  49. package/dist/route/op.d.cts +43 -0
  50. package/dist/route/op.d.ts +43 -0
  51. package/dist/route/op.js +27 -0
  52. package/dist/route/signed-url.cjs +1047 -85
  53. package/dist/route/signed-url.js +3 -3
  54. package/dist/route/tts-preview.cjs +1047 -85
  55. package/dist/route/tts-preview.d.cts +6 -4
  56. package/dist/route/tts-preview.d.ts +6 -4
  57. package/dist/route/tts-preview.js +3 -3
  58. package/dist/server.cjs +1047 -85
  59. package/dist/server.d.cts +40 -8
  60. package/dist/server.d.ts +40 -8
  61. package/dist/server.js +2 -2
  62. package/dist/types-Bfhu-aQH.d.cts +199 -0
  63. package/dist/types-Bfhu-aQH.d.ts +199 -0
  64. package/dist/{types-W9y6eMj3.d.ts → types-CadjTXNH.d.cts} +193 -12
  65. package/dist/{types-W9y6eMj3.d.cts → types-CadjTXNH.d.ts} +193 -12
  66. package/package.json +21 -1
  67. package/dist/chunk-B4JC3DK2.js +0 -529
  68. package/dist/chunk-O362EJQR.js +0 -524
@@ -0,0 +1,988 @@
1
+ import {
2
+ CONVAI_TTS_MODEL_FALLBACK,
3
+ DEFAULT_TTS_MODEL,
4
+ ElevenLabsApiError,
5
+ ElevenLabsClient,
6
+ MEMORY_CLIENT_TOOLS,
7
+ VOICE_OP_IDS,
8
+ extractBasePrompt,
9
+ getVoiceOp,
10
+ parseVoiceOpForm,
11
+ parseVoiceOpJsonBody,
12
+ readRuntimeAuthSecret,
13
+ resolveConvaiTtsModel,
14
+ voiceOpForwardPath,
15
+ voiceOpPathParams,
16
+ voiceOpReturnsAudio,
17
+ voiceOpUpstreamBody,
18
+ voiceOpUpstreamPath
19
+ } from "./chunk-IOFWML5L.js";
20
+ import {
21
+ AUDIO_TAGS,
22
+ AUDIO_TAG_GROUPS,
23
+ DEFAULT_ALLOWED_TAGS,
24
+ DEFAULT_VOICE_SETTINGS,
25
+ EXPRESSIVITY_OPTIONS,
26
+ buildPreviewLine,
27
+ normalizeAudioTags,
28
+ normalizeExpressivity,
29
+ normalizeVoiceSettings,
30
+ readAudioTags,
31
+ readExpressivity,
32
+ readVoiceSettings
33
+ } from "./chunk-Q3TJAJ54.js";
34
+
35
+ // src/server.ts
36
+ function json(data, status = 200, extraHeaders = {}) {
37
+ return new Response(JSON.stringify(data), {
38
+ status,
39
+ headers: { "Content-Type": "application/json", ...extraHeaders }
40
+ });
41
+ }
42
+ async function readJson(req) {
43
+ try {
44
+ const text = await req.text();
45
+ if (!text) return {};
46
+ return JSON.parse(text);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ var NOOP_LOGGER = () => {
52
+ };
53
+ function parseMusicBody(raw) {
54
+ if (raw === null || typeof raw !== "object") return { error: "Invalid JSON body" };
55
+ const b = raw;
56
+ const hasPrompt = typeof b.prompt === "string" && b.prompt.trim().length > 0;
57
+ const hasPlan = b.compositionPlan != null && typeof b.compositionPlan === "object";
58
+ if (hasPrompt === hasPlan) {
59
+ return { error: "Provide exactly one of `prompt` or `compositionPlan`" };
60
+ }
61
+ const params = {};
62
+ if (hasPrompt) {
63
+ params.prompt = b.prompt.trim();
64
+ if (typeof b.musicLengthMs === "number") params.musicLengthMs = b.musicLengthMs;
65
+ if (typeof b.forceInstrumental === "boolean") params.forceInstrumental = b.forceInstrumental;
66
+ } else {
67
+ params.compositionPlan = b.compositionPlan;
68
+ }
69
+ if (typeof b.modelId === "string" && b.modelId.length > 0) params.modelId = b.modelId;
70
+ if (typeof b.outputFormat === "string" && b.outputFormat.length > 0) {
71
+ params.outputFormat = b.outputFormat;
72
+ }
73
+ return { params };
74
+ }
75
+ var MISSING_API_HOST_MESSAGE = "createVoiceHandlers: runtime-token proxy mode needs SHOGO_API_URL (or SHOGO_CLOUD_URL) in env; refusing to guess an API host";
76
+ function envApiUrl() {
77
+ if (typeof process === "undefined" || !process.env) return void 0;
78
+ return process.env.SHOGO_API_URL ?? process.env.SHOGO_CLOUD_URL;
79
+ }
80
+ function resolveProxyOptions(config) {
81
+ if (config.mode === "byo") {
82
+ if (config.proxy) {
83
+ throw new Error(
84
+ "createVoiceHandlers: mode 'byo' cannot be combined with an explicit proxy"
85
+ );
86
+ }
87
+ return null;
88
+ }
89
+ const envProxy = () => {
90
+ if (config.mode === "proxy" && config.apiKey) {
91
+ LOG_WARN_CACHE.apiKeyIgnored(config.logger);
92
+ }
93
+ if (typeof process === "undefined" || !process.env) return null;
94
+ const runtimeToken = readRuntimeAuthSecret();
95
+ const projectId = process.env.PROJECT_ID;
96
+ if (!runtimeToken || !projectId) return null;
97
+ return {
98
+ runtimeToken,
99
+ projectId,
100
+ // Deliberately not required here: see `resolveApiBase`. Throwing at
101
+ // construction made every local-only handler (400 validation, 405s,
102
+ // the static audio-tags catalog) unusable on a host with a token but
103
+ // no URL — which is exactly CI's env.
104
+ apiUrl: envApiUrl() ?? "",
105
+ ...config.fetch ? { fetch: config.fetch } : {}
106
+ };
107
+ };
108
+ if (config.proxy) {
109
+ if (!config.proxy.runtimeToken || !config.proxy.projectId || !config.proxy.apiUrl) {
110
+ throw new Error(
111
+ "createVoiceHandlers: proxy requires runtimeToken, projectId, and apiUrl"
112
+ );
113
+ }
114
+ if (config.apiKey) LOG_WARN_CACHE.apiKeyIgnored(config.logger);
115
+ return config.proxy;
116
+ }
117
+ if (config.mode === "proxy") return envProxy();
118
+ if (config.apiKey) return null;
119
+ return envProxy();
120
+ }
121
+ var LOG_WARN_CACHE = {
122
+ warned: false,
123
+ apiKeyIgnored(logger) {
124
+ if (LOG_WARN_CACHE.warned) return;
125
+ LOG_WARN_CACHE.warned = true;
126
+ (logger ?? NOOP_LOGGER)(
127
+ "warn",
128
+ 'createVoiceHandlers: apiKey provided but runtime-token proxy mode is active; apiKey is ignored (pass mode: "byo" to use it)'
129
+ );
130
+ }
131
+ };
132
+ function resolveOpId(req, opId) {
133
+ if (opId) return opId;
134
+ try {
135
+ const url = new URL(req.url);
136
+ const fromQuery = url.searchParams.get("op");
137
+ if (fromQuery) return fromQuery;
138
+ const segments = url.pathname.split("/").filter(Boolean);
139
+ const last = segments[segments.length - 1];
140
+ if (last && segments[segments.length - 2] === "op") return last;
141
+ return void 0;
142
+ } catch {
143
+ return void 0;
144
+ }
145
+ }
146
+ async function readVoiceOpBody(op, req) {
147
+ if (op.method === "GET") return { kind: "none" };
148
+ if (op.transport === "multipart-binary" || op.transport === "multipart-json") {
149
+ let form;
150
+ try {
151
+ form = await req.formData();
152
+ } catch {
153
+ return json({ error: "Invalid multipart body", detail: "send a multipart/form-data body" }, 400);
154
+ }
155
+ const parsed2 = parseVoiceOpForm(op, form);
156
+ if (!parsed2.ok) {
157
+ return json(
158
+ {
159
+ error: parsed2.error,
160
+ ...parsed2.detail ? { detail: parsed2.detail } : {},
161
+ ...parsed2.allowed ? { allowed: parsed2.allowed } : {}
162
+ },
163
+ parsed2.status
164
+ );
165
+ }
166
+ return { kind: "form", form: parsed2.form, fields: parsed2.fields };
167
+ }
168
+ const raw = await readJson(req);
169
+ const parsed = parseVoiceOpJsonBody(op, raw);
170
+ if (!parsed.ok) {
171
+ return json(
172
+ {
173
+ error: parsed.error,
174
+ ...parsed.detail ? { detail: parsed.detail } : {},
175
+ ...parsed.allowed ? { allowed: parsed.allowed } : {}
176
+ },
177
+ parsed.status
178
+ );
179
+ }
180
+ return { kind: "json", fields: parsed.fields };
181
+ }
182
+ function createProxyHandlers(proxy, log) {
183
+ const fetchImpl = proxy.fetch ?? globalThis.fetch;
184
+ if (typeof fetchImpl !== "function") {
185
+ throw new Error(
186
+ "createVoiceHandlers proxy: global fetch is unavailable; pass proxy.fetch"
187
+ );
188
+ }
189
+ function resolveApiBase() {
190
+ const raw = proxy.apiUrl || envApiUrl();
191
+ if (!raw) throw new Error(MISSING_API_HOST_MESSAGE);
192
+ return raw.replace(/\/+$/, "");
193
+ }
194
+ function proxyUrl(base, path, forward) {
195
+ const tail = [`projectId=${encodeURIComponent(proxy.projectId)}`];
196
+ for (const [k, v] of Object.entries(forward)) {
197
+ if (typeof v === "string" && v.length > 0) {
198
+ tail.push(`${k}=${encodeURIComponent(v)}`);
199
+ }
200
+ }
201
+ const sep = path.includes("?") ? "&" : "?";
202
+ return `${base}${path}${sep}${tail.join("&")}`;
203
+ }
204
+ const FORWARDED_SIGNED_URL_PARAMS = ["agentName"];
205
+ async function doSignedUrl(req) {
206
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
207
+ const base = resolveApiBase();
208
+ try {
209
+ let incomingUrl = null;
210
+ try {
211
+ incomingUrl = new URL(req.url);
212
+ } catch {
213
+ }
214
+ const forward = {};
215
+ if (incomingUrl) {
216
+ for (const key of FORWARDED_SIGNED_URL_PARAMS) {
217
+ const v = incomingUrl.searchParams.get(key);
218
+ if (v != null) forward[key] = v;
219
+ }
220
+ }
221
+ const upstream = await fetchImpl(proxyUrl(base, "/api/voice/signed-url", forward), {
222
+ method: "GET",
223
+ headers: { "x-runtime-token": proxy.runtimeToken },
224
+ credentials: "omit"
225
+ });
226
+ const body = await upstream.text();
227
+ const headers = { "content-type": "application/json" };
228
+ const ct = upstream.headers.get("content-type");
229
+ if (ct) headers["content-type"] = ct;
230
+ return new Response(body, { status: upstream.status, headers });
231
+ } catch (err) {
232
+ log("error", "proxy signedUrl failed", {
233
+ error: err instanceof Error ? err.message : String(err)
234
+ });
235
+ return json({ error: "signedUrl proxy failed" }, 502);
236
+ }
237
+ }
238
+ async function doAudioTags(req) {
239
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
240
+ return json({
241
+ tags: AUDIO_TAGS,
242
+ groups: AUDIO_TAG_GROUPS,
243
+ expressivity: EXPRESSIVITY_OPTIONS,
244
+ defaults: {
245
+ allowedTags: DEFAULT_ALLOWED_TAGS,
246
+ voiceSettings: DEFAULT_VOICE_SETTINGS,
247
+ ttsModelId: CONVAI_TTS_MODEL_FALLBACK
248
+ }
249
+ });
250
+ }
251
+ async function doOp(req, opId) {
252
+ const id = resolveOpId(req, opId);
253
+ const op = getVoiceOp(id);
254
+ if (!op) {
255
+ return json(
256
+ { error: "unknown_op", detail: `Unknown voice op '${id ?? ""}'`, allowed: VOICE_OP_IDS },
257
+ 404
258
+ );
259
+ }
260
+ if (req.method !== op.method) return json({ error: "Method Not Allowed" }, 405);
261
+ const parsed = await readVoiceOpBody(op, req);
262
+ if (parsed instanceof Response) return parsed;
263
+ const base = resolveApiBase();
264
+ const forward = {};
265
+ let incomingUrl = null;
266
+ try {
267
+ incomingUrl = new URL(req.url);
268
+ } catch {
269
+ }
270
+ if (incomingUrl) {
271
+ for (const key of [...op.pathParams ?? [], ...op.queryForward ?? []]) {
272
+ const value = incomingUrl.searchParams.get(key);
273
+ if (value != null) forward[key] = value;
274
+ }
275
+ }
276
+ if (parsed.kind === "form") {
277
+ for (const name of op.pathParams ?? []) {
278
+ const value = parsed.fields[name];
279
+ if (typeof value === "string" && value.length > 0) forward[name] = value;
280
+ }
281
+ }
282
+ const hostedPath = `/api${voiceOpForwardPath(op)}`;
283
+ const headers = { "x-runtime-token": proxy.runtimeToken };
284
+ let body;
285
+ if (parsed.kind === "json") {
286
+ headers["content-type"] = "application/json";
287
+ body = JSON.stringify(parsed.fields);
288
+ } else if (parsed.kind === "form") {
289
+ body = parsed.form;
290
+ }
291
+ if (op.accept) headers.accept = op.accept;
292
+ try {
293
+ const upstream = await fetchImpl(proxyUrl(base, hostedPath, forward), {
294
+ method: op.method,
295
+ headers,
296
+ ...body !== void 0 ? { body } : {},
297
+ credentials: "omit"
298
+ });
299
+ if (upstream.status === 404) {
300
+ return json(
301
+ {
302
+ error: "Not Implemented",
303
+ detail: `this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 ${hostedPath} returned 404 upstream`
304
+ },
305
+ 501
306
+ );
307
+ }
308
+ const buf = await upstream.arrayBuffer();
309
+ const responseHeaders = {};
310
+ const ct = upstream.headers.get("content-type");
311
+ if (ct) responseHeaders["content-type"] = ct;
312
+ for (const name of op.responseHeaders ?? []) {
313
+ const value = upstream.headers.get(name);
314
+ if (value) responseHeaders[name] = value;
315
+ }
316
+ return new Response(buf, { status: upstream.status, headers: responseHeaders });
317
+ } catch (err) {
318
+ log("error", `proxy ${op.id} failed`, {
319
+ error: err instanceof Error ? err.message : String(err)
320
+ });
321
+ return json({ error: `${op.id} proxy failed` }, 502);
322
+ }
323
+ }
324
+ const doMusic = (req) => doOp(req, "music");
325
+ function notImplemented(label) {
326
+ return async () => json(
327
+ {
328
+ error: "Not Implemented",
329
+ detail: `${label} is not available in runtime-token proxy mode; use BYO-EL or the hosted bearer SDK path.`
330
+ },
331
+ 501
332
+ );
333
+ }
334
+ async function doTts(req) {
335
+ if (req.method === "POST") {
336
+ let raw = null;
337
+ try {
338
+ const parsed = await req.clone().json();
339
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
340
+ raw = parsed;
341
+ }
342
+ } catch {
343
+ }
344
+ if (raw && (typeof raw.text !== "string" || raw.text.trim().length === 0)) {
345
+ const sentTags = Array.isArray(raw.audioTags) && raw.audioTags.length > 0;
346
+ if (sentTags) {
347
+ const tags = normalizeAudioTags(raw.audioTags) ?? [];
348
+ const characterName = typeof raw.characterName === "string" ? raw.characterName : void 0;
349
+ const text = buildPreviewLine(tags, characterName ?? "your companion");
350
+ return doOp(
351
+ new Request(req.url, {
352
+ method: "POST",
353
+ headers: { "content-type": "application/json" },
354
+ body: JSON.stringify({ ...raw, text })
355
+ }),
356
+ "text-to-speech"
357
+ );
358
+ }
359
+ }
360
+ }
361
+ return doOp(req, "text-to-speech");
362
+ }
363
+ function createProxyElevenLabs() {
364
+ const unsupported = (prop) => {
365
+ throw new Error(
366
+ `createVoiceHandlers: elevenLabs.${prop} is not available in runtime-token proxy mode (textToSpeech, composeMusic, speechToSpeech, soundEffects and transcribe are hosted); pass mode: 'byo' with an apiKey`
367
+ );
368
+ };
369
+ const textToSpeech = async (params) => {
370
+ const upstream = await fetchImpl(proxyUrl(resolveApiBase(), "/api/voice/tts", {}), {
371
+ method: "POST",
372
+ headers: {
373
+ "x-runtime-token": proxy.runtimeToken,
374
+ "content-type": "application/json",
375
+ accept: params.accept ?? "audio/mpeg"
376
+ },
377
+ body: JSON.stringify({
378
+ text: params.text,
379
+ voiceId: params.voiceId,
380
+ ...params.modelId ? { modelId: params.modelId } : {},
381
+ ...params.voiceSettings ? { voiceSettings: params.voiceSettings } : {}
382
+ }),
383
+ credentials: "omit"
384
+ });
385
+ if (upstream.status === 404) {
386
+ throw new Error(
387
+ "this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 /api/voice/tts returned 404 upstream"
388
+ );
389
+ }
390
+ if (!upstream.ok) {
391
+ throw new ElevenLabsApiError(
392
+ `textToSpeech failed: ${upstream.status}`,
393
+ upstream.status,
394
+ await upstream.text()
395
+ );
396
+ }
397
+ const requestedModel = params.modelId ?? DEFAULT_TTS_MODEL;
398
+ const usedModel = upstream.headers.get("x-tts-model-used") ?? requestedModel;
399
+ const requestedHeader = upstream.headers.get("x-tts-model-requested") ?? requestedModel;
400
+ return {
401
+ audio: await upstream.arrayBuffer(),
402
+ modelId: usedModel,
403
+ requestedModel: requestedHeader,
404
+ fellBack: usedModel !== requestedHeader,
405
+ contentType: upstream.headers.get("content-type") ?? params.accept ?? "audio/mpeg"
406
+ };
407
+ };
408
+ const composeMusic = async (params) => {
409
+ const upstream = await fetchImpl(proxyUrl(resolveApiBase(), "/api/voice/music", {}), {
410
+ method: "POST",
411
+ headers: {
412
+ "x-runtime-token": proxy.runtimeToken,
413
+ "content-type": "application/json",
414
+ accept: "audio/mpeg"
415
+ },
416
+ body: JSON.stringify(params),
417
+ credentials: "omit"
418
+ });
419
+ if (upstream.status === 404) {
420
+ throw new Error(
421
+ "this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 /api/voice/music returned 404 upstream"
422
+ );
423
+ }
424
+ if (!upstream.ok) {
425
+ throw new ElevenLabsApiError(
426
+ `composeMusic failed: ${upstream.status}`,
427
+ upstream.status,
428
+ await upstream.text()
429
+ );
430
+ }
431
+ return {
432
+ audio: await upstream.arrayBuffer(),
433
+ contentType: upstream.headers.get("content-type") ?? "audio/mpeg"
434
+ };
435
+ };
436
+ const opResponse = async (opId, req) => {
437
+ const op = getVoiceOp(opId);
438
+ const res = await doOp(req, opId);
439
+ if (res.status === 404) {
440
+ throw new Error(
441
+ `this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 /api${voiceOpForwardPath(op)} returned 404 upstream`
442
+ );
443
+ }
444
+ if (!res.ok) {
445
+ throw new ElevenLabsApiError(
446
+ `${opId} failed: ${res.status}`,
447
+ res.status,
448
+ await res.text()
449
+ );
450
+ }
451
+ return res;
452
+ };
453
+ const opAudio = async (opId, req) => {
454
+ const res = await opResponse(opId, req);
455
+ return {
456
+ audio: await res.arrayBuffer(),
457
+ contentType: res.headers.get("content-type") ?? "audio/mpeg"
458
+ };
459
+ };
460
+ const speechToSpeech = (params) => {
461
+ const form = new FormData();
462
+ form.set("audio", params.audio, params.fileName ?? "audio");
463
+ if (params.modelId) form.set("modelId", params.modelId);
464
+ if (params.voiceSettings) form.set("voiceSettings", JSON.stringify(params.voiceSettings));
465
+ if (params.outputFormat) form.set("outputFormat", params.outputFormat);
466
+ if (params.fileFormat) form.set("fileFormat", params.fileFormat);
467
+ if (params.seed !== void 0) form.set("seed", String(params.seed));
468
+ if (params.removeBackgroundNoise !== void 0) {
469
+ form.set("removeBackgroundNoise", String(params.removeBackgroundNoise));
470
+ }
471
+ const query = `?voiceId=${encodeURIComponent(params.voiceId)}`;
472
+ return opAudio(
473
+ "speech-to-speech",
474
+ new Request(`http://voice.invalid/voice/op/speech-to-speech${query}`, {
475
+ method: "POST",
476
+ body: form
477
+ })
478
+ );
479
+ };
480
+ const soundEffects = (params) => opAudio(
481
+ "sound-generation",
482
+ new Request(
483
+ // `output_format` is a query param on this endpoint (see the row's
484
+ // `queryForward`); sending it in the body would be dropped at EL.
485
+ `http://voice.invalid/voice/op/sound-generation${params.outputFormat ? `?output_format=${encodeURIComponent(params.outputFormat)}` : ""}`,
486
+ {
487
+ method: "POST",
488
+ headers: { "content-type": "application/json" },
489
+ body: JSON.stringify({
490
+ text: params.text,
491
+ ...params.durationSeconds !== void 0 ? { durationSeconds: params.durationSeconds } : {},
492
+ ...params.promptInfluence !== void 0 ? { promptInfluence: params.promptInfluence } : {},
493
+ ...params.loop !== void 0 ? { loop: params.loop } : {},
494
+ ...params.modelId ? { modelId: params.modelId } : {}
495
+ })
496
+ }
497
+ )
498
+ );
499
+ const transcribe = async (params) => {
500
+ const form = new FormData();
501
+ form.set("file", params.file, params.fileName ?? "audio");
502
+ if (params.modelId) form.set("modelId", params.modelId);
503
+ if (params.languageCode) form.set("languageCode", params.languageCode);
504
+ if (params.diarize !== void 0) form.set("diarize", String(params.diarize));
505
+ if (params.tagAudioEvents !== void 0) {
506
+ form.set("tagAudioEvents", String(params.tagAudioEvents));
507
+ }
508
+ if (params.numSpeakers !== void 0) form.set("numSpeakers", String(params.numSpeakers));
509
+ const res = await opResponse(
510
+ "speech-to-text",
511
+ new Request("http://voice.invalid/voice/op/speech-to-text", {
512
+ method: "POST",
513
+ body: form
514
+ })
515
+ );
516
+ return await res.json();
517
+ };
518
+ return new Proxy({}, {
519
+ get(_t, prop) {
520
+ if (prop === "textToSpeech") return textToSpeech;
521
+ if (prop === "composeMusic") return composeMusic;
522
+ if (prop === "speechToSpeech") return speechToSpeech;
523
+ if (prop === "soundEffects") return soundEffects;
524
+ if (prop === "transcribe") return transcribe;
525
+ return unsupported(String(prop));
526
+ }
527
+ });
528
+ }
529
+ return {
530
+ signedUrl: doSignedUrl,
531
+ tts: doTts,
532
+ music: doMusic,
533
+ agent: {
534
+ create: notImplemented("agent.create"),
535
+ patch: notImplemented("agent.patch"),
536
+ delete: notImplemented("agent.delete")
537
+ },
538
+ audioTags: doAudioTags,
539
+ op: doOp,
540
+ // In proxy mode we don't own an EL client: media calls go through the
541
+ // Shogo API, anything else is a loud error.
542
+ elevenLabs: createProxyElevenLabs()
543
+ };
544
+ }
545
+ function createVoiceHandlers(config) {
546
+ const log = config.logger ?? NOOP_LOGGER;
547
+ const proxy = resolveProxyOptions(config);
548
+ if (proxy) {
549
+ return createProxyHandlers(proxy, log);
550
+ }
551
+ if (!config.apiKey) {
552
+ throw new Error(
553
+ config.mode === "proxy" ? 'createVoiceHandlers: mode "proxy" needs RUNTIME_AUTH_SECRET + PROJECT_ID (+ SHOGO_API_URL) in env' : "createVoiceHandlers: apiKey is required (or run in a pod with RUNTIME_AUTH_SECRET + PROJECT_ID for proxy mode)"
554
+ );
555
+ }
556
+ const getUser = config.getUser;
557
+ const companionStore = config.companionStore;
558
+ const includeMemoryTools = config.includeMemoryTools ?? true;
559
+ const tools = includeMemoryTools ? MEMORY_CLIENT_TOOLS : void 0;
560
+ const elConfig = {
561
+ apiKey: config.apiKey,
562
+ ...config.elevenLabsBaseUrl ? { baseUrl: config.elevenLabsBaseUrl } : {},
563
+ ...config.fetch ? { fetch: config.fetch } : {}
564
+ };
565
+ const el = new ElevenLabsClient(elConfig);
566
+ async function resolveUser(req) {
567
+ if (!getUser) {
568
+ return json(
569
+ {
570
+ error: "Voice handler misconfigured",
571
+ detail: "signedUrl / agent.* need getUser (and companionStore); tts and music do not."
572
+ },
573
+ 500
574
+ );
575
+ }
576
+ const u = await getUser(req);
577
+ if (!u) return json({ error: "Unauthorized" }, 401);
578
+ return u;
579
+ }
580
+ function missingCompanionStore() {
581
+ return json(
582
+ {
583
+ error: "Voice handler misconfigured",
584
+ detail: "companionStore is required for signedUrl / agent.*; tts and music do not need it."
585
+ },
586
+ 500
587
+ );
588
+ }
589
+ function handleElError(e, fallbackLabel) {
590
+ if (e instanceof ElevenLabsApiError) {
591
+ log("warn", `${fallbackLabel}: 11Labs ${e.status}`, { body: e.body });
592
+ return json({ error: fallbackLabel, detail: e.body }, 502);
593
+ }
594
+ const message = e instanceof Error ? e.message : String(e);
595
+ log("error", `${fallbackLabel}: ${message}`);
596
+ return json({ error: fallbackLabel, detail: message }, 500);
597
+ }
598
+ async function doSignedUrl(req) {
599
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
600
+ const u = await resolveUser(req);
601
+ if (u instanceof Response) return u;
602
+ if (!companionStore) return missingCompanionStore();
603
+ const companion = await companionStore.findByUserId(u.id);
604
+ if (!companion || !companion.agentId) return json({ error: "No companion" }, 404);
605
+ const basePrompt = extractBasePrompt(companion.systemPrompt || "");
606
+ try {
607
+ await el.patchAgent(companion.agentId, {
608
+ systemPrompt: basePrompt,
609
+ tools,
610
+ enableUserContextOverride: includeMemoryTools,
611
+ voiceId: companion.voiceId,
612
+ expressivity: readExpressivity(companion.expressivity),
613
+ audioTags: readAudioTags(companion.audioTags),
614
+ voiceSettings: readVoiceSettings(companion.voiceSettings),
615
+ ttsModelId: companion.ttsModelId ?? null,
616
+ ...includeMemoryTools ? {} : { memoryBlock: null }
617
+ });
618
+ if (basePrompt !== companion.systemPrompt) {
619
+ await companionStore.update(u.id, { systemPrompt: basePrompt });
620
+ }
621
+ } catch (e) {
622
+ log("warn", "signedUrl: pre-sync patch failed; continuing", {
623
+ error: e instanceof Error ? e.message : String(e)
624
+ });
625
+ }
626
+ let signedUrl;
627
+ try {
628
+ signedUrl = await el.getSignedUrl(companion.agentId);
629
+ } catch (e) {
630
+ return handleElError(e, "Failed to get signed url");
631
+ }
632
+ let userContext = "";
633
+ if (config.memoryStore) {
634
+ try {
635
+ const store = config.memoryStore(u.id);
636
+ const hits = store.search(companion.characterName || "recent context", { limit: 5 });
637
+ if (hits.length) {
638
+ userContext = hits.map(
639
+ (h) => `- ${h.matchType ? `[${h.matchType}] ` : ""}${h.chunk.trim().replace(/\s+/g, " ").slice(0, 220)}`
640
+ ).join("\n");
641
+ }
642
+ } catch (e) {
643
+ log("warn", "signedUrl: memory preload failed", {
644
+ error: e instanceof Error ? e.message : String(e)
645
+ });
646
+ }
647
+ }
648
+ return json({ signedUrl, agentId: companion.agentId, userContext });
649
+ }
650
+ async function doTts(req) {
651
+ if (req.method !== "POST") return json({ error: "Method Not Allowed" }, 405);
652
+ if (getUser) {
653
+ const u = await resolveUser(req);
654
+ if (u instanceof Response) return u;
655
+ }
656
+ const raw = await readJson(req);
657
+ if (raw === null || typeof raw !== "object") return json({ error: "Invalid JSON body" }, 400);
658
+ const body = raw;
659
+ const voiceId = typeof body.voiceId === "string" ? body.voiceId.trim() : "";
660
+ if (!voiceId) return json({ error: "Missing voiceId" }, 400);
661
+ const voiceSettings = normalizeVoiceSettings(body.voiceSettings) ?? { stability: 0.5, similarity_boost: 0.8, style: 0 };
662
+ let text;
663
+ if (typeof body.text === "string" && body.text.trim().length > 0) {
664
+ text = body.text.slice(0, 400);
665
+ } else {
666
+ const tags = normalizeAudioTags(body.audioTags) ?? [];
667
+ text = buildPreviewLine(tags, body.characterName ?? "your companion").slice(0, 400);
668
+ }
669
+ try {
670
+ const result = await el.textToSpeech({
671
+ voiceId,
672
+ text,
673
+ ...body.modelId ? { modelId: body.modelId } : {},
674
+ voiceSettings
675
+ });
676
+ const headers = {
677
+ "content-type": result.contentType,
678
+ "cache-control": "private, max-age=3600",
679
+ "x-tts-model-used": result.modelId,
680
+ "x-tts-model-requested": result.requestedModel
681
+ };
682
+ if (result.fellBack) {
683
+ headers.warning = `199 - "model ${result.requestedModel} not available, used ${result.modelId}"`;
684
+ log("warn", "tts fell back to another model", {
685
+ requested: result.requestedModel,
686
+ used: result.modelId,
687
+ reason: result.fallbackReason
688
+ });
689
+ }
690
+ return new Response(result.audio, { headers });
691
+ } catch (e) {
692
+ return handleElError(e, "TTS failed");
693
+ }
694
+ }
695
+ async function doMusic(req) {
696
+ if (req.method !== "POST") return json({ error: "Method Not Allowed" }, 405);
697
+ if (getUser) {
698
+ const u = await resolveUser(req);
699
+ if (u instanceof Response) return u;
700
+ }
701
+ const raw = await readJson(req);
702
+ const parsed = parseMusicBody(raw);
703
+ if ("error" in parsed) return json({ error: parsed.error }, 400);
704
+ try {
705
+ const { audio, contentType } = await el.composeMusic(parsed.params);
706
+ return new Response(audio, {
707
+ headers: {
708
+ "content-type": contentType,
709
+ "cache-control": "private, max-age=3600"
710
+ }
711
+ });
712
+ } catch (e) {
713
+ return handleElError(e, "Music generation failed");
714
+ }
715
+ }
716
+ async function doOp(req, opId) {
717
+ const id = resolveOpId(req, opId);
718
+ const op = getVoiceOp(id);
719
+ if (!op) {
720
+ return json(
721
+ { error: "unknown_op", detail: `Unknown voice op '${id ?? ""}'`, allowed: VOICE_OP_IDS },
722
+ 404
723
+ );
724
+ }
725
+ if (req.method !== op.method) return json({ error: "Method Not Allowed" }, 405);
726
+ const parsed = await readVoiceOpBody(op, req);
727
+ if (parsed instanceof Response) return parsed;
728
+ const fields = parsed.kind === "none" ? {} : parsed.fields;
729
+ let query;
730
+ try {
731
+ query = new URL(req.url).searchParams;
732
+ } catch {
733
+ query = new URLSearchParams();
734
+ }
735
+ const pathParams = voiceOpPathParams(op, fields, query);
736
+ const upstream = voiceOpUpstreamPath(op, pathParams);
737
+ if (typeof upstream !== "string") {
738
+ return json({ error: upstream.error, detail: `set it in the body or as ?${op.pathParams?.[0]}=` }, 400);
739
+ }
740
+ const forwardedQuery = {};
741
+ for (const key of op.queryForward ?? []) {
742
+ const value = query.get(key);
743
+ if (value) forwardedQuery[key] = value;
744
+ }
745
+ if (op.defaultVoiceId && !pathParams.voiceId) {
746
+ return json(
747
+ {
748
+ error: "Missing `voiceId`",
749
+ detail: `BYO mode has no project default voice \u2014 pass voiceId (or ?voiceId=)`
750
+ },
751
+ 400
752
+ );
753
+ }
754
+ try {
755
+ if (op.call === "textToSpeech") {
756
+ const result = await el.textToSpeech({
757
+ voiceId: pathParams.voiceId ?? String(fields.voiceId ?? ""),
758
+ text: String(fields.text ?? ""),
759
+ ...typeof fields.modelId === "string" ? { modelId: fields.modelId } : {},
760
+ ...fields.voiceSettings ? { voiceSettings: fields.voiceSettings } : {},
761
+ ...op.accept ? { accept: op.accept } : {}
762
+ });
763
+ const headers2 = {
764
+ "content-type": result.contentType,
765
+ "cache-control": "private, max-age=3600",
766
+ "x-tts-model-used": result.modelId,
767
+ "x-tts-model-requested": result.requestedModel
768
+ };
769
+ if (result.fellBack) {
770
+ headers2.warning = `199 - "model ${result.requestedModel} not available, used ${result.modelId}"`;
771
+ }
772
+ return new Response(result.audio, { headers: headers2 });
773
+ }
774
+ if (op.call === "composeMusic") {
775
+ const { audio, contentType } = await el.composeMusic({
776
+ ...typeof fields.prompt === "string" ? { prompt: fields.prompt } : {},
777
+ ...fields.compositionPlan ? { compositionPlan: fields.compositionPlan } : {},
778
+ ...typeof fields.musicLengthMs === "number" ? { musicLengthMs: fields.musicLengthMs } : {},
779
+ ...typeof fields.modelId === "string" ? { modelId: fields.modelId } : {},
780
+ ...typeof fields.forceInstrumental === "boolean" ? { forceInstrumental: fields.forceInstrumental } : {},
781
+ ...typeof fields.outputFormat === "string" ? { outputFormat: fields.outputFormat } : {}
782
+ });
783
+ return new Response(audio, {
784
+ headers: {
785
+ "content-type": contentType,
786
+ "cache-control": "private, max-age=3600"
787
+ }
788
+ });
789
+ }
790
+ const upstreamBody = voiceOpUpstreamBody(op, fields);
791
+ if (op.defaultModel && upstreamBody.model_id === void 0) {
792
+ upstreamBody.model_id = op.defaultModel;
793
+ }
794
+ const response = await el.request({
795
+ method: op.method,
796
+ path: upstream,
797
+ ...parsed.kind === "form" ? { body: parsed.form } : parsed.kind === "json" ? { body: upstreamBody } : {},
798
+ ...Object.keys(forwardedQuery).length ? { query: forwardedQuery } : {},
799
+ ...op.accept ? { accept: op.accept } : {}
800
+ });
801
+ const headers = { "content-type": response.contentType };
802
+ if (voiceOpReturnsAudio(op)) headers["cache-control"] = "private, max-age=3600";
803
+ const body = response.audio ?? JSON.stringify(response.json ?? {});
804
+ return new Response(body, { status: response.status, headers });
805
+ } catch (e) {
806
+ return handleElError(e, `${op.id} failed`);
807
+ }
808
+ }
809
+ async function doAgentCreate(req) {
810
+ if (req.method !== "POST") return json({ error: "Method Not Allowed" }, 405);
811
+ const u = await resolveUser(req);
812
+ if (u instanceof Response) return u;
813
+ if (!companionStore) return missingCompanionStore();
814
+ const existing = await companionStore.findByUserId(u.id);
815
+ if (existing) return json({ error: "Companion already exists" }, 409);
816
+ const raw = await readJson(req);
817
+ if (raw === null || typeof raw !== "object") return json({ error: "Invalid JSON body" }, 400);
818
+ const body = raw;
819
+ if (typeof body.displayName !== "string" || typeof body.characterName !== "string" || typeof body.voiceId !== "string" || typeof body.systemPrompt !== "string" || typeof body.firstMessage !== "string") {
820
+ return json({ error: "Missing required fields" }, 400);
821
+ }
822
+ const expressivity = readExpressivity(body.expressivity);
823
+ const audioTags = readAudioTags(body.audioTags);
824
+ const voiceSettings = readVoiceSettings(body.voiceSettings);
825
+ const ttsModelId = resolveConvaiTtsModel(body.ttsModelId);
826
+ let agentId;
827
+ try {
828
+ agentId = await el.createAgent({
829
+ displayName: body.displayName,
830
+ characterName: body.characterName,
831
+ voiceId: body.voiceId,
832
+ systemPrompt: body.systemPrompt,
833
+ firstMessage: body.firstMessage,
834
+ expressivity,
835
+ audioTags,
836
+ voiceSettings,
837
+ ttsModelId,
838
+ ...tools ? { tools } : {},
839
+ ...includeMemoryTools ? {} : { memoryBlock: null }
840
+ });
841
+ } catch (e) {
842
+ return handleElError(e, "Failed to create agent");
843
+ }
844
+ const cleanedPrompt = extractBasePrompt(body.systemPrompt);
845
+ const companion = await companionStore.create({
846
+ userId: u.id,
847
+ agentId,
848
+ displayName: body.displayName,
849
+ characterName: body.characterName,
850
+ voiceId: body.voiceId,
851
+ systemPrompt: cleanedPrompt,
852
+ firstMessage: body.firstMessage,
853
+ expressivity,
854
+ audioTags,
855
+ voiceSettings,
856
+ ttsModelId
857
+ });
858
+ return json(companion);
859
+ }
860
+ async function doAgentPatch(req) {
861
+ if (req.method !== "PATCH") return json({ error: "Method Not Allowed" }, 405);
862
+ const u = await resolveUser(req);
863
+ if (u instanceof Response) return u;
864
+ if (!companionStore) return missingCompanionStore();
865
+ const existing = await companionStore.findByUserId(u.id);
866
+ if (!existing) return json({ error: "No companion" }, 404);
867
+ const raw = await readJson(req);
868
+ if (raw === null || typeof raw !== "object") return json({ error: "Invalid JSON body" }, 400);
869
+ const body = raw;
870
+ const patch = {};
871
+ if (body.displayName !== void 0) patch.displayName = body.displayName;
872
+ if (body.characterName !== void 0) patch.characterName = body.characterName;
873
+ if (body.voiceId !== void 0) patch.voiceId = body.voiceId;
874
+ if (body.firstMessage !== void 0) patch.firstMessage = body.firstMessage;
875
+ const expressivity = normalizeExpressivity(body.expressivity);
876
+ if (expressivity !== void 0) patch.expressivity = expressivity;
877
+ let audioTagsPatch;
878
+ if (body.audioTags !== void 0) {
879
+ audioTagsPatch = normalizeAudioTags(body.audioTags) ?? [];
880
+ patch.audioTags = audioTagsPatch;
881
+ }
882
+ if (body.voiceSettings !== void 0) {
883
+ const vs = normalizeVoiceSettings(body.voiceSettings);
884
+ patch.voiceSettings = vs ?? null;
885
+ }
886
+ if (body.ttsModelId !== void 0) patch.ttsModelId = body.ttsModelId ?? null;
887
+ const cleanedPrompt = body.systemPrompt !== void 0 ? extractBasePrompt(body.systemPrompt) : void 0;
888
+ if (cleanedPrompt !== void 0) patch.systemPrompt = cleanedPrompt;
889
+ const needs11Labs = body.displayName !== void 0 || body.characterName !== void 0 || body.voiceId !== void 0 || body.systemPrompt !== void 0 || body.firstMessage !== void 0 || body.expressivity !== void 0 || body.audioTags !== void 0 || body.voiceSettings !== void 0 || body.ttsModelId !== void 0;
890
+ if (existing.agentId && needs11Labs) {
891
+ const finalExpressivity = patch.expressivity ?? readExpressivity(existing.expressivity);
892
+ const finalTags = audioTagsPatch !== void 0 ? audioTagsPatch : readAudioTags(existing.audioTags);
893
+ const finalVoiceSettings = body.voiceSettings !== void 0 ? normalizeVoiceSettings(body.voiceSettings) ?? void 0 : readVoiceSettings(existing.voiceSettings);
894
+ const finalTtsModelId = body.ttsModelId !== void 0 ? body.ttsModelId ?? null : existing.ttsModelId ?? null;
895
+ try {
896
+ await el.patchAgent(existing.agentId, {
897
+ ...body.displayName !== void 0 ? { displayName: body.displayName } : {},
898
+ ...body.characterName !== void 0 ? { characterName: body.characterName } : {},
899
+ ...body.voiceId !== void 0 ? { voiceId: body.voiceId } : {},
900
+ systemPrompt: cleanedPrompt !== void 0 ? cleanedPrompt : body.expressivity !== void 0 || body.audioTags !== void 0 ? existing.systemPrompt : void 0,
901
+ ...body.firstMessage !== void 0 ? { firstMessage: body.firstMessage } : {},
902
+ expressivity: finalExpressivity,
903
+ audioTags: finalTags,
904
+ ...finalVoiceSettings ? { voiceSettings: finalVoiceSettings } : {},
905
+ ttsModelId: finalTtsModelId,
906
+ ...includeMemoryTools ? {} : { memoryBlock: null }
907
+ });
908
+ } catch (e) {
909
+ return handleElError(e, "Failed to patch agent");
910
+ }
911
+ }
912
+ const companion = await companionStore.update(u.id, patch);
913
+ return json(companion);
914
+ }
915
+ async function doAgentDelete(req) {
916
+ if (req.method !== "DELETE") return json({ error: "Method Not Allowed" }, 405);
917
+ const u = await resolveUser(req);
918
+ if (u instanceof Response) return u;
919
+ if (!companionStore) return missingCompanionStore();
920
+ const existing = await companionStore.findByUserId(u.id);
921
+ if (!existing) return json({ ok: true });
922
+ if (existing.agentId) await el.deleteAgent(existing.agentId);
923
+ await companionStore.delete(u.id);
924
+ return json({ ok: true });
925
+ }
926
+ async function doAudioTags(req) {
927
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
928
+ return json({
929
+ tags: AUDIO_TAGS,
930
+ groups: AUDIO_TAG_GROUPS,
931
+ expressivity: EXPRESSIVITY_OPTIONS,
932
+ defaults: {
933
+ allowedTags: DEFAULT_ALLOWED_TAGS,
934
+ voiceSettings: DEFAULT_VOICE_SETTINGS,
935
+ ttsModelId: CONVAI_TTS_MODEL_FALLBACK
936
+ }
937
+ });
938
+ }
939
+ return {
940
+ signedUrl: doSignedUrl,
941
+ tts: doTts,
942
+ music: doMusic,
943
+ agent: {
944
+ create: doAgentCreate,
945
+ patch: doAgentPatch,
946
+ delete: doAgentDelete
947
+ },
948
+ audioTags: doAudioTags,
949
+ op: doOp,
950
+ elevenLabs: el
951
+ };
952
+ }
953
+ function toNodeListener(handler) {
954
+ return (nodeReq, nodeRes) => {
955
+ void (async () => {
956
+ const url = `http://${nodeReq.headers.host ?? "localhost"}${nodeReq.url ?? "/"}`;
957
+ const chunks = [];
958
+ for await (const chunk of nodeReq) {
959
+ chunks.push(chunk);
960
+ }
961
+ const body = Buffer.concat(chunks);
962
+ const init = {
963
+ method: nodeReq.method,
964
+ headers: nodeReq.headers
965
+ };
966
+ if (body.length > 0) {
967
+ init.body = body;
968
+ }
969
+ const req = new Request(url, init);
970
+ const res = await handler(req);
971
+ nodeRes.statusCode = res.status;
972
+ res.headers.forEach((value, key) => {
973
+ nodeRes.setHeader(key, value);
974
+ });
975
+ const buf = Buffer.from(await res.arrayBuffer());
976
+ nodeRes.end(buf);
977
+ })().catch((err) => {
978
+ nodeRes.statusCode = 500;
979
+ nodeRes.end(err instanceof Error ? err.message : String(err));
980
+ });
981
+ };
982
+ }
983
+
984
+ export {
985
+ createVoiceHandlers,
986
+ toNodeListener
987
+ };
988
+ //# sourceMappingURL=chunk-U5UC7ZVS.js.map