@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,2127 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/route/op.ts
21
+ var op_exports = {};
22
+ __export(op_exports, {
23
+ GET: () => GET,
24
+ POST: () => POST,
25
+ voiceOpRoutes: () => voiceOpRoutes
26
+ });
27
+ module.exports = __toCommonJS(op_exports);
28
+
29
+ // src/audioTags.ts
30
+ var AUDIO_TAGS = [
31
+ // Emotions
32
+ { tag: "happy", group: "emotion", label: "Happy", description: "Warm, upbeat delivery.", example: "[happy] Oh, I\u2019m so glad you stopped by." },
33
+ { tag: "sad", group: "emotion", label: "Sad", description: "Subdued, heavy-hearted tone.", example: "[sad] I wish things had gone differently." },
34
+ { tag: "excited", group: "emotion", label: "Excited", description: "Energized, eager delivery.", example: "[excited] Wait until you hear this!" },
35
+ { tag: "curious", group: "emotion", label: "Curious", description: "Inquisitive, rising inflection.", example: "[curious] Hmm, what did they look like?" },
36
+ { tag: "sarcastic", group: "emotion", label: "Sarcastic", description: "Dry, ironic tone.", example: "[sarcastic] Oh, fantastic. Another Monday." },
37
+ { tag: "angry", group: "emotion", label: "Angry", description: "Sharp, irritated edge.", example: "[angry] I told you not to touch that." },
38
+ { tag: "nervous", group: "emotion", label: "Nervous", description: "Uneasy, faltering delivery.", example: "[nervous] I\u2014 I didn\u2019t think you\u2019d actually come." },
39
+ { tag: "confident", group: "emotion", label: "Confident", description: "Grounded, assured delivery.", example: "[confident] Trust me. I\u2019ve done this before." },
40
+ // Delivery
41
+ { tag: "whispers", group: "delivery", label: "Whispers", description: "Hushed, close-mic delivery.", example: "[whispers] Don\u2019t say it too loud." },
42
+ { tag: "shouts", group: "delivery", label: "Shouts", description: "Loud, projected delivery.", example: "[shouts] Look out behind you!" },
43
+ { tag: "softly", group: "delivery", label: "Softly", description: "Gentle, quiet tone.", example: "[softly] It\u2019s okay. I\u2019m here." },
44
+ { tag: "slowly", group: "delivery", label: "Slowly", description: "Deliberate pacing.", example: "[slowly] Tell me\u2026 exactly\u2026 what happened." },
45
+ { tag: "quickly", group: "delivery", label: "Quickly", description: "Rapid, urgent pacing.", example: "[quickly] We need to go, right now." },
46
+ // Reactions (non-verbal sounds)
47
+ { tag: "laughs", group: "reaction", label: "Laughs", description: "A genuine laugh.", example: "That\u2019s ridiculous [laughs]." },
48
+ { tag: "chuckles", group: "reaction", label: "Chuckles", description: "A small, warm chuckle.", example: "[chuckles] You always say that." },
49
+ { tag: "sighs", group: "reaction", label: "Sighs", description: "An audible sigh.", example: "[sighs] Alright, fine. Let\u2019s do it." },
50
+ { tag: "gasps", group: "reaction", label: "Gasps", description: "A sharp inhale of surprise.", example: "[gasps] You didn\u2019t!" },
51
+ { tag: "hesitates", group: "reaction", label: "Hesitates", description: "A pause with uncertainty.", example: "[hesitates] I\u2019m not sure I should say." },
52
+ { tag: "clears throat", group: "reaction", label: "Clears throat", description: "Throat-clearing beat.", example: "[clears throat] As I was saying\u2026" }
53
+ ];
54
+ var AUDIO_TAG_GROUPS = [
55
+ { id: "emotion", label: "Emotions", description: "Overall feeling carried through the line." },
56
+ { id: "delivery", label: "Delivery", description: "How the words are spoken (pace, volume)." },
57
+ { id: "reaction", label: "Reactions", description: "Non-verbal sounds mixed into speech." }
58
+ ];
59
+ var EXPRESSIVITY_OPTIONS = [
60
+ { id: "off", label: "Off", description: "No audio tags. Plain, natural delivery." },
61
+ { id: "subtle", label: "Subtle", description: "Occasional tag when it genuinely fits. Default." },
62
+ { id: "full", label: "Full", description: "Freely expressive \u2014 tags sprinkled to carry emotion and reactions." }
63
+ ];
64
+ var DEFAULT_ALLOWED_TAGS = [
65
+ "whispers",
66
+ "laughs",
67
+ "sighs",
68
+ "excited",
69
+ "curious",
70
+ "softly"
71
+ ];
72
+ var DEFAULT_VOICE_SETTINGS = {
73
+ stability: 0.5,
74
+ similarity_boost: 0.8,
75
+ style: 0,
76
+ use_speaker_boost: true
77
+ };
78
+ var KNOWN_TAG_SET = new Set(AUDIO_TAGS.map((t) => t.tag.toLowerCase()));
79
+ var EXPRESSIVITY_BLOCK_OPEN = "<!--expressivity-->";
80
+ var EXPRESSIVITY_BLOCK_CLOSE = "<!--/expressivity-->";
81
+ function stripExpressivityBlock(prompt) {
82
+ if (!prompt) return prompt;
83
+ const open = EXPRESSIVITY_BLOCK_OPEN;
84
+ const close = EXPRESSIVITY_BLOCK_CLOSE;
85
+ const openIdx = prompt.indexOf(open);
86
+ if (openIdx === -1) return prompt;
87
+ const closeIdx = prompt.indexOf(close, openIdx);
88
+ if (closeIdx === -1) return prompt;
89
+ const before = prompt.slice(0, openIdx).replace(/\s+$/, "");
90
+ const after = prompt.slice(closeIdx + close.length).replace(/^\s+/, "");
91
+ return [before, after].filter(Boolean).join("\n\n");
92
+ }
93
+ function composeExpressivityBlock(expressivity, allowedTags) {
94
+ if (expressivity === "off") return "";
95
+ const allowed = (allowedTags && allowedTags.length > 0 ? allowedTags : DEFAULT_ALLOWED_TAGS).filter((t) => AUDIO_TAGS.some((a) => a.tag === t));
96
+ if (allowed.length === 0) return "";
97
+ const intensity = expressivity === "full" ? "Use audio tags liberally to color your speech with emotion, delivery, and non-verbal reactions. Most substantive replies should include at least one tag. Never add so many tags that a line becomes unnatural." : "Use audio tags sparingly \u2014 only when one genuinely fits the emotion or reaction of the moment. Most lines will have no tag. At most one or two tags per reply.";
98
+ const tagList = allowed.map((t) => {
99
+ const entry = AUDIO_TAGS.find((a) => a.tag === t);
100
+ return ` - [${entry.tag}] \u2014 ${entry.description} Example: ${entry.example}`;
101
+ }).join("\n");
102
+ const body = [
103
+ "You speak through an expressive TTS engine that understands inline audio tags.",
104
+ 'Audio tags are lowercase words in square brackets placed directly in your spoken text \u2014 e.g. "[whispers] don\u2019t tell anyone" or "that was wild [laughs]".',
105
+ 'Tags are performance cues, not words. Never describe a tag (do not say "asterisk laughs asterisk" or "*laughs*"). Never read a tag aloud. Never put tags inside quoted speech from another character.',
106
+ intensity,
107
+ "You may only use tags from this allow-list:",
108
+ tagList,
109
+ "If no tag fits, use none. Tags are optional flavor, not filler."
110
+ ].join("\n");
111
+ return `${EXPRESSIVITY_BLOCK_OPEN}
112
+ ${body}
113
+ ${EXPRESSIVITY_BLOCK_CLOSE}`;
114
+ }
115
+ function buildPreviewLine(allowedTags, characterName = "your companion") {
116
+ const tags = (allowedTags ?? []).filter((t) => AUDIO_TAGS.some((a) => a.tag === t));
117
+ if (tags.length === 0) {
118
+ return `Hi there. This is a quick preview of how ${characterName} sounds.`;
119
+ }
120
+ const pick = tags.slice(0, 3);
121
+ const lead = pick[0];
122
+ const mid = pick[1];
123
+ const tail = pick[2];
124
+ if (pick.length === 1) {
125
+ return `[${lead}] Hey \u2014 this is how ${characterName} sounds when the mood takes over.`;
126
+ }
127
+ if (pick.length === 2) {
128
+ return `[${lead}] Oh, this is fun. [${mid}] Let me know what you think.`;
129
+ }
130
+ return `[${lead}] Okay, listen. [${mid}] This is the new voice. [${tail}] How does it feel?`;
131
+ }
132
+ function normalizeAudioTags(raw) {
133
+ if (!Array.isArray(raw)) return null;
134
+ const cleaned = raw.filter((x) => typeof x === "string").map((x) => x.trim().toLowerCase()).filter((x) => AUDIO_TAGS.some((a) => a.tag === x));
135
+ return Array.from(new Set(cleaned));
136
+ }
137
+ function normalizeExpressivity(raw) {
138
+ if (raw === "off" || raw === "subtle" || raw === "full") return raw;
139
+ return void 0;
140
+ }
141
+ function normalizeVoiceSettings(raw) {
142
+ if (!raw || typeof raw !== "object") return void 0;
143
+ const src = raw;
144
+ const out = {};
145
+ const clamp = (v) => typeof v === "number" && Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : void 0;
146
+ const stability = clamp(src.stability);
147
+ const similarity = clamp(src.similarity_boost ?? src.similarityBoost);
148
+ const style = clamp(src.style);
149
+ const boost = src.use_speaker_boost ?? src.useSpeakerBoost;
150
+ if (stability !== void 0) out.stability = stability;
151
+ if (similarity !== void 0) out.similarity_boost = similarity;
152
+ if (style !== void 0) out.style = style;
153
+ if (typeof boost === "boolean") out.use_speaker_boost = boost;
154
+ return Object.keys(out).length ? out : void 0;
155
+ }
156
+ function readAudioTags(raw) {
157
+ const norm = normalizeAudioTags(raw);
158
+ return norm && norm.length > 0 ? norm : DEFAULT_ALLOWED_TAGS;
159
+ }
160
+ function readExpressivity(raw) {
161
+ return normalizeExpressivity(raw) ?? "subtle";
162
+ }
163
+ function readVoiceSettings(raw) {
164
+ return normalizeVoiceSettings(raw) ?? { ...DEFAULT_VOICE_SETTINGS };
165
+ }
166
+
167
+ // src/prompt.ts
168
+ var DEFAULT_MEMORY_BLOCK = `# Memory
169
+
170
+ Known context about this user from past conversations:
171
+ {{user_context}}
172
+
173
+ Memory retrieval is automatic \u2014 the top matching facts are already injected above. Never say "let me check my memory" or "searching memories."
174
+
175
+ You have one tool:
176
+ - add_memory(fact): save a concise, canonical fact about the user \u2014 preferences, decisions, personal details, follow-ups. Use short normalized phrasings like "favorite color: green" or "lives in Honolulu". Always call it when the user explicitly asks you to remember something.`;
177
+ function stripMemoryBlock(prompt) {
178
+ if (!prompt) return prompt;
179
+ return prompt.replace(/\n*<!-- zix-memory-v\d+ -->[\s\S]*$/m, "").replace(/\n*# Memory\s*\n\s*Known context about this user[\s\S]*$/m, "").trimEnd();
180
+ }
181
+ function extractBasePrompt(prompt) {
182
+ return stripExpressivityBlock(stripMemoryBlock(prompt));
183
+ }
184
+ function composeAgentPrompt(basePrompt, options = {}) {
185
+ const { expressivity = "subtle", audioTags = null, memoryBlock = DEFAULT_MEMORY_BLOCK } = options;
186
+ const stripped = extractBasePrompt(basePrompt);
187
+ const expr = composeExpressivityBlock(expressivity, audioTags);
188
+ const parts = [stripped];
189
+ if (expr) parts.push(expr);
190
+ if (memoryBlock) parts.push(memoryBlock);
191
+ return parts.join("\n\n");
192
+ }
193
+
194
+ // src/elevenlabs.ts
195
+ var DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
196
+ var VOICE_TTS_MODELS = [
197
+ { id: "eleven_v4", label: "Eleven v4", realtime: false, maxChars: 1e4 },
198
+ { id: "eleven_v4_turbo", label: "Eleven v4 Turbo", realtime: true, maxChars: 1e4 },
199
+ { id: "eleven_v3", label: "Eleven v3", realtime: false, maxChars: 5e3 },
200
+ {
201
+ id: "eleven_v3_conversational",
202
+ label: "Eleven v3 Conversational",
203
+ realtime: true,
204
+ maxChars: 5e3
205
+ },
206
+ { id: "eleven_multilingual_v2", label: "Eleven Multilingual v2", realtime: false, maxChars: 1e4 },
207
+ { id: "eleven_flash_v2_5", label: "Eleven Flash v2.5", realtime: true, maxChars: 4e4 },
208
+ { id: "eleven_flash_v2", label: "Eleven Flash v2", realtime: true, maxChars: 3e4 },
209
+ { id: "eleven_turbo_v2_5", label: "Eleven Turbo v2.5", realtime: true, maxChars: 4e4 },
210
+ { id: "eleven_turbo_v2", label: "Eleven Turbo v2", realtime: true, maxChars: 3e4, deprecated: true }
211
+ ];
212
+ var VOICE_TTS_MODEL_IDS = VOICE_TTS_MODELS.map((m) => m.id);
213
+ var DEFAULT_TTS_MODEL = "eleven_v4";
214
+ var CONVAI_SUPPORTED_TTS_MODELS = new Set(
215
+ VOICE_TTS_MODELS.filter((m) => m.realtime).map((m) => m.id)
216
+ );
217
+ var CONVAI_TTS_MODEL_FALLBACK = "eleven_v4_turbo";
218
+ var TTS_FALLBACK_MODEL = "eleven_flash_v2_5";
219
+ function getVoiceTtsModel(modelId) {
220
+ if (!modelId) return void 0;
221
+ return VOICE_TTS_MODELS.find((m) => m.id === modelId);
222
+ }
223
+ var loggedDowngrades = /* @__PURE__ */ new Set();
224
+ function resolveConvaiTtsModelDetailed(requested) {
225
+ if (requested && CONVAI_SUPPORTED_TTS_MODELS.has(requested)) {
226
+ return { modelId: requested, requested, downgraded: false };
227
+ }
228
+ const known = getVoiceTtsModel(requested);
229
+ const reason = !requested ? void 0 : known ? `${requested} is not supported by the convai realtime runtime` : `unknown TTS model ${requested}`;
230
+ if (reason && !loggedDowngrades.has(requested)) {
231
+ loggedDowngrades.add(requested);
232
+ try {
233
+ console.warn(
234
+ `[voice] convai TTS model ${requested} is unavailable (${reason}); using ${CONVAI_TTS_MODEL_FALLBACK}`
235
+ );
236
+ } catch {
237
+ }
238
+ }
239
+ return {
240
+ modelId: CONVAI_TTS_MODEL_FALLBACK,
241
+ requested: requested ?? null,
242
+ downgraded: Boolean(requested),
243
+ ...reason ? { reason } : {}
244
+ };
245
+ }
246
+ function resolveConvaiTtsModel(requested) {
247
+ return resolveConvaiTtsModelDetailed(requested).modelId;
248
+ }
249
+ function isModelRelatedTtsFailure(status, body) {
250
+ if (status === 402) return true;
251
+ if (status !== 400 && status !== 403 && status !== 404 && status !== 422) return false;
252
+ return /model|plan|permission/i.test(body);
253
+ }
254
+ var DEFAULT_MUSIC_MODEL = "music_v1";
255
+ var VOICE_STS_MODEL_IDS = [
256
+ "eleven_multilingual_sts_v2",
257
+ "eleven_english_sts_v2"
258
+ ];
259
+ var DEFAULT_STS_MODEL = "eleven_multilingual_sts_v2";
260
+ var VOICE_SPEECH_TO_TEXT_MODEL_IDS = ["scribe_v1"];
261
+ var DEFAULT_SPEECH_TO_TEXT_MODEL = "scribe_v1";
262
+ var VOICE_SOUND_GENERATION_MODEL_IDS = ["eleven_text_to_sound_v2"];
263
+ var DEFAULT_SOUND_GENERATION_MODEL = "eleven_text_to_sound_v2";
264
+ var ElevenLabsApiError = class extends Error {
265
+ constructor(message, status, body) {
266
+ super(message);
267
+ this.status = status;
268
+ this.body = body;
269
+ this.name = "ElevenLabsApiError";
270
+ }
271
+ };
272
+ var ElevenLabsClient = class {
273
+ constructor(config) {
274
+ this.config = config;
275
+ if (!config.apiKey) throw new Error("ElevenLabsClient: apiKey is required");
276
+ this.baseUrl = (config.baseUrl ?? DEFAULT_ELEVENLABS_BASE_URL).replace(/\/+$/, "");
277
+ this.fetchImpl = config.fetch ?? globalThis.fetch;
278
+ if (typeof this.fetchImpl !== "function") {
279
+ throw new Error("ElevenLabsClient: global fetch is unavailable; pass config.fetch");
280
+ }
281
+ }
282
+ headers(extra = {}) {
283
+ return {
284
+ "xi-api-key": this.config.apiKey,
285
+ ...extra
286
+ };
287
+ }
288
+ /** Create a new convai agent. Returns the new `agent_id`. */
289
+ async createAgent(params) {
290
+ const prompt = composeAgentPrompt(params.systemPrompt, {
291
+ expressivity: params.expressivity ?? "subtle",
292
+ audioTags: params.audioTags ?? null,
293
+ memoryBlock: params.memoryBlock
294
+ });
295
+ const ttsConfig = {
296
+ voice_id: params.voiceId,
297
+ model_id: resolveConvaiTtsModel(params.ttsModelId)
298
+ };
299
+ if (params.voiceSettings) ttsConfig.voice_settings = params.voiceSettings;
300
+ const body = {
301
+ name: `Companion-${params.characterName}-for-${params.displayName}`,
302
+ conversation_config: {
303
+ agent: {
304
+ prompt: {
305
+ prompt,
306
+ ...params.tools ? { tools: params.tools } : {}
307
+ },
308
+ first_message: params.firstMessage,
309
+ language: params.language ?? "en"
310
+ },
311
+ tts: ttsConfig
312
+ },
313
+ platform_settings: {
314
+ overrides: {
315
+ conversation_config_override: {
316
+ agent: { prompt: { prompt: true }, first_message: true, language: true },
317
+ tts: { voice_id: true },
318
+ conversation: { text_only: true }
319
+ }
320
+ },
321
+ dynamic_variables: {
322
+ dynamic_variable_placeholders: {
323
+ user_context: "No prior memories yet.",
324
+ character_name: "",
325
+ ...params.dynamicVariablePlaceholders ?? {}
326
+ }
327
+ }
328
+ }
329
+ };
330
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/create`, {
331
+ method: "POST",
332
+ headers: this.headers({ "content-type": "application/json" }),
333
+ body: JSON.stringify(body)
334
+ });
335
+ if (!res.ok) {
336
+ const text = await res.text();
337
+ throw new ElevenLabsApiError(`createAgent failed: ${res.status}`, res.status, text);
338
+ }
339
+ const data = await res.json();
340
+ return data.agent_id;
341
+ }
342
+ /** PATCH any subset of agent settings. Idempotent; only sends the keys you supply. */
343
+ async patchAgent(agentId, params) {
344
+ const body = {};
345
+ if (params.displayName !== void 0 || params.characterName !== void 0) {
346
+ body.name = `Companion-${params.characterName ?? ""}-for-${params.displayName ?? ""}`.replace(
347
+ /-for-$/,
348
+ ""
349
+ );
350
+ }
351
+ const conv = {};
352
+ const agent = {};
353
+ const prompt = {};
354
+ if (params.systemPrompt !== void 0) {
355
+ prompt.prompt = composeAgentPrompt(params.systemPrompt, {
356
+ expressivity: params.expressivity ?? "subtle",
357
+ audioTags: params.audioTags ?? null,
358
+ memoryBlock: params.memoryBlock
359
+ });
360
+ }
361
+ if (params.tools) prompt.tools = params.tools;
362
+ if (Object.keys(prompt).length) agent.prompt = prompt;
363
+ if (params.firstMessage !== void 0) agent.first_message = params.firstMessage;
364
+ if (Object.keys(agent).length) conv.agent = agent;
365
+ const tts = {};
366
+ if (params.voiceId !== void 0) tts.voice_id = params.voiceId;
367
+ if (params.voiceSettings) tts.voice_settings = params.voiceSettings;
368
+ if (params.ttsModelId !== void 0) tts.model_id = resolveConvaiTtsModel(params.ttsModelId);
369
+ if (Object.keys(tts).length) conv.tts = tts;
370
+ if (Object.keys(conv).length) body.conversation_config = conv;
371
+ if (params.enableUserContextOverride) {
372
+ body.platform_settings = {
373
+ overrides: {
374
+ conversation_config_override: {
375
+ agent: { prompt: { prompt: true }, first_message: true, language: true },
376
+ tts: { voice_id: true },
377
+ conversation: { text_only: true }
378
+ },
379
+ custom_llm_extra_body: false,
380
+ enable_conversation_initiation_client_data_from_webhook: false
381
+ },
382
+ dynamic_variables: {
383
+ dynamic_variable_placeholders: {
384
+ user_context: "No prior memories yet.",
385
+ character_name: ""
386
+ }
387
+ }
388
+ };
389
+ }
390
+ if (!Object.keys(body).length) return;
391
+ const res = await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/${encodeURIComponent(agentId)}`, {
392
+ method: "PATCH",
393
+ headers: this.headers({ "content-type": "application/json" }),
394
+ body: JSON.stringify(body)
395
+ });
396
+ if (!res.ok) {
397
+ const text = await res.text();
398
+ throw new ElevenLabsApiError(`patchAgent failed: ${res.status}`, res.status, text);
399
+ }
400
+ }
401
+ /**
402
+ * Delete a convai agent. Best-effort: swallow errors so teardown during a
403
+ * 500 response doesn't cascade into a second failure.
404
+ */
405
+ async deleteAgent(agentId) {
406
+ try {
407
+ await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/${encodeURIComponent(agentId)}`, {
408
+ method: "DELETE",
409
+ headers: this.headers()
410
+ });
411
+ } catch {
412
+ }
413
+ }
414
+ /** Fetch a short-lived signed URL the browser can use to start a session. */
415
+ async getSignedUrl(agentId) {
416
+ const res = await this.fetchImpl(
417
+ `${this.baseUrl}/v1/convai/conversation/get-signed-url?agent_id=${encodeURIComponent(agentId)}`,
418
+ { headers: this.headers() }
419
+ );
420
+ if (!res.ok) {
421
+ const text = await res.text();
422
+ throw new ElevenLabsApiError(`getSignedUrl failed: ${res.status}`, res.status, text);
423
+ }
424
+ const data = await res.json();
425
+ return data.signed_url;
426
+ }
427
+ /**
428
+ * One-shot TTS. Returns the raw audio bytes plus the model that actually
429
+ * produced them.
430
+ *
431
+ * The requested model is never swapped silently: we only retry on
432
+ * {@link TTS_FALLBACK_MODEL} when the failure is plausibly about
433
+ * the model (400/403/404/422 whose body mentions model/plan/permission,
434
+ * or 402 for an out-of-plan model). Auth (401), rate limit (429), 5xx and
435
+ * network errors are surfaced as-is. When a fallback does happen the
436
+ * result reports `requestedModel`, `fellBack` and `fallbackReason`.
437
+ */
438
+ async textToSpeech(params) {
439
+ const requestedModel = params.modelId && params.modelId.length > 0 ? params.modelId : DEFAULT_TTS_MODEL;
440
+ const accept = params.accept ?? "audio/mpeg";
441
+ const attempt = async (m) => {
442
+ const res2 = await this.fetchImpl(
443
+ `${this.baseUrl}/v1/text-to-speech/${encodeURIComponent(params.voiceId)}`,
444
+ {
445
+ method: "POST",
446
+ headers: this.headers({ "content-type": "application/json", accept }),
447
+ body: JSON.stringify({
448
+ text: params.text,
449
+ model_id: m,
450
+ voice_settings: params.voiceSettings
451
+ })
452
+ }
453
+ );
454
+ return res2;
455
+ };
456
+ let res = await attempt(requestedModel);
457
+ let used = requestedModel;
458
+ let fellBack = false;
459
+ let fallbackReason;
460
+ if (!res.ok) {
461
+ const body = await res.text();
462
+ if (requestedModel !== TTS_FALLBACK_MODEL && isModelRelatedTtsFailure(res.status, body)) {
463
+ fallbackReason = `elevenlabs ${res.status} for ${requestedModel}: ${body.slice(0, 200)}`;
464
+ res = await attempt(TTS_FALLBACK_MODEL);
465
+ used = TTS_FALLBACK_MODEL;
466
+ fellBack = true;
467
+ if (!res.ok) {
468
+ const retryBody = await res.text();
469
+ throw new ElevenLabsApiError(
470
+ `textToSpeech failed: ${res.status}`,
471
+ res.status,
472
+ retryBody
473
+ );
474
+ }
475
+ } else {
476
+ throw new ElevenLabsApiError(`textToSpeech failed: ${res.status}`, res.status, body);
477
+ }
478
+ }
479
+ const audio = await res.arrayBuffer();
480
+ const contentType = res.headers.get("content-type") ?? accept;
481
+ return {
482
+ audio,
483
+ modelId: used,
484
+ requestedModel,
485
+ fellBack,
486
+ ...fallbackReason ? { fallbackReason } : {},
487
+ contentType
488
+ };
489
+ }
490
+ /**
491
+ * Low-level escape hatch for any ElevenLabs endpoint not covered by a
492
+ * dedicated method (music, sound-generation, speech-to-text, dubbing,
493
+ * voice-design, …). Handles auth, query encoding, JSON body encoding,
494
+ * and JSON-vs-binary response parsing; non-2xx responses throw
495
+ * {@link ElevenLabsApiError} like the typed methods do.
496
+ *
497
+ * @example
498
+ * ```ts
499
+ * const { json } = await el.request({ method: 'GET', path: '/v1/models' })
500
+ * const { audio } = await el.request({
501
+ * method: 'POST',
502
+ * path: '/v1/sound-generation',
503
+ * body: { text: 'door creak' },
504
+ * accept: 'audio/mpeg',
505
+ * })
506
+ * ```
507
+ */
508
+ async request(opts) {
509
+ const accept = opts.accept ?? "application/json";
510
+ let url = `${this.baseUrl}${opts.path.startsWith("/") ? "" : "/"}${opts.path}`;
511
+ if (opts.query) {
512
+ const params = new URLSearchParams();
513
+ for (const [k, v] of Object.entries(opts.query)) {
514
+ if (v !== null && v !== void 0) params.set(k, String(v));
515
+ }
516
+ const qs = params.toString();
517
+ if (qs) url += `${url.includes("?") ? "&" : "?"}${qs}`;
518
+ }
519
+ const headers = this.headers({ accept, ...opts.headers ?? {} });
520
+ const init = { method: opts.method, headers };
521
+ if (opts.body !== void 0 && opts.body !== null) {
522
+ const isPlainPayload = typeof opts.body === "object" && !(opts.body instanceof ArrayBuffer) && !(opts.body instanceof Uint8Array) && !(typeof Blob !== "undefined" && opts.body instanceof Blob) && !(typeof FormData !== "undefined" && opts.body instanceof FormData);
523
+ if (isPlainPayload) {
524
+ headers["content-type"] = "application/json";
525
+ init.body = JSON.stringify(opts.body);
526
+ } else {
527
+ init.body = opts.body;
528
+ }
529
+ }
530
+ const res = await this.fetchImpl(url, init);
531
+ if (!res.ok) {
532
+ const text = await res.text();
533
+ throw new ElevenLabsApiError(
534
+ `request ${opts.method} ${opts.path} failed: ${res.status}`,
535
+ res.status,
536
+ text
537
+ );
538
+ }
539
+ const contentType = res.headers.get("content-type") ?? accept;
540
+ if (contentType.includes("application/json")) {
541
+ return { status: res.status, contentType, json: await res.json() };
542
+ }
543
+ return { status: res.status, contentType, audio: await res.arrayBuffer() };
544
+ }
545
+ /**
546
+ * Generate music via `POST /v1/music`. Provide exactly one of `prompt`
547
+ * or `compositionPlan` (ElevenLabs returns 422 otherwise). Returns the
548
+ * raw audio bytes plus the response content type.
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * const { audio } = await el.composeMusic({
553
+ * prompt: 'Upbeat synthwave with driving bass',
554
+ * musicLengthMs: 30_000,
555
+ * })
556
+ * ```
557
+ */
558
+ async composeMusic(params) {
559
+ const hasPrompt = typeof params.prompt === "string" && params.prompt.length > 0;
560
+ const hasPlan = params.compositionPlan != null;
561
+ if (hasPrompt === hasPlan) {
562
+ throw new Error(
563
+ "composeMusic: provide exactly one of `prompt` or `compositionPlan`"
564
+ );
565
+ }
566
+ const body = {
567
+ model_id: params.modelId ?? DEFAULT_MUSIC_MODEL
568
+ };
569
+ if (hasPrompt) {
570
+ body.prompt = params.prompt;
571
+ if (params.musicLengthMs !== void 0) body.music_length_ms = params.musicLengthMs;
572
+ if (params.forceInstrumental !== void 0) {
573
+ body.force_instrumental = params.forceInstrumental;
574
+ }
575
+ } else {
576
+ body.composition_plan = params.compositionPlan;
577
+ }
578
+ const { audio, contentType } = await this.request({
579
+ method: "POST",
580
+ path: "/v1/music",
581
+ body,
582
+ accept: "audio/mpeg",
583
+ ...params.outputFormat ? { query: { output_format: params.outputFormat } } : {}
584
+ });
585
+ if (!audio) {
586
+ throw new ElevenLabsApiError(
587
+ "composeMusic: expected audio bytes but got a JSON response",
588
+ 500,
589
+ ""
590
+ );
591
+ }
592
+ return { audio, contentType };
593
+ }
594
+ /**
595
+ * Convert speech to speech — the "voice changer": keep the performance,
596
+ * swap the voice. `POST /v1/speech-to-speech/{voiceId}` with a multipart
597
+ * audio part. Returns the converted audio bytes.
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * const { audio } = await el.speechToSpeech({
602
+ * voiceId: '21m00Tcm4TlvDq8ikWAM',
603
+ * audio: await fetch(myRecording).then((r) => r.blob()),
604
+ * modelId: 'eleven_multilingual_sts_v2',
605
+ * })
606
+ * ```
607
+ */
608
+ async speechToSpeech(params) {
609
+ if (!params.voiceId) throw new Error("speechToSpeech: voiceId is required");
610
+ if (!params.audio) throw new Error("speechToSpeech: audio is required");
611
+ const form = new FormData();
612
+ form.set("audio", params.audio, params.fileName ?? "audio");
613
+ form.set("model_id", params.modelId ?? DEFAULT_STS_MODEL);
614
+ if (params.voiceSettings) {
615
+ form.set("voice_settings", JSON.stringify(params.voiceSettings));
616
+ }
617
+ if (params.outputFormat) form.set("output_format", params.outputFormat);
618
+ if (params.fileFormat) form.set("file_format", params.fileFormat);
619
+ if (params.seed !== void 0) form.set("seed", String(params.seed));
620
+ if (params.removeBackgroundNoise !== void 0) {
621
+ form.set("remove_background_noise", String(params.removeBackgroundNoise));
622
+ }
623
+ const { audio, contentType } = await this.request({
624
+ method: "POST",
625
+ path: `/v1/speech-to-speech/${encodeURIComponent(params.voiceId)}`,
626
+ body: form,
627
+ accept: "audio/mpeg"
628
+ });
629
+ if (!audio) {
630
+ throw new ElevenLabsApiError(
631
+ "speechToSpeech: expected audio bytes but got a JSON response",
632
+ 500,
633
+ ""
634
+ );
635
+ }
636
+ return { audio, contentType };
637
+ }
638
+ /**
639
+ * Generate a sound effect from a text prompt. `POST /v1/sound-generation`.
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * const { audio } = await el.soundEffects({ text: 'heavy wooden door creaking' })
644
+ * ```
645
+ */
646
+ async soundEffects(params) {
647
+ if (!params.text || !params.text.trim()) {
648
+ throw new Error("soundEffects: text is required");
649
+ }
650
+ const body = {
651
+ text: params.text,
652
+ model_id: params.modelId ?? DEFAULT_SOUND_GENERATION_MODEL
653
+ };
654
+ if (params.durationSeconds !== void 0) body.duration_seconds = params.durationSeconds;
655
+ if (params.promptInfluence !== void 0) body.prompt_influence = params.promptInfluence;
656
+ if (params.loop !== void 0) body.loop = params.loop;
657
+ const { audio, contentType } = await this.request({
658
+ method: "POST",
659
+ path: "/v1/sound-generation",
660
+ body,
661
+ accept: "audio/mpeg",
662
+ ...params.outputFormat ? { query: { output_format: params.outputFormat } } : {}
663
+ });
664
+ if (!audio) {
665
+ throw new ElevenLabsApiError(
666
+ "soundEffects: expected audio bytes but got a JSON response",
667
+ 500,
668
+ ""
669
+ );
670
+ }
671
+ return { audio, contentType };
672
+ }
673
+ /**
674
+ * Transcribe audio with Scribe. `POST /v1/speech-to-text` (multipart).
675
+ *
676
+ * @example
677
+ * ```ts
678
+ * const { text } = await el.transcribe({ file: recording })
679
+ * ```
680
+ */
681
+ async transcribe(params) {
682
+ if (!params.file) throw new Error("transcribe: file is required");
683
+ const form = new FormData();
684
+ form.set("file", params.file, params.fileName ?? "audio");
685
+ form.set("model_id", params.modelId ?? DEFAULT_SPEECH_TO_TEXT_MODEL);
686
+ if (params.languageCode) form.set("language_code", params.languageCode);
687
+ if (params.diarize !== void 0) form.set("diarize", String(params.diarize));
688
+ if (params.tagAudioEvents !== void 0) {
689
+ form.set("tag_audio_events", String(params.tagAudioEvents));
690
+ }
691
+ if (params.numSpeakers !== void 0) form.set("num_speakers", String(params.numSpeakers));
692
+ const { json: json2 } = await this.request({
693
+ method: "POST",
694
+ path: "/v1/speech-to-text",
695
+ body: form,
696
+ accept: "application/json"
697
+ });
698
+ if (!json2 || typeof json2 !== "object") {
699
+ throw new ElevenLabsApiError(
700
+ "transcribe: expected a JSON transcription but got audio",
701
+ 500,
702
+ ""
703
+ );
704
+ }
705
+ return json2;
706
+ }
707
+ /** Probe whether a given voice id still exists on the caller's account. */
708
+ async voiceExists(voiceId) {
709
+ const res = await this.fetchImpl(
710
+ `${this.baseUrl}/v1/voices/${encodeURIComponent(voiceId)}`,
711
+ { headers: this.headers() }
712
+ );
713
+ return res.ok;
714
+ }
715
+ /**
716
+ * Register a Twilio phone number with an ElevenLabs agent. EL becomes
717
+ * the SIP target for inbound calls to that number — Twilio bridges
718
+ * the call via EL's native integration and we never have to run a
719
+ * Media Streams ↔ convai WebSocket bridge ourselves.
720
+ *
721
+ * Returns the ElevenLabs `phone_number_id` (distinct from the Twilio
722
+ * `sid`) which is the handle used by `outboundCall` + `deletePhoneNumber`.
723
+ */
724
+ async createPhoneNumberTwilio(params) {
725
+ const body = {
726
+ phone_number: params.phoneNumber,
727
+ label: params.label ?? params.phoneNumber,
728
+ provider: "twilio",
729
+ sid: params.twilioAccountSid,
730
+ token: params.twilioAuthToken
731
+ };
732
+ const res = await this.fetchImpl(
733
+ `${this.baseUrl}/v1/convai/phone-numbers`,
734
+ {
735
+ method: "POST",
736
+ headers: this.headers({ "content-type": "application/json" }),
737
+ body: JSON.stringify(body)
738
+ }
739
+ );
740
+ if (!res.ok) {
741
+ const text = await res.text();
742
+ throw new ElevenLabsApiError(
743
+ `createPhoneNumberTwilio failed: ${res.status}`,
744
+ res.status,
745
+ text
746
+ );
747
+ }
748
+ const data = await res.json();
749
+ const id = data.phone_number_id ?? data.id;
750
+ if (!id) {
751
+ throw new ElevenLabsApiError(
752
+ "createPhoneNumberTwilio returned no id",
753
+ 500,
754
+ JSON.stringify(data)
755
+ );
756
+ }
757
+ const patchRes = await this.fetchImpl(
758
+ `${this.baseUrl}/v1/convai/phone-numbers/${encodeURIComponent(id)}`,
759
+ {
760
+ method: "PATCH",
761
+ headers: this.headers({ "content-type": "application/json" }),
762
+ body: JSON.stringify({ agent_id: params.agentId })
763
+ }
764
+ );
765
+ if (!patchRes.ok) {
766
+ const text = await patchRes.text();
767
+ try {
768
+ await this.deletePhoneNumber(id);
769
+ } catch {
770
+ }
771
+ throw new ElevenLabsApiError(
772
+ `assignAgentToPhoneNumber failed: ${patchRes.status}`,
773
+ patchRes.status,
774
+ text
775
+ );
776
+ }
777
+ return { phoneNumberId: id };
778
+ }
779
+ /** Detach a previously-registered phone number from ElevenLabs. */
780
+ async deletePhoneNumber(phoneNumberId) {
781
+ const res = await this.fetchImpl(
782
+ `${this.baseUrl}/v1/convai/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
783
+ { method: "DELETE", headers: this.headers() }
784
+ );
785
+ if (!res.ok && res.status !== 404) {
786
+ const text = await res.text();
787
+ throw new ElevenLabsApiError(
788
+ `deletePhoneNumber failed: ${res.status}`,
789
+ res.status,
790
+ text
791
+ );
792
+ }
793
+ }
794
+ /**
795
+ * Place an outbound PSTN call via a previously-registered EL phone
796
+ * number. EL drives Twilio (or a SIP trunk) under the hood — we just
797
+ * hand it the destination number and the agent to bridge to. Returns
798
+ * the Twilio `callSid` + EL `conversationId` for metering / tracing.
799
+ *
800
+ * As of 2026-Q2 the EL API uses provider-scoped routes:
801
+ * - Twilio numbers → POST /v1/convai/twilio/outbound-call
802
+ * - SIP numbers → POST /v1/convai/sip-trunk/outbound-call
803
+ * and `agent_phone_number_id` is a body field (no longer a path
804
+ * segment). The previous `/v1/convai/phone-numbers/{id}/outbound-call`
805
+ * route now returns 404. Callers that don't know the provider can
806
+ * look it up from `GET /v1/convai/phone-numbers/{id}` (`provider:
807
+ * "twilio" | "sip_trunk"`); most hosted setups are Twilio, so that's
808
+ * the default here.
809
+ */
810
+ async outboundCall(params) {
811
+ const provider = params.provider ?? "twilio";
812
+ const body = {
813
+ agent_id: params.agentId,
814
+ agent_phone_number_id: params.phoneNumberId,
815
+ to_number: params.toNumber
816
+ };
817
+ if (params.dynamicVariables) {
818
+ body.conversation_initiation_client_data = {
819
+ dynamic_variables: params.dynamicVariables
820
+ };
821
+ }
822
+ const res = await this.fetchImpl(
823
+ `${this.baseUrl}/v1/convai/${provider}/outbound-call`,
824
+ {
825
+ method: "POST",
826
+ headers: this.headers({ "content-type": "application/json" }),
827
+ body: JSON.stringify(body)
828
+ }
829
+ );
830
+ if (!res.ok) {
831
+ const text = await res.text();
832
+ throw new ElevenLabsApiError(
833
+ `outboundCall failed: ${res.status}`,
834
+ res.status,
835
+ text
836
+ );
837
+ }
838
+ const data = await res.json();
839
+ const callSid = data.call_sid ?? data.callSid;
840
+ const conversationId = data.conversation_id ?? data.conversationId;
841
+ if (!callSid || !conversationId) {
842
+ throw new ElevenLabsApiError(
843
+ "outboundCall missing callSid or conversationId",
844
+ 500,
845
+ JSON.stringify(data)
846
+ );
847
+ }
848
+ return { callSid, conversationId };
849
+ }
850
+ };
851
+ var MEMORY_CLIENT_TOOLS = [
852
+ {
853
+ type: "client",
854
+ name: "add_memory",
855
+ description: "Save a concise canonical fact about the user (preferences, decisions, personal details, follow-ups). Use whenever the user shares something worth remembering.",
856
+ expects_response: true,
857
+ parameters: {
858
+ type: "object",
859
+ properties: {
860
+ fact: {
861
+ type: "string",
862
+ description: 'A short normalized fact, e.g. "prefers window seats", "favorite color: green". Max 500 chars.'
863
+ }
864
+ },
865
+ required: ["fact"]
866
+ }
867
+ }
868
+ ];
869
+
870
+ // src/ops.ts
871
+ var MAX_AUDIO_BYTES = 25 * 1024 * 1024;
872
+ var MAX_JSON_BYTES = 8 * 1024;
873
+ var MAX_TEXT_BYTES = 256 * 1024;
874
+ var VOICE_OPS = [
875
+ {
876
+ id: "text-to-speech",
877
+ method: "POST",
878
+ hostedPath: "/voice/op/text-to-speech",
879
+ legacyHostedPath: "/voice/tts",
880
+ upstreamPath: "/v1/text-to-speech/{voiceId}",
881
+ pathParams: ["voiceId"],
882
+ transport: "json-binary",
883
+ call: "textToSpeech",
884
+ permission: "project:read",
885
+ maxBodyBytes: MAX_TEXT_BYTES,
886
+ cost: { unit: "character", per: 1 },
887
+ models: VOICE_TTS_MODEL_IDS,
888
+ defaultModel: CONVAI_TTS_MODEL_FALLBACK,
889
+ defaultVoiceId: true,
890
+ forward: {
891
+ text: "text",
892
+ voiceId: "voice_id",
893
+ modelId: "model_id",
894
+ voiceSettings: "voice_settings"
895
+ },
896
+ required: ["text"],
897
+ responseHeaders: ["x-tts-model-used", "x-tts-model-requested", "warning"],
898
+ accept: "audio/mpeg",
899
+ errorLabel: "TTS failed"
900
+ },
901
+ {
902
+ id: "text-to-speech-with-timestamps",
903
+ method: "POST",
904
+ hostedPath: "/voice/op/text-to-speech-with-timestamps",
905
+ upstreamPath: "/v1/text-to-speech/{voiceId}/with-timestamps",
906
+ pathParams: ["voiceId"],
907
+ transport: "json-json",
908
+ permission: "project:read",
909
+ maxBodyBytes: MAX_TEXT_BYTES,
910
+ cost: { unit: "character", per: 1 },
911
+ models: VOICE_TTS_MODEL_IDS,
912
+ defaultModel: CONVAI_TTS_MODEL_FALLBACK,
913
+ defaultVoiceId: true,
914
+ forward: {
915
+ text: "text",
916
+ voiceId: "voice_id",
917
+ modelId: "model_id",
918
+ voiceSettings: "voice_settings",
919
+ outputFormat: "output_format"
920
+ },
921
+ required: ["text"],
922
+ accept: "application/json"
923
+ },
924
+ {
925
+ id: "speech-to-speech",
926
+ method: "POST",
927
+ hostedPath: "/voice/op/speech-to-speech",
928
+ upstreamPath: "/v1/speech-to-speech/{voiceId}",
929
+ pathParams: ["voiceId"],
930
+ transport: "multipart-binary",
931
+ permission: "project:read",
932
+ maxBodyBytes: MAX_AUDIO_BYTES,
933
+ cost: { unit: "second", per: 1 },
934
+ models: VOICE_STS_MODEL_IDS,
935
+ defaultModel: DEFAULT_STS_MODEL,
936
+ defaultVoiceId: true,
937
+ forward: {
938
+ audio: "audio",
939
+ voiceId: "voice_id",
940
+ modelId: "model_id",
941
+ voiceSettings: "voice_settings",
942
+ outputFormat: "output_format",
943
+ fileFormat: "file_format",
944
+ seed: "seed",
945
+ removeBackgroundNoise: "remove_background_noise"
946
+ },
947
+ required: ["audio"],
948
+ accept: "audio/mpeg"
949
+ },
950
+ {
951
+ id: "speech-to-text",
952
+ method: "POST",
953
+ hostedPath: "/voice/op/speech-to-text",
954
+ upstreamPath: "/v1/speech-to-text",
955
+ transport: "multipart-json",
956
+ permission: "project:read",
957
+ maxBodyBytes: MAX_AUDIO_BYTES,
958
+ cost: { unit: "second", per: 1 },
959
+ models: VOICE_SPEECH_TO_TEXT_MODEL_IDS,
960
+ defaultModel: DEFAULT_SPEECH_TO_TEXT_MODEL,
961
+ forward: {
962
+ file: "file",
963
+ modelId: "model_id",
964
+ languageCode: "language_code",
965
+ diarize: "diarize",
966
+ tagAudioEvents: "tag_audio_events",
967
+ numSpeakers: "num_speakers"
968
+ },
969
+ required: ["file"],
970
+ accept: "application/json"
971
+ },
972
+ {
973
+ id: "sound-generation",
974
+ method: "POST",
975
+ hostedPath: "/voice/op/sound-generation",
976
+ upstreamPath: "/v1/sound-generation",
977
+ transport: "json-binary",
978
+ permission: "project:read",
979
+ maxBodyBytes: MAX_JSON_BYTES,
980
+ cost: { unit: "second", per: 1 },
981
+ models: VOICE_SOUND_GENERATION_MODEL_IDS,
982
+ defaultModel: DEFAULT_SOUND_GENERATION_MODEL,
983
+ forward: {
984
+ text: "text",
985
+ durationSeconds: "duration_seconds",
986
+ promptInfluence: "prompt_influence",
987
+ loop: "loop",
988
+ modelId: "model_id"
989
+ },
990
+ // `output_format` is a query param on this endpoint, not a body field — the
991
+ // typed `soundEffects()` client sends it there, and a body field is
992
+ // silently ignored upstream.
993
+ queryForward: ["output_format"],
994
+ required: ["text"],
995
+ accept: "audio/mpeg"
996
+ },
997
+ {
998
+ id: "music",
999
+ method: "POST",
1000
+ hostedPath: "/voice/op/music",
1001
+ legacyHostedPath: "/voice/music",
1002
+ upstreamPath: "/v1/music",
1003
+ transport: "json-binary",
1004
+ call: "composeMusic",
1005
+ permission: "project:read",
1006
+ maxBodyBytes: MAX_JSON_BYTES,
1007
+ cost: { unit: "second", per: 1 },
1008
+ models: [DEFAULT_MUSIC_MODEL],
1009
+ defaultModel: DEFAULT_MUSIC_MODEL,
1010
+ forward: {
1011
+ prompt: "prompt",
1012
+ compositionPlan: "composition_plan",
1013
+ musicLengthMs: "music_length_ms",
1014
+ modelId: "model_id",
1015
+ forceInstrumental: "force_instrumental",
1016
+ outputFormat: "output_format"
1017
+ },
1018
+ accept: "audio/mpeg",
1019
+ errorLabel: "Music generation failed",
1020
+ validate: (fields) => {
1021
+ const hasPrompt = typeof fields.prompt === "string" && fields.prompt.trim().length > 0;
1022
+ const hasPlan = fields.compositionPlan != null && typeof fields.compositionPlan === "object";
1023
+ return hasPrompt === hasPlan ? "Provide exactly one of `prompt` or `compositionPlan`" : null;
1024
+ }
1025
+ },
1026
+ {
1027
+ id: "voices",
1028
+ method: "GET",
1029
+ hostedPath: "/voice/op/voices",
1030
+ upstreamPath: "/v1/voices",
1031
+ transport: "json-json",
1032
+ permission: "project:read",
1033
+ maxBodyBytes: 0,
1034
+ cost: { unit: "request", per: 1 },
1035
+ queryForward: ["page_size", "search", "voice_type", "category", "next_page_token"],
1036
+ accept: "application/json"
1037
+ },
1038
+ {
1039
+ id: "models",
1040
+ method: "GET",
1041
+ hostedPath: "/voice/op/models",
1042
+ upstreamPath: "/v1/models",
1043
+ transport: "json-json",
1044
+ permission: "project:read",
1045
+ maxBodyBytes: 0,
1046
+ cost: { unit: "request", per: 1 },
1047
+ accept: "application/json"
1048
+ }
1049
+ ];
1050
+ var VOICE_OP_IDS = VOICE_OPS.map((op) => op.id);
1051
+ function getVoiceOp(id) {
1052
+ if (!id) return void 0;
1053
+ return VOICE_OPS.find((op) => op.id === id);
1054
+ }
1055
+ function voiceOpForwardPath(op) {
1056
+ return op.legacyHostedPath ?? op.hostedPath;
1057
+ }
1058
+ function voiceOpReturnsAudio(op) {
1059
+ return op.transport === "json-binary" || op.transport === "multipart-binary";
1060
+ }
1061
+ function voiceOpUpstreamPath(op, params) {
1062
+ let path = op.upstreamPath;
1063
+ for (const name of op.pathParams ?? []) {
1064
+ const value = params[name];
1065
+ if (!value) {
1066
+ return { error: `Missing \`${name}\` (required path parameter for ${op.id})` };
1067
+ }
1068
+ path = path.split(`{${name}}`).join(encodeURIComponent(value));
1069
+ }
1070
+ return path;
1071
+ }
1072
+ var MODEL_FIELD = "modelId";
1073
+ function parseVoiceOpJsonBody(op, raw) {
1074
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1075
+ return { ok: false, status: 400, error: "Invalid JSON body" };
1076
+ }
1077
+ const body = raw;
1078
+ const fields = {};
1079
+ for (const [callerField, upstreamField] of Object.entries(op.forward ?? {})) {
1080
+ if (upstreamField && body[callerField] !== void 0) fields[callerField] = body[callerField];
1081
+ }
1082
+ for (const name of op.required ?? []) {
1083
+ const value = fields[name];
1084
+ if (value === void 0 || value === null || typeof value === "string" && !value.trim()) {
1085
+ return { ok: false, status: 400, error: `Missing \`${name}\`` };
1086
+ }
1087
+ }
1088
+ const modelCheck = checkVoiceOpModel(op, fields);
1089
+ if ("error" in modelCheck) return { ok: false, status: 400, ...modelCheck };
1090
+ if (op.validate) {
1091
+ const message = op.validate(fields);
1092
+ if (message) return { ok: false, status: 400, error: message };
1093
+ }
1094
+ return { ok: true, fields };
1095
+ }
1096
+ function parseVoiceOpForm(op, form) {
1097
+ const fields = {};
1098
+ for (const callerField of Object.keys(op.forward ?? {})) {
1099
+ const value = form.get(callerField);
1100
+ if (value !== null) fields[callerField] = value;
1101
+ }
1102
+ for (const name of op.required ?? []) {
1103
+ const value = fields[name];
1104
+ if (!(value instanceof Blob)) {
1105
+ return {
1106
+ ok: false,
1107
+ status: 400,
1108
+ error: `Missing \`${name}\` file part`,
1109
+ detail: value === void 0 ? `send a multipart body with a \`${name}\` file part` : `\`${name}\` must be a file part, not a text field`
1110
+ };
1111
+ }
1112
+ if (value.size === 0) {
1113
+ return { ok: false, status: 400, error: `Empty \`${name}\` file part` };
1114
+ }
1115
+ }
1116
+ const modelCheck = checkVoiceOpModel(op, fields);
1117
+ if ("error" in modelCheck) return { ok: false, status: 400, ...modelCheck };
1118
+ const forward = op.forward ?? {};
1119
+ const pathParams = new Set(op.pathParams ?? []);
1120
+ const out = new FormData();
1121
+ for (const [callerField, upstreamField] of Object.entries(forward)) {
1122
+ if (pathParams.has(callerField)) continue;
1123
+ const value = fields[callerField];
1124
+ if (value === void 0 || value === null || value === "") continue;
1125
+ if (value instanceof Blob) {
1126
+ const name = value instanceof File ? value.name : callerField;
1127
+ out.set(upstreamField, value, name);
1128
+ } else if (typeof value === "string") {
1129
+ out.set(upstreamField, value);
1130
+ } else {
1131
+ out.set(upstreamField, JSON.stringify(value));
1132
+ }
1133
+ }
1134
+ const modelField = forward[MODEL_FIELD];
1135
+ if (op.defaultModel && modelField && out.get(modelField) === null) {
1136
+ out.set(modelField, op.defaultModel);
1137
+ }
1138
+ return { ok: true, form: out, fields };
1139
+ }
1140
+ function voiceOpUpstreamBody(op, fields) {
1141
+ const pathParams = new Set(op.pathParams ?? []);
1142
+ const body = {};
1143
+ for (const [callerField, upstreamField] of Object.entries(op.forward ?? {})) {
1144
+ if (pathParams.has(callerField)) continue;
1145
+ const value = fields[callerField];
1146
+ if (value === void 0 || value === null) continue;
1147
+ body[upstreamField] = value;
1148
+ }
1149
+ return body;
1150
+ }
1151
+ function voiceOpPathParams(op, fields, query) {
1152
+ const out = {};
1153
+ for (const name of op.pathParams ?? []) {
1154
+ const fromBody = fields[name];
1155
+ const value = typeof fromBody === "string" && fromBody.length > 0 ? fromBody : query.get(name);
1156
+ if (value) out[name] = value;
1157
+ }
1158
+ return out;
1159
+ }
1160
+ function checkVoiceOpModel(op, fields) {
1161
+ const requested = fields[MODEL_FIELD];
1162
+ if (requested === void 0 || requested === null || requested === "") return { ok: true };
1163
+ if (typeof requested !== "string") {
1164
+ return { error: `\`${MODEL_FIELD}\` must be a string` };
1165
+ }
1166
+ if (op.models && !op.models.includes(requested)) {
1167
+ return {
1168
+ error: "unknown_model",
1169
+ detail: `Unknown model '${requested}' for ${op.id}`,
1170
+ allowed: op.models
1171
+ };
1172
+ }
1173
+ return { ok: true };
1174
+ }
1175
+
1176
+ // src/runtime-token.ts
1177
+ var WORKSPACE_RUNTIME_TOKEN_MESSAGE = "RUNTIME_AUTH_SECRET is the workspace runtime token; app code needs its project token (rt_v1_\u2026). Start the app through the preview (server_sync / restart), not from a workspace shell.";
1178
+ function readRuntimeAuthSecret(env = typeof process === "undefined" ? void 0 : process.env) {
1179
+ const token = env?.RUNTIME_AUTH_SECRET;
1180
+ if (!token) return void 0;
1181
+ if (token.startsWith("wrt_v1_")) throw new Error(WORKSPACE_RUNTIME_TOKEN_MESSAGE);
1182
+ return token;
1183
+ }
1184
+
1185
+ // src/server.ts
1186
+ function json(data, status = 200, extraHeaders = {}) {
1187
+ return new Response(JSON.stringify(data), {
1188
+ status,
1189
+ headers: { "Content-Type": "application/json", ...extraHeaders }
1190
+ });
1191
+ }
1192
+ async function readJson(req) {
1193
+ try {
1194
+ const text = await req.text();
1195
+ if (!text) return {};
1196
+ return JSON.parse(text);
1197
+ } catch {
1198
+ return null;
1199
+ }
1200
+ }
1201
+ var NOOP_LOGGER = () => {
1202
+ };
1203
+ function parseMusicBody(raw) {
1204
+ if (raw === null || typeof raw !== "object") return { error: "Invalid JSON body" };
1205
+ const b = raw;
1206
+ const hasPrompt = typeof b.prompt === "string" && b.prompt.trim().length > 0;
1207
+ const hasPlan = b.compositionPlan != null && typeof b.compositionPlan === "object";
1208
+ if (hasPrompt === hasPlan) {
1209
+ return { error: "Provide exactly one of `prompt` or `compositionPlan`" };
1210
+ }
1211
+ const params = {};
1212
+ if (hasPrompt) {
1213
+ params.prompt = b.prompt.trim();
1214
+ if (typeof b.musicLengthMs === "number") params.musicLengthMs = b.musicLengthMs;
1215
+ if (typeof b.forceInstrumental === "boolean") params.forceInstrumental = b.forceInstrumental;
1216
+ } else {
1217
+ params.compositionPlan = b.compositionPlan;
1218
+ }
1219
+ if (typeof b.modelId === "string" && b.modelId.length > 0) params.modelId = b.modelId;
1220
+ if (typeof b.outputFormat === "string" && b.outputFormat.length > 0) {
1221
+ params.outputFormat = b.outputFormat;
1222
+ }
1223
+ return { params };
1224
+ }
1225
+ 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";
1226
+ function envApiUrl() {
1227
+ if (typeof process === "undefined" || !process.env) return void 0;
1228
+ return process.env.SHOGO_API_URL ?? process.env.SHOGO_CLOUD_URL;
1229
+ }
1230
+ function resolveProxyOptions(config) {
1231
+ if (config.mode === "byo") {
1232
+ if (config.proxy) {
1233
+ throw new Error(
1234
+ "createVoiceHandlers: mode 'byo' cannot be combined with an explicit proxy"
1235
+ );
1236
+ }
1237
+ return null;
1238
+ }
1239
+ const envProxy = () => {
1240
+ if (config.mode === "proxy" && config.apiKey) {
1241
+ LOG_WARN_CACHE.apiKeyIgnored(config.logger);
1242
+ }
1243
+ if (typeof process === "undefined" || !process.env) return null;
1244
+ const runtimeToken = readRuntimeAuthSecret();
1245
+ const projectId = process.env.PROJECT_ID;
1246
+ if (!runtimeToken || !projectId) return null;
1247
+ return {
1248
+ runtimeToken,
1249
+ projectId,
1250
+ // Deliberately not required here: see `resolveApiBase`. Throwing at
1251
+ // construction made every local-only handler (400 validation, 405s,
1252
+ // the static audio-tags catalog) unusable on a host with a token but
1253
+ // no URL — which is exactly CI's env.
1254
+ apiUrl: envApiUrl() ?? "",
1255
+ ...config.fetch ? { fetch: config.fetch } : {}
1256
+ };
1257
+ };
1258
+ if (config.proxy) {
1259
+ if (!config.proxy.runtimeToken || !config.proxy.projectId || !config.proxy.apiUrl) {
1260
+ throw new Error(
1261
+ "createVoiceHandlers: proxy requires runtimeToken, projectId, and apiUrl"
1262
+ );
1263
+ }
1264
+ if (config.apiKey) LOG_WARN_CACHE.apiKeyIgnored(config.logger);
1265
+ return config.proxy;
1266
+ }
1267
+ if (config.mode === "proxy") return envProxy();
1268
+ if (config.apiKey) return null;
1269
+ return envProxy();
1270
+ }
1271
+ var LOG_WARN_CACHE = {
1272
+ warned: false,
1273
+ apiKeyIgnored(logger) {
1274
+ if (LOG_WARN_CACHE.warned) return;
1275
+ LOG_WARN_CACHE.warned = true;
1276
+ (logger ?? NOOP_LOGGER)(
1277
+ "warn",
1278
+ 'createVoiceHandlers: apiKey provided but runtime-token proxy mode is active; apiKey is ignored (pass mode: "byo" to use it)'
1279
+ );
1280
+ }
1281
+ };
1282
+ function resolveOpId(req, opId) {
1283
+ if (opId) return opId;
1284
+ try {
1285
+ const url = new URL(req.url);
1286
+ const fromQuery = url.searchParams.get("op");
1287
+ if (fromQuery) return fromQuery;
1288
+ const segments = url.pathname.split("/").filter(Boolean);
1289
+ const last = segments[segments.length - 1];
1290
+ if (last && segments[segments.length - 2] === "op") return last;
1291
+ return void 0;
1292
+ } catch {
1293
+ return void 0;
1294
+ }
1295
+ }
1296
+ async function readVoiceOpBody(op, req) {
1297
+ if (op.method === "GET") return { kind: "none" };
1298
+ if (op.transport === "multipart-binary" || op.transport === "multipart-json") {
1299
+ let form;
1300
+ try {
1301
+ form = await req.formData();
1302
+ } catch {
1303
+ return json({ error: "Invalid multipart body", detail: "send a multipart/form-data body" }, 400);
1304
+ }
1305
+ const parsed2 = parseVoiceOpForm(op, form);
1306
+ if (!parsed2.ok) {
1307
+ return json(
1308
+ {
1309
+ error: parsed2.error,
1310
+ ...parsed2.detail ? { detail: parsed2.detail } : {},
1311
+ ...parsed2.allowed ? { allowed: parsed2.allowed } : {}
1312
+ },
1313
+ parsed2.status
1314
+ );
1315
+ }
1316
+ return { kind: "form", form: parsed2.form, fields: parsed2.fields };
1317
+ }
1318
+ const raw = await readJson(req);
1319
+ const parsed = parseVoiceOpJsonBody(op, raw);
1320
+ if (!parsed.ok) {
1321
+ return json(
1322
+ {
1323
+ error: parsed.error,
1324
+ ...parsed.detail ? { detail: parsed.detail } : {},
1325
+ ...parsed.allowed ? { allowed: parsed.allowed } : {}
1326
+ },
1327
+ parsed.status
1328
+ );
1329
+ }
1330
+ return { kind: "json", fields: parsed.fields };
1331
+ }
1332
+ function createProxyHandlers(proxy, log) {
1333
+ const fetchImpl = proxy.fetch ?? globalThis.fetch;
1334
+ if (typeof fetchImpl !== "function") {
1335
+ throw new Error(
1336
+ "createVoiceHandlers proxy: global fetch is unavailable; pass proxy.fetch"
1337
+ );
1338
+ }
1339
+ function resolveApiBase() {
1340
+ const raw = proxy.apiUrl || envApiUrl();
1341
+ if (!raw) throw new Error(MISSING_API_HOST_MESSAGE);
1342
+ return raw.replace(/\/+$/, "");
1343
+ }
1344
+ function proxyUrl(base, path, forward) {
1345
+ const tail = [`projectId=${encodeURIComponent(proxy.projectId)}`];
1346
+ for (const [k, v] of Object.entries(forward)) {
1347
+ if (typeof v === "string" && v.length > 0) {
1348
+ tail.push(`${k}=${encodeURIComponent(v)}`);
1349
+ }
1350
+ }
1351
+ const sep = path.includes("?") ? "&" : "?";
1352
+ return `${base}${path}${sep}${tail.join("&")}`;
1353
+ }
1354
+ const FORWARDED_SIGNED_URL_PARAMS = ["agentName"];
1355
+ async function doSignedUrl(req) {
1356
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
1357
+ const base = resolveApiBase();
1358
+ try {
1359
+ let incomingUrl = null;
1360
+ try {
1361
+ incomingUrl = new URL(req.url);
1362
+ } catch {
1363
+ }
1364
+ const forward = {};
1365
+ if (incomingUrl) {
1366
+ for (const key of FORWARDED_SIGNED_URL_PARAMS) {
1367
+ const v = incomingUrl.searchParams.get(key);
1368
+ if (v != null) forward[key] = v;
1369
+ }
1370
+ }
1371
+ const upstream = await fetchImpl(proxyUrl(base, "/api/voice/signed-url", forward), {
1372
+ method: "GET",
1373
+ headers: { "x-runtime-token": proxy.runtimeToken },
1374
+ credentials: "omit"
1375
+ });
1376
+ const body = await upstream.text();
1377
+ const headers = { "content-type": "application/json" };
1378
+ const ct = upstream.headers.get("content-type");
1379
+ if (ct) headers["content-type"] = ct;
1380
+ return new Response(body, { status: upstream.status, headers });
1381
+ } catch (err) {
1382
+ log("error", "proxy signedUrl failed", {
1383
+ error: err instanceof Error ? err.message : String(err)
1384
+ });
1385
+ return json({ error: "signedUrl proxy failed" }, 502);
1386
+ }
1387
+ }
1388
+ async function doAudioTags(req) {
1389
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
1390
+ return json({
1391
+ tags: AUDIO_TAGS,
1392
+ groups: AUDIO_TAG_GROUPS,
1393
+ expressivity: EXPRESSIVITY_OPTIONS,
1394
+ defaults: {
1395
+ allowedTags: DEFAULT_ALLOWED_TAGS,
1396
+ voiceSettings: DEFAULT_VOICE_SETTINGS,
1397
+ ttsModelId: CONVAI_TTS_MODEL_FALLBACK
1398
+ }
1399
+ });
1400
+ }
1401
+ async function doOp(req, opId) {
1402
+ const id = resolveOpId(req, opId);
1403
+ const op = getVoiceOp(id);
1404
+ if (!op) {
1405
+ return json(
1406
+ { error: "unknown_op", detail: `Unknown voice op '${id ?? ""}'`, allowed: VOICE_OP_IDS },
1407
+ 404
1408
+ );
1409
+ }
1410
+ if (req.method !== op.method) return json({ error: "Method Not Allowed" }, 405);
1411
+ const parsed = await readVoiceOpBody(op, req);
1412
+ if (parsed instanceof Response) return parsed;
1413
+ const base = resolveApiBase();
1414
+ const forward = {};
1415
+ let incomingUrl = null;
1416
+ try {
1417
+ incomingUrl = new URL(req.url);
1418
+ } catch {
1419
+ }
1420
+ if (incomingUrl) {
1421
+ for (const key of [...op.pathParams ?? [], ...op.queryForward ?? []]) {
1422
+ const value = incomingUrl.searchParams.get(key);
1423
+ if (value != null) forward[key] = value;
1424
+ }
1425
+ }
1426
+ if (parsed.kind === "form") {
1427
+ for (const name of op.pathParams ?? []) {
1428
+ const value = parsed.fields[name];
1429
+ if (typeof value === "string" && value.length > 0) forward[name] = value;
1430
+ }
1431
+ }
1432
+ const hostedPath = `/api${voiceOpForwardPath(op)}`;
1433
+ const headers = { "x-runtime-token": proxy.runtimeToken };
1434
+ let body;
1435
+ if (parsed.kind === "json") {
1436
+ headers["content-type"] = "application/json";
1437
+ body = JSON.stringify(parsed.fields);
1438
+ } else if (parsed.kind === "form") {
1439
+ body = parsed.form;
1440
+ }
1441
+ if (op.accept) headers.accept = op.accept;
1442
+ try {
1443
+ const upstream = await fetchImpl(proxyUrl(base, hostedPath, forward), {
1444
+ method: op.method,
1445
+ headers,
1446
+ ...body !== void 0 ? { body } : {},
1447
+ credentials: "omit"
1448
+ });
1449
+ if (upstream.status === 404) {
1450
+ return json(
1451
+ {
1452
+ error: "Not Implemented",
1453
+ detail: `this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 ${hostedPath} returned 404 upstream`
1454
+ },
1455
+ 501
1456
+ );
1457
+ }
1458
+ const buf = await upstream.arrayBuffer();
1459
+ const responseHeaders = {};
1460
+ const ct = upstream.headers.get("content-type");
1461
+ if (ct) responseHeaders["content-type"] = ct;
1462
+ for (const name of op.responseHeaders ?? []) {
1463
+ const value = upstream.headers.get(name);
1464
+ if (value) responseHeaders[name] = value;
1465
+ }
1466
+ return new Response(buf, { status: upstream.status, headers: responseHeaders });
1467
+ } catch (err) {
1468
+ log("error", `proxy ${op.id} failed`, {
1469
+ error: err instanceof Error ? err.message : String(err)
1470
+ });
1471
+ return json({ error: `${op.id} proxy failed` }, 502);
1472
+ }
1473
+ }
1474
+ const doMusic = (req) => doOp(req, "music");
1475
+ function notImplemented(label) {
1476
+ return async () => json(
1477
+ {
1478
+ error: "Not Implemented",
1479
+ detail: `${label} is not available in runtime-token proxy mode; use BYO-EL or the hosted bearer SDK path.`
1480
+ },
1481
+ 501
1482
+ );
1483
+ }
1484
+ async function doTts(req) {
1485
+ if (req.method === "POST") {
1486
+ let raw = null;
1487
+ try {
1488
+ const parsed = await req.clone().json();
1489
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1490
+ raw = parsed;
1491
+ }
1492
+ } catch {
1493
+ }
1494
+ if (raw && (typeof raw.text !== "string" || raw.text.trim().length === 0)) {
1495
+ const sentTags = Array.isArray(raw.audioTags) && raw.audioTags.length > 0;
1496
+ if (sentTags) {
1497
+ const tags = normalizeAudioTags(raw.audioTags) ?? [];
1498
+ const characterName = typeof raw.characterName === "string" ? raw.characterName : void 0;
1499
+ const text = buildPreviewLine(tags, characterName ?? "your companion");
1500
+ return doOp(
1501
+ new Request(req.url, {
1502
+ method: "POST",
1503
+ headers: { "content-type": "application/json" },
1504
+ body: JSON.stringify({ ...raw, text })
1505
+ }),
1506
+ "text-to-speech"
1507
+ );
1508
+ }
1509
+ }
1510
+ }
1511
+ return doOp(req, "text-to-speech");
1512
+ }
1513
+ function createProxyElevenLabs() {
1514
+ const unsupported = (prop) => {
1515
+ throw new Error(
1516
+ `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`
1517
+ );
1518
+ };
1519
+ const textToSpeech = async (params) => {
1520
+ const upstream = await fetchImpl(proxyUrl(resolveApiBase(), "/api/voice/tts", {}), {
1521
+ method: "POST",
1522
+ headers: {
1523
+ "x-runtime-token": proxy.runtimeToken,
1524
+ "content-type": "application/json",
1525
+ accept: params.accept ?? "audio/mpeg"
1526
+ },
1527
+ body: JSON.stringify({
1528
+ text: params.text,
1529
+ voiceId: params.voiceId,
1530
+ ...params.modelId ? { modelId: params.modelId } : {},
1531
+ ...params.voiceSettings ? { voiceSettings: params.voiceSettings } : {}
1532
+ }),
1533
+ credentials: "omit"
1534
+ });
1535
+ if (upstream.status === 404) {
1536
+ throw new Error(
1537
+ "this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 /api/voice/tts returned 404 upstream"
1538
+ );
1539
+ }
1540
+ if (!upstream.ok) {
1541
+ throw new ElevenLabsApiError(
1542
+ `textToSpeech failed: ${upstream.status}`,
1543
+ upstream.status,
1544
+ await upstream.text()
1545
+ );
1546
+ }
1547
+ const requestedModel = params.modelId ?? DEFAULT_TTS_MODEL;
1548
+ const usedModel = upstream.headers.get("x-tts-model-used") ?? requestedModel;
1549
+ const requestedHeader = upstream.headers.get("x-tts-model-requested") ?? requestedModel;
1550
+ return {
1551
+ audio: await upstream.arrayBuffer(),
1552
+ modelId: usedModel,
1553
+ requestedModel: requestedHeader,
1554
+ fellBack: usedModel !== requestedHeader,
1555
+ contentType: upstream.headers.get("content-type") ?? params.accept ?? "audio/mpeg"
1556
+ };
1557
+ };
1558
+ const composeMusic = async (params) => {
1559
+ const upstream = await fetchImpl(proxyUrl(resolveApiBase(), "/api/voice/music", {}), {
1560
+ method: "POST",
1561
+ headers: {
1562
+ "x-runtime-token": proxy.runtimeToken,
1563
+ "content-type": "application/json",
1564
+ accept: "audio/mpeg"
1565
+ },
1566
+ body: JSON.stringify(params),
1567
+ credentials: "omit"
1568
+ });
1569
+ if (upstream.status === 404) {
1570
+ throw new Error(
1571
+ "this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 /api/voice/music returned 404 upstream"
1572
+ );
1573
+ }
1574
+ if (!upstream.ok) {
1575
+ throw new ElevenLabsApiError(
1576
+ `composeMusic failed: ${upstream.status}`,
1577
+ upstream.status,
1578
+ await upstream.text()
1579
+ );
1580
+ }
1581
+ return {
1582
+ audio: await upstream.arrayBuffer(),
1583
+ contentType: upstream.headers.get("content-type") ?? "audio/mpeg"
1584
+ };
1585
+ };
1586
+ const opResponse = async (opId, req) => {
1587
+ const op = getVoiceOp(opId);
1588
+ const res = await doOp(req, opId);
1589
+ if (res.status === 404) {
1590
+ throw new Error(
1591
+ `this Shogo API has no voice routes (desktop without ELEVENLABS_API_KEY?) \u2014 /api${voiceOpForwardPath(op)} returned 404 upstream`
1592
+ );
1593
+ }
1594
+ if (!res.ok) {
1595
+ throw new ElevenLabsApiError(
1596
+ `${opId} failed: ${res.status}`,
1597
+ res.status,
1598
+ await res.text()
1599
+ );
1600
+ }
1601
+ return res;
1602
+ };
1603
+ const opAudio = async (opId, req) => {
1604
+ const res = await opResponse(opId, req);
1605
+ return {
1606
+ audio: await res.arrayBuffer(),
1607
+ contentType: res.headers.get("content-type") ?? "audio/mpeg"
1608
+ };
1609
+ };
1610
+ const speechToSpeech = (params) => {
1611
+ const form = new FormData();
1612
+ form.set("audio", params.audio, params.fileName ?? "audio");
1613
+ if (params.modelId) form.set("modelId", params.modelId);
1614
+ if (params.voiceSettings) form.set("voiceSettings", JSON.stringify(params.voiceSettings));
1615
+ if (params.outputFormat) form.set("outputFormat", params.outputFormat);
1616
+ if (params.fileFormat) form.set("fileFormat", params.fileFormat);
1617
+ if (params.seed !== void 0) form.set("seed", String(params.seed));
1618
+ if (params.removeBackgroundNoise !== void 0) {
1619
+ form.set("removeBackgroundNoise", String(params.removeBackgroundNoise));
1620
+ }
1621
+ const query = `?voiceId=${encodeURIComponent(params.voiceId)}`;
1622
+ return opAudio(
1623
+ "speech-to-speech",
1624
+ new Request(`http://voice.invalid/voice/op/speech-to-speech${query}`, {
1625
+ method: "POST",
1626
+ body: form
1627
+ })
1628
+ );
1629
+ };
1630
+ const soundEffects = (params) => opAudio(
1631
+ "sound-generation",
1632
+ new Request(
1633
+ // `output_format` is a query param on this endpoint (see the row's
1634
+ // `queryForward`); sending it in the body would be dropped at EL.
1635
+ `http://voice.invalid/voice/op/sound-generation${params.outputFormat ? `?output_format=${encodeURIComponent(params.outputFormat)}` : ""}`,
1636
+ {
1637
+ method: "POST",
1638
+ headers: { "content-type": "application/json" },
1639
+ body: JSON.stringify({
1640
+ text: params.text,
1641
+ ...params.durationSeconds !== void 0 ? { durationSeconds: params.durationSeconds } : {},
1642
+ ...params.promptInfluence !== void 0 ? { promptInfluence: params.promptInfluence } : {},
1643
+ ...params.loop !== void 0 ? { loop: params.loop } : {},
1644
+ ...params.modelId ? { modelId: params.modelId } : {}
1645
+ })
1646
+ }
1647
+ )
1648
+ );
1649
+ const transcribe = async (params) => {
1650
+ const form = new FormData();
1651
+ form.set("file", params.file, params.fileName ?? "audio");
1652
+ if (params.modelId) form.set("modelId", params.modelId);
1653
+ if (params.languageCode) form.set("languageCode", params.languageCode);
1654
+ if (params.diarize !== void 0) form.set("diarize", String(params.diarize));
1655
+ if (params.tagAudioEvents !== void 0) {
1656
+ form.set("tagAudioEvents", String(params.tagAudioEvents));
1657
+ }
1658
+ if (params.numSpeakers !== void 0) form.set("numSpeakers", String(params.numSpeakers));
1659
+ const res = await opResponse(
1660
+ "speech-to-text",
1661
+ new Request("http://voice.invalid/voice/op/speech-to-text", {
1662
+ method: "POST",
1663
+ body: form
1664
+ })
1665
+ );
1666
+ return await res.json();
1667
+ };
1668
+ return new Proxy({}, {
1669
+ get(_t, prop) {
1670
+ if (prop === "textToSpeech") return textToSpeech;
1671
+ if (prop === "composeMusic") return composeMusic;
1672
+ if (prop === "speechToSpeech") return speechToSpeech;
1673
+ if (prop === "soundEffects") return soundEffects;
1674
+ if (prop === "transcribe") return transcribe;
1675
+ return unsupported(String(prop));
1676
+ }
1677
+ });
1678
+ }
1679
+ return {
1680
+ signedUrl: doSignedUrl,
1681
+ tts: doTts,
1682
+ music: doMusic,
1683
+ agent: {
1684
+ create: notImplemented("agent.create"),
1685
+ patch: notImplemented("agent.patch"),
1686
+ delete: notImplemented("agent.delete")
1687
+ },
1688
+ audioTags: doAudioTags,
1689
+ op: doOp,
1690
+ // In proxy mode we don't own an EL client: media calls go through the
1691
+ // Shogo API, anything else is a loud error.
1692
+ elevenLabs: createProxyElevenLabs()
1693
+ };
1694
+ }
1695
+ function createVoiceHandlers(config) {
1696
+ const log = config.logger ?? NOOP_LOGGER;
1697
+ const proxy = resolveProxyOptions(config);
1698
+ if (proxy) {
1699
+ return createProxyHandlers(proxy, log);
1700
+ }
1701
+ if (!config.apiKey) {
1702
+ throw new Error(
1703
+ 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)"
1704
+ );
1705
+ }
1706
+ const getUser = config.getUser;
1707
+ const companionStore = config.companionStore;
1708
+ const includeMemoryTools = config.includeMemoryTools ?? true;
1709
+ const tools = includeMemoryTools ? MEMORY_CLIENT_TOOLS : void 0;
1710
+ const elConfig = {
1711
+ apiKey: config.apiKey,
1712
+ ...config.elevenLabsBaseUrl ? { baseUrl: config.elevenLabsBaseUrl } : {},
1713
+ ...config.fetch ? { fetch: config.fetch } : {}
1714
+ };
1715
+ const el = new ElevenLabsClient(elConfig);
1716
+ async function resolveUser(req) {
1717
+ if (!getUser) {
1718
+ return json(
1719
+ {
1720
+ error: "Voice handler misconfigured",
1721
+ detail: "signedUrl / agent.* need getUser (and companionStore); tts and music do not."
1722
+ },
1723
+ 500
1724
+ );
1725
+ }
1726
+ const u = await getUser(req);
1727
+ if (!u) return json({ error: "Unauthorized" }, 401);
1728
+ return u;
1729
+ }
1730
+ function missingCompanionStore() {
1731
+ return json(
1732
+ {
1733
+ error: "Voice handler misconfigured",
1734
+ detail: "companionStore is required for signedUrl / agent.*; tts and music do not need it."
1735
+ },
1736
+ 500
1737
+ );
1738
+ }
1739
+ function handleElError(e, fallbackLabel) {
1740
+ if (e instanceof ElevenLabsApiError) {
1741
+ log("warn", `${fallbackLabel}: 11Labs ${e.status}`, { body: e.body });
1742
+ return json({ error: fallbackLabel, detail: e.body }, 502);
1743
+ }
1744
+ const message = e instanceof Error ? e.message : String(e);
1745
+ log("error", `${fallbackLabel}: ${message}`);
1746
+ return json({ error: fallbackLabel, detail: message }, 500);
1747
+ }
1748
+ async function doSignedUrl(req) {
1749
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
1750
+ const u = await resolveUser(req);
1751
+ if (u instanceof Response) return u;
1752
+ if (!companionStore) return missingCompanionStore();
1753
+ const companion = await companionStore.findByUserId(u.id);
1754
+ if (!companion || !companion.agentId) return json({ error: "No companion" }, 404);
1755
+ const basePrompt = extractBasePrompt(companion.systemPrompt || "");
1756
+ try {
1757
+ await el.patchAgent(companion.agentId, {
1758
+ systemPrompt: basePrompt,
1759
+ tools,
1760
+ enableUserContextOverride: includeMemoryTools,
1761
+ voiceId: companion.voiceId,
1762
+ expressivity: readExpressivity(companion.expressivity),
1763
+ audioTags: readAudioTags(companion.audioTags),
1764
+ voiceSettings: readVoiceSettings(companion.voiceSettings),
1765
+ ttsModelId: companion.ttsModelId ?? null,
1766
+ ...includeMemoryTools ? {} : { memoryBlock: null }
1767
+ });
1768
+ if (basePrompt !== companion.systemPrompt) {
1769
+ await companionStore.update(u.id, { systemPrompt: basePrompt });
1770
+ }
1771
+ } catch (e) {
1772
+ log("warn", "signedUrl: pre-sync patch failed; continuing", {
1773
+ error: e instanceof Error ? e.message : String(e)
1774
+ });
1775
+ }
1776
+ let signedUrl;
1777
+ try {
1778
+ signedUrl = await el.getSignedUrl(companion.agentId);
1779
+ } catch (e) {
1780
+ return handleElError(e, "Failed to get signed url");
1781
+ }
1782
+ let userContext = "";
1783
+ if (config.memoryStore) {
1784
+ try {
1785
+ const store = config.memoryStore(u.id);
1786
+ const hits = store.search(companion.characterName || "recent context", { limit: 5 });
1787
+ if (hits.length) {
1788
+ userContext = hits.map(
1789
+ (h) => `- ${h.matchType ? `[${h.matchType}] ` : ""}${h.chunk.trim().replace(/\s+/g, " ").slice(0, 220)}`
1790
+ ).join("\n");
1791
+ }
1792
+ } catch (e) {
1793
+ log("warn", "signedUrl: memory preload failed", {
1794
+ error: e instanceof Error ? e.message : String(e)
1795
+ });
1796
+ }
1797
+ }
1798
+ return json({ signedUrl, agentId: companion.agentId, userContext });
1799
+ }
1800
+ async function doTts(req) {
1801
+ if (req.method !== "POST") return json({ error: "Method Not Allowed" }, 405);
1802
+ if (getUser) {
1803
+ const u = await resolveUser(req);
1804
+ if (u instanceof Response) return u;
1805
+ }
1806
+ const raw = await readJson(req);
1807
+ if (raw === null || typeof raw !== "object") return json({ error: "Invalid JSON body" }, 400);
1808
+ const body = raw;
1809
+ const voiceId = typeof body.voiceId === "string" ? body.voiceId.trim() : "";
1810
+ if (!voiceId) return json({ error: "Missing voiceId" }, 400);
1811
+ const voiceSettings = normalizeVoiceSettings(body.voiceSettings) ?? { stability: 0.5, similarity_boost: 0.8, style: 0 };
1812
+ let text;
1813
+ if (typeof body.text === "string" && body.text.trim().length > 0) {
1814
+ text = body.text.slice(0, 400);
1815
+ } else {
1816
+ const tags = normalizeAudioTags(body.audioTags) ?? [];
1817
+ text = buildPreviewLine(tags, body.characterName ?? "your companion").slice(0, 400);
1818
+ }
1819
+ try {
1820
+ const result = await el.textToSpeech({
1821
+ voiceId,
1822
+ text,
1823
+ ...body.modelId ? { modelId: body.modelId } : {},
1824
+ voiceSettings
1825
+ });
1826
+ const headers = {
1827
+ "content-type": result.contentType,
1828
+ "cache-control": "private, max-age=3600",
1829
+ "x-tts-model-used": result.modelId,
1830
+ "x-tts-model-requested": result.requestedModel
1831
+ };
1832
+ if (result.fellBack) {
1833
+ headers.warning = `199 - "model ${result.requestedModel} not available, used ${result.modelId}"`;
1834
+ log("warn", "tts fell back to another model", {
1835
+ requested: result.requestedModel,
1836
+ used: result.modelId,
1837
+ reason: result.fallbackReason
1838
+ });
1839
+ }
1840
+ return new Response(result.audio, { headers });
1841
+ } catch (e) {
1842
+ return handleElError(e, "TTS failed");
1843
+ }
1844
+ }
1845
+ async function doMusic(req) {
1846
+ if (req.method !== "POST") return json({ error: "Method Not Allowed" }, 405);
1847
+ if (getUser) {
1848
+ const u = await resolveUser(req);
1849
+ if (u instanceof Response) return u;
1850
+ }
1851
+ const raw = await readJson(req);
1852
+ const parsed = parseMusicBody(raw);
1853
+ if ("error" in parsed) return json({ error: parsed.error }, 400);
1854
+ try {
1855
+ const { audio, contentType } = await el.composeMusic(parsed.params);
1856
+ return new Response(audio, {
1857
+ headers: {
1858
+ "content-type": contentType,
1859
+ "cache-control": "private, max-age=3600"
1860
+ }
1861
+ });
1862
+ } catch (e) {
1863
+ return handleElError(e, "Music generation failed");
1864
+ }
1865
+ }
1866
+ async function doOp(req, opId) {
1867
+ const id = resolveOpId(req, opId);
1868
+ const op = getVoiceOp(id);
1869
+ if (!op) {
1870
+ return json(
1871
+ { error: "unknown_op", detail: `Unknown voice op '${id ?? ""}'`, allowed: VOICE_OP_IDS },
1872
+ 404
1873
+ );
1874
+ }
1875
+ if (req.method !== op.method) return json({ error: "Method Not Allowed" }, 405);
1876
+ const parsed = await readVoiceOpBody(op, req);
1877
+ if (parsed instanceof Response) return parsed;
1878
+ const fields = parsed.kind === "none" ? {} : parsed.fields;
1879
+ let query;
1880
+ try {
1881
+ query = new URL(req.url).searchParams;
1882
+ } catch {
1883
+ query = new URLSearchParams();
1884
+ }
1885
+ const pathParams = voiceOpPathParams(op, fields, query);
1886
+ const upstream = voiceOpUpstreamPath(op, pathParams);
1887
+ if (typeof upstream !== "string") {
1888
+ return json({ error: upstream.error, detail: `set it in the body or as ?${op.pathParams?.[0]}=` }, 400);
1889
+ }
1890
+ const forwardedQuery = {};
1891
+ for (const key of op.queryForward ?? []) {
1892
+ const value = query.get(key);
1893
+ if (value) forwardedQuery[key] = value;
1894
+ }
1895
+ if (op.defaultVoiceId && !pathParams.voiceId) {
1896
+ return json(
1897
+ {
1898
+ error: "Missing `voiceId`",
1899
+ detail: `BYO mode has no project default voice \u2014 pass voiceId (or ?voiceId=)`
1900
+ },
1901
+ 400
1902
+ );
1903
+ }
1904
+ try {
1905
+ if (op.call === "textToSpeech") {
1906
+ const result = await el.textToSpeech({
1907
+ voiceId: pathParams.voiceId ?? String(fields.voiceId ?? ""),
1908
+ text: String(fields.text ?? ""),
1909
+ ...typeof fields.modelId === "string" ? { modelId: fields.modelId } : {},
1910
+ ...fields.voiceSettings ? { voiceSettings: fields.voiceSettings } : {},
1911
+ ...op.accept ? { accept: op.accept } : {}
1912
+ });
1913
+ const headers2 = {
1914
+ "content-type": result.contentType,
1915
+ "cache-control": "private, max-age=3600",
1916
+ "x-tts-model-used": result.modelId,
1917
+ "x-tts-model-requested": result.requestedModel
1918
+ };
1919
+ if (result.fellBack) {
1920
+ headers2.warning = `199 - "model ${result.requestedModel} not available, used ${result.modelId}"`;
1921
+ }
1922
+ return new Response(result.audio, { headers: headers2 });
1923
+ }
1924
+ if (op.call === "composeMusic") {
1925
+ const { audio, contentType } = await el.composeMusic({
1926
+ ...typeof fields.prompt === "string" ? { prompt: fields.prompt } : {},
1927
+ ...fields.compositionPlan ? { compositionPlan: fields.compositionPlan } : {},
1928
+ ...typeof fields.musicLengthMs === "number" ? { musicLengthMs: fields.musicLengthMs } : {},
1929
+ ...typeof fields.modelId === "string" ? { modelId: fields.modelId } : {},
1930
+ ...typeof fields.forceInstrumental === "boolean" ? { forceInstrumental: fields.forceInstrumental } : {},
1931
+ ...typeof fields.outputFormat === "string" ? { outputFormat: fields.outputFormat } : {}
1932
+ });
1933
+ return new Response(audio, {
1934
+ headers: {
1935
+ "content-type": contentType,
1936
+ "cache-control": "private, max-age=3600"
1937
+ }
1938
+ });
1939
+ }
1940
+ const upstreamBody = voiceOpUpstreamBody(op, fields);
1941
+ if (op.defaultModel && upstreamBody.model_id === void 0) {
1942
+ upstreamBody.model_id = op.defaultModel;
1943
+ }
1944
+ const response = await el.request({
1945
+ method: op.method,
1946
+ path: upstream,
1947
+ ...parsed.kind === "form" ? { body: parsed.form } : parsed.kind === "json" ? { body: upstreamBody } : {},
1948
+ ...Object.keys(forwardedQuery).length ? { query: forwardedQuery } : {},
1949
+ ...op.accept ? { accept: op.accept } : {}
1950
+ });
1951
+ const headers = { "content-type": response.contentType };
1952
+ if (voiceOpReturnsAudio(op)) headers["cache-control"] = "private, max-age=3600";
1953
+ const body = response.audio ?? JSON.stringify(response.json ?? {});
1954
+ return new Response(body, { status: response.status, headers });
1955
+ } catch (e) {
1956
+ return handleElError(e, `${op.id} failed`);
1957
+ }
1958
+ }
1959
+ async function doAgentCreate(req) {
1960
+ if (req.method !== "POST") return json({ error: "Method Not Allowed" }, 405);
1961
+ const u = await resolveUser(req);
1962
+ if (u instanceof Response) return u;
1963
+ if (!companionStore) return missingCompanionStore();
1964
+ const existing = await companionStore.findByUserId(u.id);
1965
+ if (existing) return json({ error: "Companion already exists" }, 409);
1966
+ const raw = await readJson(req);
1967
+ if (raw === null || typeof raw !== "object") return json({ error: "Invalid JSON body" }, 400);
1968
+ const body = raw;
1969
+ if (typeof body.displayName !== "string" || typeof body.characterName !== "string" || typeof body.voiceId !== "string" || typeof body.systemPrompt !== "string" || typeof body.firstMessage !== "string") {
1970
+ return json({ error: "Missing required fields" }, 400);
1971
+ }
1972
+ const expressivity = readExpressivity(body.expressivity);
1973
+ const audioTags = readAudioTags(body.audioTags);
1974
+ const voiceSettings = readVoiceSettings(body.voiceSettings);
1975
+ const ttsModelId = resolveConvaiTtsModel(body.ttsModelId);
1976
+ let agentId;
1977
+ try {
1978
+ agentId = await el.createAgent({
1979
+ displayName: body.displayName,
1980
+ characterName: body.characterName,
1981
+ voiceId: body.voiceId,
1982
+ systemPrompt: body.systemPrompt,
1983
+ firstMessage: body.firstMessage,
1984
+ expressivity,
1985
+ audioTags,
1986
+ voiceSettings,
1987
+ ttsModelId,
1988
+ ...tools ? { tools } : {},
1989
+ ...includeMemoryTools ? {} : { memoryBlock: null }
1990
+ });
1991
+ } catch (e) {
1992
+ return handleElError(e, "Failed to create agent");
1993
+ }
1994
+ const cleanedPrompt = extractBasePrompt(body.systemPrompt);
1995
+ const companion = await companionStore.create({
1996
+ userId: u.id,
1997
+ agentId,
1998
+ displayName: body.displayName,
1999
+ characterName: body.characterName,
2000
+ voiceId: body.voiceId,
2001
+ systemPrompt: cleanedPrompt,
2002
+ firstMessage: body.firstMessage,
2003
+ expressivity,
2004
+ audioTags,
2005
+ voiceSettings,
2006
+ ttsModelId
2007
+ });
2008
+ return json(companion);
2009
+ }
2010
+ async function doAgentPatch(req) {
2011
+ if (req.method !== "PATCH") return json({ error: "Method Not Allowed" }, 405);
2012
+ const u = await resolveUser(req);
2013
+ if (u instanceof Response) return u;
2014
+ if (!companionStore) return missingCompanionStore();
2015
+ const existing = await companionStore.findByUserId(u.id);
2016
+ if (!existing) return json({ error: "No companion" }, 404);
2017
+ const raw = await readJson(req);
2018
+ if (raw === null || typeof raw !== "object") return json({ error: "Invalid JSON body" }, 400);
2019
+ const body = raw;
2020
+ const patch = {};
2021
+ if (body.displayName !== void 0) patch.displayName = body.displayName;
2022
+ if (body.characterName !== void 0) patch.characterName = body.characterName;
2023
+ if (body.voiceId !== void 0) patch.voiceId = body.voiceId;
2024
+ if (body.firstMessage !== void 0) patch.firstMessage = body.firstMessage;
2025
+ const expressivity = normalizeExpressivity(body.expressivity);
2026
+ if (expressivity !== void 0) patch.expressivity = expressivity;
2027
+ let audioTagsPatch;
2028
+ if (body.audioTags !== void 0) {
2029
+ audioTagsPatch = normalizeAudioTags(body.audioTags) ?? [];
2030
+ patch.audioTags = audioTagsPatch;
2031
+ }
2032
+ if (body.voiceSettings !== void 0) {
2033
+ const vs = normalizeVoiceSettings(body.voiceSettings);
2034
+ patch.voiceSettings = vs ?? null;
2035
+ }
2036
+ if (body.ttsModelId !== void 0) patch.ttsModelId = body.ttsModelId ?? null;
2037
+ const cleanedPrompt = body.systemPrompt !== void 0 ? extractBasePrompt(body.systemPrompt) : void 0;
2038
+ if (cleanedPrompt !== void 0) patch.systemPrompt = cleanedPrompt;
2039
+ 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;
2040
+ if (existing.agentId && needs11Labs) {
2041
+ const finalExpressivity = patch.expressivity ?? readExpressivity(existing.expressivity);
2042
+ const finalTags = audioTagsPatch !== void 0 ? audioTagsPatch : readAudioTags(existing.audioTags);
2043
+ const finalVoiceSettings = body.voiceSettings !== void 0 ? normalizeVoiceSettings(body.voiceSettings) ?? void 0 : readVoiceSettings(existing.voiceSettings);
2044
+ const finalTtsModelId = body.ttsModelId !== void 0 ? body.ttsModelId ?? null : existing.ttsModelId ?? null;
2045
+ try {
2046
+ await el.patchAgent(existing.agentId, {
2047
+ ...body.displayName !== void 0 ? { displayName: body.displayName } : {},
2048
+ ...body.characterName !== void 0 ? { characterName: body.characterName } : {},
2049
+ ...body.voiceId !== void 0 ? { voiceId: body.voiceId } : {},
2050
+ systemPrompt: cleanedPrompt !== void 0 ? cleanedPrompt : body.expressivity !== void 0 || body.audioTags !== void 0 ? existing.systemPrompt : void 0,
2051
+ ...body.firstMessage !== void 0 ? { firstMessage: body.firstMessage } : {},
2052
+ expressivity: finalExpressivity,
2053
+ audioTags: finalTags,
2054
+ ...finalVoiceSettings ? { voiceSettings: finalVoiceSettings } : {},
2055
+ ttsModelId: finalTtsModelId,
2056
+ ...includeMemoryTools ? {} : { memoryBlock: null }
2057
+ });
2058
+ } catch (e) {
2059
+ return handleElError(e, "Failed to patch agent");
2060
+ }
2061
+ }
2062
+ const companion = await companionStore.update(u.id, patch);
2063
+ return json(companion);
2064
+ }
2065
+ async function doAgentDelete(req) {
2066
+ if (req.method !== "DELETE") return json({ error: "Method Not Allowed" }, 405);
2067
+ const u = await resolveUser(req);
2068
+ if (u instanceof Response) return u;
2069
+ if (!companionStore) return missingCompanionStore();
2070
+ const existing = await companionStore.findByUserId(u.id);
2071
+ if (!existing) return json({ ok: true });
2072
+ if (existing.agentId) await el.deleteAgent(existing.agentId);
2073
+ await companionStore.delete(u.id);
2074
+ return json({ ok: true });
2075
+ }
2076
+ async function doAudioTags(req) {
2077
+ if (req.method !== "GET") return json({ error: "Method Not Allowed" }, 405);
2078
+ return json({
2079
+ tags: AUDIO_TAGS,
2080
+ groups: AUDIO_TAG_GROUPS,
2081
+ expressivity: EXPRESSIVITY_OPTIONS,
2082
+ defaults: {
2083
+ allowedTags: DEFAULT_ALLOWED_TAGS,
2084
+ voiceSettings: DEFAULT_VOICE_SETTINGS,
2085
+ ttsModelId: CONVAI_TTS_MODEL_FALLBACK
2086
+ }
2087
+ });
2088
+ }
2089
+ return {
2090
+ signedUrl: doSignedUrl,
2091
+ tts: doTts,
2092
+ music: doMusic,
2093
+ agent: {
2094
+ create: doAgentCreate,
2095
+ patch: doAgentPatch,
2096
+ delete: doAgentDelete
2097
+ },
2098
+ audioTags: doAudioTags,
2099
+ op: doOp,
2100
+ elevenLabs: el
2101
+ };
2102
+ }
2103
+
2104
+ // src/route/index.ts
2105
+ function defaultHandlers() {
2106
+ return createVoiceHandlers({});
2107
+ }
2108
+
2109
+ // src/route/op.ts
2110
+ var GET = (req) => defaultHandlers().op(req);
2111
+ var POST = (req) => defaultHandlers().op(req);
2112
+ var voiceOpRoutes = Object.fromEntries(
2113
+ VOICE_OPS.map((op) => [
2114
+ op.id,
2115
+ {
2116
+ GET: (req) => defaultHandlers().op(req, op.id),
2117
+ POST: (req) => defaultHandlers().op(req, op.id)
2118
+ }
2119
+ ])
2120
+ );
2121
+ // Annotate the CommonJS export names for ESM import in node:
2122
+ 0 && (module.exports = {
2123
+ GET,
2124
+ POST,
2125
+ voiceOpRoutes
2126
+ });
2127
+ //# sourceMappingURL=op.cjs.map