@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.
- package/README.md +89 -0
- package/dist/{chunk-S3S2RNO6.js → chunk-BI6SL35K.js} +1 -184
- package/dist/chunk-IOFWML5L.js +1078 -0
- package/dist/chunk-KMSL5PWK.js +540 -0
- package/dist/chunk-ME3SIZZU.js +581 -0
- package/dist/{chunk-T3T4EBWY.js → chunk-RALZRMFL.js} +6 -2
- package/dist/chunk-TQOBJMEK.js +17 -0
- package/dist/chunk-U5UC7ZVS.js +988 -0
- package/dist/chunk-UW7VQ3S6.js +148 -0
- package/dist/chunk-ZAW6F3YT.js +172 -0
- package/dist/index.cjs +748 -25
- package/dist/index.d.cts +396 -3
- package/dist/index.d.ts +396 -3
- package/dist/index.js +63 -3
- package/dist/live/client.cjs +173 -0
- package/dist/live/client.d.cts +132 -0
- package/dist/live/client.d.ts +132 -0
- package/dist/live/client.js +9 -0
- package/dist/native/index.d.cts +5 -2
- package/dist/native/index.d.ts +5 -2
- package/dist/native/index.js +15 -11
- package/dist/{organicParticlesShaders-DIva0vBO.d.ts → organicParticlesShaders-seZq7jYX.d.cts} +1 -421
- package/dist/{organicParticlesShaders-DIva0vBO.d.cts → organicParticlesShaders-seZq7jYX.d.ts} +1 -421
- package/dist/particlesConfig-0YbdPOdY.d.cts +223 -0
- package/dist/particlesConfig-0YbdPOdY.d.ts +223 -0
- package/dist/react/conversation.cjs +789 -0
- package/dist/react/conversation.d.cts +134 -0
- package/dist/react/conversation.d.ts +134 -0
- package/dist/react/conversation.js +13 -0
- package/dist/react/index.cjs +374 -36
- package/dist/react/index.d.cts +10 -141
- package/dist/react/index.d.ts +10 -141
- package/dist/react/index.js +23 -770
- package/dist/react/visuals.cjs +1048 -0
- package/dist/react/visuals.d.cts +70 -0
- package/dist/react/visuals.d.ts +70 -0
- package/dist/react/visuals.js +10 -0
- package/dist/route/agent.cjs +1047 -85
- package/dist/route/agent.js +3 -3
- package/dist/route/audio-tags.cjs +1047 -85
- package/dist/route/audio-tags.js +3 -3
- package/dist/route/index.cjs +1051 -85
- package/dist/route/index.d.cts +19 -3
- package/dist/route/index.d.ts +19 -3
- package/dist/route/index.js +3 -3
- package/dist/route/music.cjs +1047 -85
- package/dist/route/music.js +3 -3
- package/dist/route/op.cjs +2127 -0
- package/dist/route/op.d.cts +43 -0
- package/dist/route/op.d.ts +43 -0
- package/dist/route/op.js +27 -0
- package/dist/route/signed-url.cjs +1047 -85
- package/dist/route/signed-url.js +3 -3
- package/dist/route/tts-preview.cjs +1047 -85
- package/dist/route/tts-preview.d.cts +6 -4
- package/dist/route/tts-preview.d.ts +6 -4
- package/dist/route/tts-preview.js +3 -3
- package/dist/server.cjs +1047 -85
- package/dist/server.d.cts +40 -8
- package/dist/server.d.ts +40 -8
- package/dist/server.js +2 -2
- package/dist/types-Bfhu-aQH.d.cts +199 -0
- package/dist/types-Bfhu-aQH.d.ts +199 -0
- package/dist/{types-W9y6eMj3.d.ts → types-CadjTXNH.d.cts} +193 -12
- package/dist/{types-W9y6eMj3.d.cts → types-CadjTXNH.d.ts} +193 -12
- package/package.json +21 -1
- package/dist/chunk-B4JC3DK2.js +0 -529
- package/dist/chunk-O362EJQR.js +0 -524
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
import {
|
|
2
|
+
composeExpressivityBlock,
|
|
3
|
+
stripExpressivityBlock
|
|
4
|
+
} from "./chunk-Q3TJAJ54.js";
|
|
5
|
+
|
|
6
|
+
// src/prompt.ts
|
|
7
|
+
var DEFAULT_MEMORY_BLOCK = `# Memory
|
|
8
|
+
|
|
9
|
+
Known context about this user from past conversations:
|
|
10
|
+
{{user_context}}
|
|
11
|
+
|
|
12
|
+
Memory retrieval is automatic \u2014 the top matching facts are already injected above. Never say "let me check my memory" or "searching memories."
|
|
13
|
+
|
|
14
|
+
You have one tool:
|
|
15
|
+
- 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.`;
|
|
16
|
+
function stripMemoryBlock(prompt) {
|
|
17
|
+
if (!prompt) return prompt;
|
|
18
|
+
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();
|
|
19
|
+
}
|
|
20
|
+
function extractBasePrompt(prompt) {
|
|
21
|
+
return stripExpressivityBlock(stripMemoryBlock(prompt));
|
|
22
|
+
}
|
|
23
|
+
function composeAgentPrompt(basePrompt, options = {}) {
|
|
24
|
+
const { expressivity = "subtle", audioTags = null, memoryBlock = DEFAULT_MEMORY_BLOCK } = options;
|
|
25
|
+
const stripped = extractBasePrompt(basePrompt);
|
|
26
|
+
const expr = composeExpressivityBlock(expressivity, audioTags);
|
|
27
|
+
const parts = [stripped];
|
|
28
|
+
if (expr) parts.push(expr);
|
|
29
|
+
if (memoryBlock) parts.push(memoryBlock);
|
|
30
|
+
return parts.join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/elevenlabs.ts
|
|
34
|
+
var DEFAULT_ELEVENLABS_BASE_URL = "https://api.elevenlabs.io";
|
|
35
|
+
var VOICE_TTS_MODELS = [
|
|
36
|
+
{ id: "eleven_v4", label: "Eleven v4", realtime: false, maxChars: 1e4 },
|
|
37
|
+
{ id: "eleven_v4_turbo", label: "Eleven v4 Turbo", realtime: true, maxChars: 1e4 },
|
|
38
|
+
{ id: "eleven_v3", label: "Eleven v3", realtime: false, maxChars: 5e3 },
|
|
39
|
+
{
|
|
40
|
+
id: "eleven_v3_conversational",
|
|
41
|
+
label: "Eleven v3 Conversational",
|
|
42
|
+
realtime: true,
|
|
43
|
+
maxChars: 5e3
|
|
44
|
+
},
|
|
45
|
+
{ id: "eleven_multilingual_v2", label: "Eleven Multilingual v2", realtime: false, maxChars: 1e4 },
|
|
46
|
+
{ id: "eleven_flash_v2_5", label: "Eleven Flash v2.5", realtime: true, maxChars: 4e4 },
|
|
47
|
+
{ id: "eleven_flash_v2", label: "Eleven Flash v2", realtime: true, maxChars: 3e4 },
|
|
48
|
+
{ id: "eleven_turbo_v2_5", label: "Eleven Turbo v2.5", realtime: true, maxChars: 4e4 },
|
|
49
|
+
{ id: "eleven_turbo_v2", label: "Eleven Turbo v2", realtime: true, maxChars: 3e4, deprecated: true }
|
|
50
|
+
];
|
|
51
|
+
var VOICE_TTS_MODEL_IDS = VOICE_TTS_MODELS.map((m) => m.id);
|
|
52
|
+
var DEFAULT_TTS_MODEL = "eleven_v4";
|
|
53
|
+
var CONVAI_SUPPORTED_TTS_MODELS = new Set(
|
|
54
|
+
VOICE_TTS_MODELS.filter((m) => m.realtime).map((m) => m.id)
|
|
55
|
+
);
|
|
56
|
+
var CONVAI_TTS_MODEL_FALLBACK = "eleven_v4_turbo";
|
|
57
|
+
var TTS_FALLBACK_MODEL = "eleven_flash_v2_5";
|
|
58
|
+
function getVoiceTtsModel(modelId) {
|
|
59
|
+
if (!modelId) return void 0;
|
|
60
|
+
return VOICE_TTS_MODELS.find((m) => m.id === modelId);
|
|
61
|
+
}
|
|
62
|
+
var loggedDowngrades = /* @__PURE__ */ new Set();
|
|
63
|
+
function resolveConvaiTtsModelDetailed(requested) {
|
|
64
|
+
if (requested && CONVAI_SUPPORTED_TTS_MODELS.has(requested)) {
|
|
65
|
+
return { modelId: requested, requested, downgraded: false };
|
|
66
|
+
}
|
|
67
|
+
const known = getVoiceTtsModel(requested);
|
|
68
|
+
const reason = !requested ? void 0 : known ? `${requested} is not supported by the convai realtime runtime` : `unknown TTS model ${requested}`;
|
|
69
|
+
if (reason && !loggedDowngrades.has(requested)) {
|
|
70
|
+
loggedDowngrades.add(requested);
|
|
71
|
+
try {
|
|
72
|
+
console.warn(
|
|
73
|
+
`[voice] convai TTS model ${requested} is unavailable (${reason}); using ${CONVAI_TTS_MODEL_FALLBACK}`
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
modelId: CONVAI_TTS_MODEL_FALLBACK,
|
|
80
|
+
requested: requested ?? null,
|
|
81
|
+
downgraded: Boolean(requested),
|
|
82
|
+
...reason ? { reason } : {}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function resolveConvaiTtsModel(requested) {
|
|
86
|
+
return resolveConvaiTtsModelDetailed(requested).modelId;
|
|
87
|
+
}
|
|
88
|
+
function isModelRelatedTtsFailure(status, body) {
|
|
89
|
+
if (status === 402) return true;
|
|
90
|
+
if (status !== 400 && status !== 403 && status !== 404 && status !== 422) return false;
|
|
91
|
+
return /model|plan|permission/i.test(body);
|
|
92
|
+
}
|
|
93
|
+
var DEFAULT_MUSIC_MODEL = "music_v1";
|
|
94
|
+
var VOICE_STS_MODEL_IDS = [
|
|
95
|
+
"eleven_multilingual_sts_v2",
|
|
96
|
+
"eleven_english_sts_v2"
|
|
97
|
+
];
|
|
98
|
+
var DEFAULT_STS_MODEL = "eleven_multilingual_sts_v2";
|
|
99
|
+
var VOICE_SPEECH_TO_TEXT_MODEL_IDS = ["scribe_v1"];
|
|
100
|
+
var DEFAULT_SPEECH_TO_TEXT_MODEL = "scribe_v1";
|
|
101
|
+
var VOICE_SOUND_GENERATION_MODEL_IDS = ["eleven_text_to_sound_v2"];
|
|
102
|
+
var DEFAULT_SOUND_GENERATION_MODEL = "eleven_text_to_sound_v2";
|
|
103
|
+
var ElevenLabsApiError = class extends Error {
|
|
104
|
+
constructor(message, status, body) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.status = status;
|
|
107
|
+
this.body = body;
|
|
108
|
+
this.name = "ElevenLabsApiError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var ElevenLabsClient = class {
|
|
112
|
+
constructor(config) {
|
|
113
|
+
this.config = config;
|
|
114
|
+
if (!config.apiKey) throw new Error("ElevenLabsClient: apiKey is required");
|
|
115
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_ELEVENLABS_BASE_URL).replace(/\/+$/, "");
|
|
116
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch;
|
|
117
|
+
if (typeof this.fetchImpl !== "function") {
|
|
118
|
+
throw new Error("ElevenLabsClient: global fetch is unavailable; pass config.fetch");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
headers(extra = {}) {
|
|
122
|
+
return {
|
|
123
|
+
"xi-api-key": this.config.apiKey,
|
|
124
|
+
...extra
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Create a new convai agent. Returns the new `agent_id`. */
|
|
128
|
+
async createAgent(params) {
|
|
129
|
+
const prompt = composeAgentPrompt(params.systemPrompt, {
|
|
130
|
+
expressivity: params.expressivity ?? "subtle",
|
|
131
|
+
audioTags: params.audioTags ?? null,
|
|
132
|
+
memoryBlock: params.memoryBlock
|
|
133
|
+
});
|
|
134
|
+
const ttsConfig = {
|
|
135
|
+
voice_id: params.voiceId,
|
|
136
|
+
model_id: resolveConvaiTtsModel(params.ttsModelId)
|
|
137
|
+
};
|
|
138
|
+
if (params.voiceSettings) ttsConfig.voice_settings = params.voiceSettings;
|
|
139
|
+
const body = {
|
|
140
|
+
name: `Companion-${params.characterName}-for-${params.displayName}`,
|
|
141
|
+
conversation_config: {
|
|
142
|
+
agent: {
|
|
143
|
+
prompt: {
|
|
144
|
+
prompt,
|
|
145
|
+
...params.tools ? { tools: params.tools } : {}
|
|
146
|
+
},
|
|
147
|
+
first_message: params.firstMessage,
|
|
148
|
+
language: params.language ?? "en"
|
|
149
|
+
},
|
|
150
|
+
tts: ttsConfig
|
|
151
|
+
},
|
|
152
|
+
platform_settings: {
|
|
153
|
+
overrides: {
|
|
154
|
+
conversation_config_override: {
|
|
155
|
+
agent: { prompt: { prompt: true }, first_message: true, language: true },
|
|
156
|
+
tts: { voice_id: true },
|
|
157
|
+
conversation: { text_only: true }
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
dynamic_variables: {
|
|
161
|
+
dynamic_variable_placeholders: {
|
|
162
|
+
user_context: "No prior memories yet.",
|
|
163
|
+
character_name: "",
|
|
164
|
+
...params.dynamicVariablePlaceholders ?? {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/create`, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
172
|
+
body: JSON.stringify(body)
|
|
173
|
+
});
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const text = await res.text();
|
|
176
|
+
throw new ElevenLabsApiError(`createAgent failed: ${res.status}`, res.status, text);
|
|
177
|
+
}
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
return data.agent_id;
|
|
180
|
+
}
|
|
181
|
+
/** PATCH any subset of agent settings. Idempotent; only sends the keys you supply. */
|
|
182
|
+
async patchAgent(agentId, params) {
|
|
183
|
+
const body = {};
|
|
184
|
+
if (params.displayName !== void 0 || params.characterName !== void 0) {
|
|
185
|
+
body.name = `Companion-${params.characterName ?? ""}-for-${params.displayName ?? ""}`.replace(
|
|
186
|
+
/-for-$/,
|
|
187
|
+
""
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const conv = {};
|
|
191
|
+
const agent = {};
|
|
192
|
+
const prompt = {};
|
|
193
|
+
if (params.systemPrompt !== void 0) {
|
|
194
|
+
prompt.prompt = composeAgentPrompt(params.systemPrompt, {
|
|
195
|
+
expressivity: params.expressivity ?? "subtle",
|
|
196
|
+
audioTags: params.audioTags ?? null,
|
|
197
|
+
memoryBlock: params.memoryBlock
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (params.tools) prompt.tools = params.tools;
|
|
201
|
+
if (Object.keys(prompt).length) agent.prompt = prompt;
|
|
202
|
+
if (params.firstMessage !== void 0) agent.first_message = params.firstMessage;
|
|
203
|
+
if (Object.keys(agent).length) conv.agent = agent;
|
|
204
|
+
const tts = {};
|
|
205
|
+
if (params.voiceId !== void 0) tts.voice_id = params.voiceId;
|
|
206
|
+
if (params.voiceSettings) tts.voice_settings = params.voiceSettings;
|
|
207
|
+
if (params.ttsModelId !== void 0) tts.model_id = resolveConvaiTtsModel(params.ttsModelId);
|
|
208
|
+
if (Object.keys(tts).length) conv.tts = tts;
|
|
209
|
+
if (Object.keys(conv).length) body.conversation_config = conv;
|
|
210
|
+
if (params.enableUserContextOverride) {
|
|
211
|
+
body.platform_settings = {
|
|
212
|
+
overrides: {
|
|
213
|
+
conversation_config_override: {
|
|
214
|
+
agent: { prompt: { prompt: true }, first_message: true, language: true },
|
|
215
|
+
tts: { voice_id: true },
|
|
216
|
+
conversation: { text_only: true }
|
|
217
|
+
},
|
|
218
|
+
custom_llm_extra_body: false,
|
|
219
|
+
enable_conversation_initiation_client_data_from_webhook: false
|
|
220
|
+
},
|
|
221
|
+
dynamic_variables: {
|
|
222
|
+
dynamic_variable_placeholders: {
|
|
223
|
+
user_context: "No prior memories yet.",
|
|
224
|
+
character_name: ""
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (!Object.keys(body).length) return;
|
|
230
|
+
const res = await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/${encodeURIComponent(agentId)}`, {
|
|
231
|
+
method: "PATCH",
|
|
232
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
233
|
+
body: JSON.stringify(body)
|
|
234
|
+
});
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
const text = await res.text();
|
|
237
|
+
throw new ElevenLabsApiError(`patchAgent failed: ${res.status}`, res.status, text);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Delete a convai agent. Best-effort: swallow errors so teardown during a
|
|
242
|
+
* 500 response doesn't cascade into a second failure.
|
|
243
|
+
*/
|
|
244
|
+
async deleteAgent(agentId) {
|
|
245
|
+
try {
|
|
246
|
+
await this.fetchImpl(`${this.baseUrl}/v1/convai/agents/${encodeURIComponent(agentId)}`, {
|
|
247
|
+
method: "DELETE",
|
|
248
|
+
headers: this.headers()
|
|
249
|
+
});
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Fetch a short-lived signed URL the browser can use to start a session. */
|
|
254
|
+
async getSignedUrl(agentId) {
|
|
255
|
+
const res = await this.fetchImpl(
|
|
256
|
+
`${this.baseUrl}/v1/convai/conversation/get-signed-url?agent_id=${encodeURIComponent(agentId)}`,
|
|
257
|
+
{ headers: this.headers() }
|
|
258
|
+
);
|
|
259
|
+
if (!res.ok) {
|
|
260
|
+
const text = await res.text();
|
|
261
|
+
throw new ElevenLabsApiError(`getSignedUrl failed: ${res.status}`, res.status, text);
|
|
262
|
+
}
|
|
263
|
+
const data = await res.json();
|
|
264
|
+
return data.signed_url;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* One-shot TTS. Returns the raw audio bytes plus the model that actually
|
|
268
|
+
* produced them.
|
|
269
|
+
*
|
|
270
|
+
* The requested model is never swapped silently: we only retry on
|
|
271
|
+
* {@link TTS_FALLBACK_MODEL} when the failure is plausibly about
|
|
272
|
+
* the model (400/403/404/422 whose body mentions model/plan/permission,
|
|
273
|
+
* or 402 for an out-of-plan model). Auth (401), rate limit (429), 5xx and
|
|
274
|
+
* network errors are surfaced as-is. When a fallback does happen the
|
|
275
|
+
* result reports `requestedModel`, `fellBack` and `fallbackReason`.
|
|
276
|
+
*/
|
|
277
|
+
async textToSpeech(params) {
|
|
278
|
+
const requestedModel = params.modelId && params.modelId.length > 0 ? params.modelId : DEFAULT_TTS_MODEL;
|
|
279
|
+
const accept = params.accept ?? "audio/mpeg";
|
|
280
|
+
const attempt = async (m) => {
|
|
281
|
+
const res2 = await this.fetchImpl(
|
|
282
|
+
`${this.baseUrl}/v1/text-to-speech/${encodeURIComponent(params.voiceId)}`,
|
|
283
|
+
{
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: this.headers({ "content-type": "application/json", accept }),
|
|
286
|
+
body: JSON.stringify({
|
|
287
|
+
text: params.text,
|
|
288
|
+
model_id: m,
|
|
289
|
+
voice_settings: params.voiceSettings
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
return res2;
|
|
294
|
+
};
|
|
295
|
+
let res = await attempt(requestedModel);
|
|
296
|
+
let used = requestedModel;
|
|
297
|
+
let fellBack = false;
|
|
298
|
+
let fallbackReason;
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
const body = await res.text();
|
|
301
|
+
if (requestedModel !== TTS_FALLBACK_MODEL && isModelRelatedTtsFailure(res.status, body)) {
|
|
302
|
+
fallbackReason = `elevenlabs ${res.status} for ${requestedModel}: ${body.slice(0, 200)}`;
|
|
303
|
+
res = await attempt(TTS_FALLBACK_MODEL);
|
|
304
|
+
used = TTS_FALLBACK_MODEL;
|
|
305
|
+
fellBack = true;
|
|
306
|
+
if (!res.ok) {
|
|
307
|
+
const retryBody = await res.text();
|
|
308
|
+
throw new ElevenLabsApiError(
|
|
309
|
+
`textToSpeech failed: ${res.status}`,
|
|
310
|
+
res.status,
|
|
311
|
+
retryBody
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
throw new ElevenLabsApiError(`textToSpeech failed: ${res.status}`, res.status, body);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const audio = await res.arrayBuffer();
|
|
319
|
+
const contentType = res.headers.get("content-type") ?? accept;
|
|
320
|
+
return {
|
|
321
|
+
audio,
|
|
322
|
+
modelId: used,
|
|
323
|
+
requestedModel,
|
|
324
|
+
fellBack,
|
|
325
|
+
...fallbackReason ? { fallbackReason } : {},
|
|
326
|
+
contentType
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Low-level escape hatch for any ElevenLabs endpoint not covered by a
|
|
331
|
+
* dedicated method (music, sound-generation, speech-to-text, dubbing,
|
|
332
|
+
* voice-design, …). Handles auth, query encoding, JSON body encoding,
|
|
333
|
+
* and JSON-vs-binary response parsing; non-2xx responses throw
|
|
334
|
+
* {@link ElevenLabsApiError} like the typed methods do.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* const { json } = await el.request({ method: 'GET', path: '/v1/models' })
|
|
339
|
+
* const { audio } = await el.request({
|
|
340
|
+
* method: 'POST',
|
|
341
|
+
* path: '/v1/sound-generation',
|
|
342
|
+
* body: { text: 'door creak' },
|
|
343
|
+
* accept: 'audio/mpeg',
|
|
344
|
+
* })
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
async request(opts) {
|
|
348
|
+
const accept = opts.accept ?? "application/json";
|
|
349
|
+
let url = `${this.baseUrl}${opts.path.startsWith("/") ? "" : "/"}${opts.path}`;
|
|
350
|
+
if (opts.query) {
|
|
351
|
+
const params = new URLSearchParams();
|
|
352
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
353
|
+
if (v !== null && v !== void 0) params.set(k, String(v));
|
|
354
|
+
}
|
|
355
|
+
const qs = params.toString();
|
|
356
|
+
if (qs) url += `${url.includes("?") ? "&" : "?"}${qs}`;
|
|
357
|
+
}
|
|
358
|
+
const headers = this.headers({ accept, ...opts.headers ?? {} });
|
|
359
|
+
const init = { method: opts.method, headers };
|
|
360
|
+
if (opts.body !== void 0 && opts.body !== null) {
|
|
361
|
+
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);
|
|
362
|
+
if (isPlainPayload) {
|
|
363
|
+
headers["content-type"] = "application/json";
|
|
364
|
+
init.body = JSON.stringify(opts.body);
|
|
365
|
+
} else {
|
|
366
|
+
init.body = opts.body;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const res = await this.fetchImpl(url, init);
|
|
370
|
+
if (!res.ok) {
|
|
371
|
+
const text = await res.text();
|
|
372
|
+
throw new ElevenLabsApiError(
|
|
373
|
+
`request ${opts.method} ${opts.path} failed: ${res.status}`,
|
|
374
|
+
res.status,
|
|
375
|
+
text
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const contentType = res.headers.get("content-type") ?? accept;
|
|
379
|
+
if (contentType.includes("application/json")) {
|
|
380
|
+
return { status: res.status, contentType, json: await res.json() };
|
|
381
|
+
}
|
|
382
|
+
return { status: res.status, contentType, audio: await res.arrayBuffer() };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Generate music via `POST /v1/music`. Provide exactly one of `prompt`
|
|
386
|
+
* or `compositionPlan` (ElevenLabs returns 422 otherwise). Returns the
|
|
387
|
+
* raw audio bytes plus the response content type.
|
|
388
|
+
*
|
|
389
|
+
* @example
|
|
390
|
+
* ```ts
|
|
391
|
+
* const { audio } = await el.composeMusic({
|
|
392
|
+
* prompt: 'Upbeat synthwave with driving bass',
|
|
393
|
+
* musicLengthMs: 30_000,
|
|
394
|
+
* })
|
|
395
|
+
* ```
|
|
396
|
+
*/
|
|
397
|
+
async composeMusic(params) {
|
|
398
|
+
const hasPrompt = typeof params.prompt === "string" && params.prompt.length > 0;
|
|
399
|
+
const hasPlan = params.compositionPlan != null;
|
|
400
|
+
if (hasPrompt === hasPlan) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
"composeMusic: provide exactly one of `prompt` or `compositionPlan`"
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const body = {
|
|
406
|
+
model_id: params.modelId ?? DEFAULT_MUSIC_MODEL
|
|
407
|
+
};
|
|
408
|
+
if (hasPrompt) {
|
|
409
|
+
body.prompt = params.prompt;
|
|
410
|
+
if (params.musicLengthMs !== void 0) body.music_length_ms = params.musicLengthMs;
|
|
411
|
+
if (params.forceInstrumental !== void 0) {
|
|
412
|
+
body.force_instrumental = params.forceInstrumental;
|
|
413
|
+
}
|
|
414
|
+
} else {
|
|
415
|
+
body.composition_plan = params.compositionPlan;
|
|
416
|
+
}
|
|
417
|
+
const { audio, contentType } = await this.request({
|
|
418
|
+
method: "POST",
|
|
419
|
+
path: "/v1/music",
|
|
420
|
+
body,
|
|
421
|
+
accept: "audio/mpeg",
|
|
422
|
+
...params.outputFormat ? { query: { output_format: params.outputFormat } } : {}
|
|
423
|
+
});
|
|
424
|
+
if (!audio) {
|
|
425
|
+
throw new ElevenLabsApiError(
|
|
426
|
+
"composeMusic: expected audio bytes but got a JSON response",
|
|
427
|
+
500,
|
|
428
|
+
""
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return { audio, contentType };
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Convert speech to speech — the "voice changer": keep the performance,
|
|
435
|
+
* swap the voice. `POST /v1/speech-to-speech/{voiceId}` with a multipart
|
|
436
|
+
* audio part. Returns the converted audio bytes.
|
|
437
|
+
*
|
|
438
|
+
* @example
|
|
439
|
+
* ```ts
|
|
440
|
+
* const { audio } = await el.speechToSpeech({
|
|
441
|
+
* voiceId: '21m00Tcm4TlvDq8ikWAM',
|
|
442
|
+
* audio: await fetch(myRecording).then((r) => r.blob()),
|
|
443
|
+
* modelId: 'eleven_multilingual_sts_v2',
|
|
444
|
+
* })
|
|
445
|
+
* ```
|
|
446
|
+
*/
|
|
447
|
+
async speechToSpeech(params) {
|
|
448
|
+
if (!params.voiceId) throw new Error("speechToSpeech: voiceId is required");
|
|
449
|
+
if (!params.audio) throw new Error("speechToSpeech: audio is required");
|
|
450
|
+
const form = new FormData();
|
|
451
|
+
form.set("audio", params.audio, params.fileName ?? "audio");
|
|
452
|
+
form.set("model_id", params.modelId ?? DEFAULT_STS_MODEL);
|
|
453
|
+
if (params.voiceSettings) {
|
|
454
|
+
form.set("voice_settings", JSON.stringify(params.voiceSettings));
|
|
455
|
+
}
|
|
456
|
+
if (params.outputFormat) form.set("output_format", params.outputFormat);
|
|
457
|
+
if (params.fileFormat) form.set("file_format", params.fileFormat);
|
|
458
|
+
if (params.seed !== void 0) form.set("seed", String(params.seed));
|
|
459
|
+
if (params.removeBackgroundNoise !== void 0) {
|
|
460
|
+
form.set("remove_background_noise", String(params.removeBackgroundNoise));
|
|
461
|
+
}
|
|
462
|
+
const { audio, contentType } = await this.request({
|
|
463
|
+
method: "POST",
|
|
464
|
+
path: `/v1/speech-to-speech/${encodeURIComponent(params.voiceId)}`,
|
|
465
|
+
body: form,
|
|
466
|
+
accept: "audio/mpeg"
|
|
467
|
+
});
|
|
468
|
+
if (!audio) {
|
|
469
|
+
throw new ElevenLabsApiError(
|
|
470
|
+
"speechToSpeech: expected audio bytes but got a JSON response",
|
|
471
|
+
500,
|
|
472
|
+
""
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return { audio, contentType };
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Generate a sound effect from a text prompt. `POST /v1/sound-generation`.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const { audio } = await el.soundEffects({ text: 'heavy wooden door creaking' })
|
|
483
|
+
* ```
|
|
484
|
+
*/
|
|
485
|
+
async soundEffects(params) {
|
|
486
|
+
if (!params.text || !params.text.trim()) {
|
|
487
|
+
throw new Error("soundEffects: text is required");
|
|
488
|
+
}
|
|
489
|
+
const body = {
|
|
490
|
+
text: params.text,
|
|
491
|
+
model_id: params.modelId ?? DEFAULT_SOUND_GENERATION_MODEL
|
|
492
|
+
};
|
|
493
|
+
if (params.durationSeconds !== void 0) body.duration_seconds = params.durationSeconds;
|
|
494
|
+
if (params.promptInfluence !== void 0) body.prompt_influence = params.promptInfluence;
|
|
495
|
+
if (params.loop !== void 0) body.loop = params.loop;
|
|
496
|
+
const { audio, contentType } = await this.request({
|
|
497
|
+
method: "POST",
|
|
498
|
+
path: "/v1/sound-generation",
|
|
499
|
+
body,
|
|
500
|
+
accept: "audio/mpeg",
|
|
501
|
+
...params.outputFormat ? { query: { output_format: params.outputFormat } } : {}
|
|
502
|
+
});
|
|
503
|
+
if (!audio) {
|
|
504
|
+
throw new ElevenLabsApiError(
|
|
505
|
+
"soundEffects: expected audio bytes but got a JSON response",
|
|
506
|
+
500,
|
|
507
|
+
""
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return { audio, contentType };
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Transcribe audio with Scribe. `POST /v1/speech-to-text` (multipart).
|
|
514
|
+
*
|
|
515
|
+
* @example
|
|
516
|
+
* ```ts
|
|
517
|
+
* const { text } = await el.transcribe({ file: recording })
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
520
|
+
async transcribe(params) {
|
|
521
|
+
if (!params.file) throw new Error("transcribe: file is required");
|
|
522
|
+
const form = new FormData();
|
|
523
|
+
form.set("file", params.file, params.fileName ?? "audio");
|
|
524
|
+
form.set("model_id", params.modelId ?? DEFAULT_SPEECH_TO_TEXT_MODEL);
|
|
525
|
+
if (params.languageCode) form.set("language_code", params.languageCode);
|
|
526
|
+
if (params.diarize !== void 0) form.set("diarize", String(params.diarize));
|
|
527
|
+
if (params.tagAudioEvents !== void 0) {
|
|
528
|
+
form.set("tag_audio_events", String(params.tagAudioEvents));
|
|
529
|
+
}
|
|
530
|
+
if (params.numSpeakers !== void 0) form.set("num_speakers", String(params.numSpeakers));
|
|
531
|
+
const { json } = await this.request({
|
|
532
|
+
method: "POST",
|
|
533
|
+
path: "/v1/speech-to-text",
|
|
534
|
+
body: form,
|
|
535
|
+
accept: "application/json"
|
|
536
|
+
});
|
|
537
|
+
if (!json || typeof json !== "object") {
|
|
538
|
+
throw new ElevenLabsApiError(
|
|
539
|
+
"transcribe: expected a JSON transcription but got audio",
|
|
540
|
+
500,
|
|
541
|
+
""
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return json;
|
|
545
|
+
}
|
|
546
|
+
/** Probe whether a given voice id still exists on the caller's account. */
|
|
547
|
+
async voiceExists(voiceId) {
|
|
548
|
+
const res = await this.fetchImpl(
|
|
549
|
+
`${this.baseUrl}/v1/voices/${encodeURIComponent(voiceId)}`,
|
|
550
|
+
{ headers: this.headers() }
|
|
551
|
+
);
|
|
552
|
+
return res.ok;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Register a Twilio phone number with an ElevenLabs agent. EL becomes
|
|
556
|
+
* the SIP target for inbound calls to that number — Twilio bridges
|
|
557
|
+
* the call via EL's native integration and we never have to run a
|
|
558
|
+
* Media Streams ↔ convai WebSocket bridge ourselves.
|
|
559
|
+
*
|
|
560
|
+
* Returns the ElevenLabs `phone_number_id` (distinct from the Twilio
|
|
561
|
+
* `sid`) which is the handle used by `outboundCall` + `deletePhoneNumber`.
|
|
562
|
+
*/
|
|
563
|
+
async createPhoneNumberTwilio(params) {
|
|
564
|
+
const body = {
|
|
565
|
+
phone_number: params.phoneNumber,
|
|
566
|
+
label: params.label ?? params.phoneNumber,
|
|
567
|
+
provider: "twilio",
|
|
568
|
+
sid: params.twilioAccountSid,
|
|
569
|
+
token: params.twilioAuthToken
|
|
570
|
+
};
|
|
571
|
+
const res = await this.fetchImpl(
|
|
572
|
+
`${this.baseUrl}/v1/convai/phone-numbers`,
|
|
573
|
+
{
|
|
574
|
+
method: "POST",
|
|
575
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
576
|
+
body: JSON.stringify(body)
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
if (!res.ok) {
|
|
580
|
+
const text = await res.text();
|
|
581
|
+
throw new ElevenLabsApiError(
|
|
582
|
+
`createPhoneNumberTwilio failed: ${res.status}`,
|
|
583
|
+
res.status,
|
|
584
|
+
text
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const data = await res.json();
|
|
588
|
+
const id = data.phone_number_id ?? data.id;
|
|
589
|
+
if (!id) {
|
|
590
|
+
throw new ElevenLabsApiError(
|
|
591
|
+
"createPhoneNumberTwilio returned no id",
|
|
592
|
+
500,
|
|
593
|
+
JSON.stringify(data)
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const patchRes = await this.fetchImpl(
|
|
597
|
+
`${this.baseUrl}/v1/convai/phone-numbers/${encodeURIComponent(id)}`,
|
|
598
|
+
{
|
|
599
|
+
method: "PATCH",
|
|
600
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
601
|
+
body: JSON.stringify({ agent_id: params.agentId })
|
|
602
|
+
}
|
|
603
|
+
);
|
|
604
|
+
if (!patchRes.ok) {
|
|
605
|
+
const text = await patchRes.text();
|
|
606
|
+
try {
|
|
607
|
+
await this.deletePhoneNumber(id);
|
|
608
|
+
} catch {
|
|
609
|
+
}
|
|
610
|
+
throw new ElevenLabsApiError(
|
|
611
|
+
`assignAgentToPhoneNumber failed: ${patchRes.status}`,
|
|
612
|
+
patchRes.status,
|
|
613
|
+
text
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return { phoneNumberId: id };
|
|
617
|
+
}
|
|
618
|
+
/** Detach a previously-registered phone number from ElevenLabs. */
|
|
619
|
+
async deletePhoneNumber(phoneNumberId) {
|
|
620
|
+
const res = await this.fetchImpl(
|
|
621
|
+
`${this.baseUrl}/v1/convai/phone-numbers/${encodeURIComponent(phoneNumberId)}`,
|
|
622
|
+
{ method: "DELETE", headers: this.headers() }
|
|
623
|
+
);
|
|
624
|
+
if (!res.ok && res.status !== 404) {
|
|
625
|
+
const text = await res.text();
|
|
626
|
+
throw new ElevenLabsApiError(
|
|
627
|
+
`deletePhoneNumber failed: ${res.status}`,
|
|
628
|
+
res.status,
|
|
629
|
+
text
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Place an outbound PSTN call via a previously-registered EL phone
|
|
635
|
+
* number. EL drives Twilio (or a SIP trunk) under the hood — we just
|
|
636
|
+
* hand it the destination number and the agent to bridge to. Returns
|
|
637
|
+
* the Twilio `callSid` + EL `conversationId` for metering / tracing.
|
|
638
|
+
*
|
|
639
|
+
* As of 2026-Q2 the EL API uses provider-scoped routes:
|
|
640
|
+
* - Twilio numbers → POST /v1/convai/twilio/outbound-call
|
|
641
|
+
* - SIP numbers → POST /v1/convai/sip-trunk/outbound-call
|
|
642
|
+
* and `agent_phone_number_id` is a body field (no longer a path
|
|
643
|
+
* segment). The previous `/v1/convai/phone-numbers/{id}/outbound-call`
|
|
644
|
+
* route now returns 404. Callers that don't know the provider can
|
|
645
|
+
* look it up from `GET /v1/convai/phone-numbers/{id}` (`provider:
|
|
646
|
+
* "twilio" | "sip_trunk"`); most hosted setups are Twilio, so that's
|
|
647
|
+
* the default here.
|
|
648
|
+
*/
|
|
649
|
+
async outboundCall(params) {
|
|
650
|
+
const provider = params.provider ?? "twilio";
|
|
651
|
+
const body = {
|
|
652
|
+
agent_id: params.agentId,
|
|
653
|
+
agent_phone_number_id: params.phoneNumberId,
|
|
654
|
+
to_number: params.toNumber
|
|
655
|
+
};
|
|
656
|
+
if (params.dynamicVariables) {
|
|
657
|
+
body.conversation_initiation_client_data = {
|
|
658
|
+
dynamic_variables: params.dynamicVariables
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
const res = await this.fetchImpl(
|
|
662
|
+
`${this.baseUrl}/v1/convai/${provider}/outbound-call`,
|
|
663
|
+
{
|
|
664
|
+
method: "POST",
|
|
665
|
+
headers: this.headers({ "content-type": "application/json" }),
|
|
666
|
+
body: JSON.stringify(body)
|
|
667
|
+
}
|
|
668
|
+
);
|
|
669
|
+
if (!res.ok) {
|
|
670
|
+
const text = await res.text();
|
|
671
|
+
throw new ElevenLabsApiError(
|
|
672
|
+
`outboundCall failed: ${res.status}`,
|
|
673
|
+
res.status,
|
|
674
|
+
text
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const data = await res.json();
|
|
678
|
+
const callSid = data.call_sid ?? data.callSid;
|
|
679
|
+
const conversationId = data.conversation_id ?? data.conversationId;
|
|
680
|
+
if (!callSid || !conversationId) {
|
|
681
|
+
throw new ElevenLabsApiError(
|
|
682
|
+
"outboundCall missing callSid or conversationId",
|
|
683
|
+
500,
|
|
684
|
+
JSON.stringify(data)
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return { callSid, conversationId };
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
var MEMORY_CLIENT_TOOLS = [
|
|
691
|
+
{
|
|
692
|
+
type: "client",
|
|
693
|
+
name: "add_memory",
|
|
694
|
+
description: "Save a concise canonical fact about the user (preferences, decisions, personal details, follow-ups). Use whenever the user shares something worth remembering.",
|
|
695
|
+
expects_response: true,
|
|
696
|
+
parameters: {
|
|
697
|
+
type: "object",
|
|
698
|
+
properties: {
|
|
699
|
+
fact: {
|
|
700
|
+
type: "string",
|
|
701
|
+
description: 'A short normalized fact, e.g. "prefers window seats", "favorite color: green". Max 500 chars.'
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
required: ["fact"]
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
];
|
|
708
|
+
|
|
709
|
+
// src/ops.ts
|
|
710
|
+
var MAX_AUDIO_BYTES = 25 * 1024 * 1024;
|
|
711
|
+
var MAX_JSON_BYTES = 8 * 1024;
|
|
712
|
+
var MAX_TEXT_BYTES = 256 * 1024;
|
|
713
|
+
var VOICE_OPS = [
|
|
714
|
+
{
|
|
715
|
+
id: "text-to-speech",
|
|
716
|
+
method: "POST",
|
|
717
|
+
hostedPath: "/voice/op/text-to-speech",
|
|
718
|
+
legacyHostedPath: "/voice/tts",
|
|
719
|
+
upstreamPath: "/v1/text-to-speech/{voiceId}",
|
|
720
|
+
pathParams: ["voiceId"],
|
|
721
|
+
transport: "json-binary",
|
|
722
|
+
call: "textToSpeech",
|
|
723
|
+
permission: "project:read",
|
|
724
|
+
maxBodyBytes: MAX_TEXT_BYTES,
|
|
725
|
+
cost: { unit: "character", per: 1 },
|
|
726
|
+
models: VOICE_TTS_MODEL_IDS,
|
|
727
|
+
defaultModel: CONVAI_TTS_MODEL_FALLBACK,
|
|
728
|
+
defaultVoiceId: true,
|
|
729
|
+
forward: {
|
|
730
|
+
text: "text",
|
|
731
|
+
voiceId: "voice_id",
|
|
732
|
+
modelId: "model_id",
|
|
733
|
+
voiceSettings: "voice_settings"
|
|
734
|
+
},
|
|
735
|
+
required: ["text"],
|
|
736
|
+
responseHeaders: ["x-tts-model-used", "x-tts-model-requested", "warning"],
|
|
737
|
+
accept: "audio/mpeg",
|
|
738
|
+
errorLabel: "TTS failed"
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: "text-to-speech-with-timestamps",
|
|
742
|
+
method: "POST",
|
|
743
|
+
hostedPath: "/voice/op/text-to-speech-with-timestamps",
|
|
744
|
+
upstreamPath: "/v1/text-to-speech/{voiceId}/with-timestamps",
|
|
745
|
+
pathParams: ["voiceId"],
|
|
746
|
+
transport: "json-json",
|
|
747
|
+
permission: "project:read",
|
|
748
|
+
maxBodyBytes: MAX_TEXT_BYTES,
|
|
749
|
+
cost: { unit: "character", per: 1 },
|
|
750
|
+
models: VOICE_TTS_MODEL_IDS,
|
|
751
|
+
defaultModel: CONVAI_TTS_MODEL_FALLBACK,
|
|
752
|
+
defaultVoiceId: true,
|
|
753
|
+
forward: {
|
|
754
|
+
text: "text",
|
|
755
|
+
voiceId: "voice_id",
|
|
756
|
+
modelId: "model_id",
|
|
757
|
+
voiceSettings: "voice_settings",
|
|
758
|
+
outputFormat: "output_format"
|
|
759
|
+
},
|
|
760
|
+
required: ["text"],
|
|
761
|
+
accept: "application/json"
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
id: "speech-to-speech",
|
|
765
|
+
method: "POST",
|
|
766
|
+
hostedPath: "/voice/op/speech-to-speech",
|
|
767
|
+
upstreamPath: "/v1/speech-to-speech/{voiceId}",
|
|
768
|
+
pathParams: ["voiceId"],
|
|
769
|
+
transport: "multipart-binary",
|
|
770
|
+
permission: "project:read",
|
|
771
|
+
maxBodyBytes: MAX_AUDIO_BYTES,
|
|
772
|
+
cost: { unit: "second", per: 1 },
|
|
773
|
+
models: VOICE_STS_MODEL_IDS,
|
|
774
|
+
defaultModel: DEFAULT_STS_MODEL,
|
|
775
|
+
defaultVoiceId: true,
|
|
776
|
+
forward: {
|
|
777
|
+
audio: "audio",
|
|
778
|
+
voiceId: "voice_id",
|
|
779
|
+
modelId: "model_id",
|
|
780
|
+
voiceSettings: "voice_settings",
|
|
781
|
+
outputFormat: "output_format",
|
|
782
|
+
fileFormat: "file_format",
|
|
783
|
+
seed: "seed",
|
|
784
|
+
removeBackgroundNoise: "remove_background_noise"
|
|
785
|
+
},
|
|
786
|
+
required: ["audio"],
|
|
787
|
+
accept: "audio/mpeg"
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
id: "speech-to-text",
|
|
791
|
+
method: "POST",
|
|
792
|
+
hostedPath: "/voice/op/speech-to-text",
|
|
793
|
+
upstreamPath: "/v1/speech-to-text",
|
|
794
|
+
transport: "multipart-json",
|
|
795
|
+
permission: "project:read",
|
|
796
|
+
maxBodyBytes: MAX_AUDIO_BYTES,
|
|
797
|
+
cost: { unit: "second", per: 1 },
|
|
798
|
+
models: VOICE_SPEECH_TO_TEXT_MODEL_IDS,
|
|
799
|
+
defaultModel: DEFAULT_SPEECH_TO_TEXT_MODEL,
|
|
800
|
+
forward: {
|
|
801
|
+
file: "file",
|
|
802
|
+
modelId: "model_id",
|
|
803
|
+
languageCode: "language_code",
|
|
804
|
+
diarize: "diarize",
|
|
805
|
+
tagAudioEvents: "tag_audio_events",
|
|
806
|
+
numSpeakers: "num_speakers"
|
|
807
|
+
},
|
|
808
|
+
required: ["file"],
|
|
809
|
+
accept: "application/json"
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
id: "sound-generation",
|
|
813
|
+
method: "POST",
|
|
814
|
+
hostedPath: "/voice/op/sound-generation",
|
|
815
|
+
upstreamPath: "/v1/sound-generation",
|
|
816
|
+
transport: "json-binary",
|
|
817
|
+
permission: "project:read",
|
|
818
|
+
maxBodyBytes: MAX_JSON_BYTES,
|
|
819
|
+
cost: { unit: "second", per: 1 },
|
|
820
|
+
models: VOICE_SOUND_GENERATION_MODEL_IDS,
|
|
821
|
+
defaultModel: DEFAULT_SOUND_GENERATION_MODEL,
|
|
822
|
+
forward: {
|
|
823
|
+
text: "text",
|
|
824
|
+
durationSeconds: "duration_seconds",
|
|
825
|
+
promptInfluence: "prompt_influence",
|
|
826
|
+
loop: "loop",
|
|
827
|
+
modelId: "model_id"
|
|
828
|
+
},
|
|
829
|
+
// `output_format` is a query param on this endpoint, not a body field — the
|
|
830
|
+
// typed `soundEffects()` client sends it there, and a body field is
|
|
831
|
+
// silently ignored upstream.
|
|
832
|
+
queryForward: ["output_format"],
|
|
833
|
+
required: ["text"],
|
|
834
|
+
accept: "audio/mpeg"
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
id: "music",
|
|
838
|
+
method: "POST",
|
|
839
|
+
hostedPath: "/voice/op/music",
|
|
840
|
+
legacyHostedPath: "/voice/music",
|
|
841
|
+
upstreamPath: "/v1/music",
|
|
842
|
+
transport: "json-binary",
|
|
843
|
+
call: "composeMusic",
|
|
844
|
+
permission: "project:read",
|
|
845
|
+
maxBodyBytes: MAX_JSON_BYTES,
|
|
846
|
+
cost: { unit: "second", per: 1 },
|
|
847
|
+
models: [DEFAULT_MUSIC_MODEL],
|
|
848
|
+
defaultModel: DEFAULT_MUSIC_MODEL,
|
|
849
|
+
forward: {
|
|
850
|
+
prompt: "prompt",
|
|
851
|
+
compositionPlan: "composition_plan",
|
|
852
|
+
musicLengthMs: "music_length_ms",
|
|
853
|
+
modelId: "model_id",
|
|
854
|
+
forceInstrumental: "force_instrumental",
|
|
855
|
+
outputFormat: "output_format"
|
|
856
|
+
},
|
|
857
|
+
accept: "audio/mpeg",
|
|
858
|
+
errorLabel: "Music generation failed",
|
|
859
|
+
validate: (fields) => {
|
|
860
|
+
const hasPrompt = typeof fields.prompt === "string" && fields.prompt.trim().length > 0;
|
|
861
|
+
const hasPlan = fields.compositionPlan != null && typeof fields.compositionPlan === "object";
|
|
862
|
+
return hasPrompt === hasPlan ? "Provide exactly one of `prompt` or `compositionPlan`" : null;
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
id: "voices",
|
|
867
|
+
method: "GET",
|
|
868
|
+
hostedPath: "/voice/op/voices",
|
|
869
|
+
upstreamPath: "/v1/voices",
|
|
870
|
+
transport: "json-json",
|
|
871
|
+
permission: "project:read",
|
|
872
|
+
maxBodyBytes: 0,
|
|
873
|
+
cost: { unit: "request", per: 1 },
|
|
874
|
+
queryForward: ["page_size", "search", "voice_type", "category", "next_page_token"],
|
|
875
|
+
accept: "application/json"
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
id: "models",
|
|
879
|
+
method: "GET",
|
|
880
|
+
hostedPath: "/voice/op/models",
|
|
881
|
+
upstreamPath: "/v1/models",
|
|
882
|
+
transport: "json-json",
|
|
883
|
+
permission: "project:read",
|
|
884
|
+
maxBodyBytes: 0,
|
|
885
|
+
cost: { unit: "request", per: 1 },
|
|
886
|
+
accept: "application/json"
|
|
887
|
+
}
|
|
888
|
+
];
|
|
889
|
+
var VOICE_OP_IDS = VOICE_OPS.map((op) => op.id);
|
|
890
|
+
function getVoiceOp(id) {
|
|
891
|
+
if (!id) return void 0;
|
|
892
|
+
return VOICE_OPS.find((op) => op.id === id);
|
|
893
|
+
}
|
|
894
|
+
function voiceOpForwardPath(op) {
|
|
895
|
+
return op.legacyHostedPath ?? op.hostedPath;
|
|
896
|
+
}
|
|
897
|
+
function voiceOpIsMultipart(op) {
|
|
898
|
+
return op.transport === "multipart-binary" || op.transport === "multipart-json";
|
|
899
|
+
}
|
|
900
|
+
function voiceOpReturnsAudio(op) {
|
|
901
|
+
return op.transport === "json-binary" || op.transport === "multipart-binary";
|
|
902
|
+
}
|
|
903
|
+
function voiceOpUpstreamPath(op, params) {
|
|
904
|
+
let path = op.upstreamPath;
|
|
905
|
+
for (const name of op.pathParams ?? []) {
|
|
906
|
+
const value = params[name];
|
|
907
|
+
if (!value) {
|
|
908
|
+
return { error: `Missing \`${name}\` (required path parameter for ${op.id})` };
|
|
909
|
+
}
|
|
910
|
+
path = path.split(`{${name}}`).join(encodeURIComponent(value));
|
|
911
|
+
}
|
|
912
|
+
return path;
|
|
913
|
+
}
|
|
914
|
+
function voiceOpCredentialError(op, credential) {
|
|
915
|
+
if (op.byoOnly && credential === "pooled") {
|
|
916
|
+
return {
|
|
917
|
+
status: 403,
|
|
918
|
+
error: "byo_key_required",
|
|
919
|
+
detail: `${op.id} runs on your own ElevenLabs key, not Shogo's pooled key \u2014 pass it to ElevenLabsClient and call request() directly`
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
var MODEL_FIELD = "modelId";
|
|
925
|
+
function parseVoiceOpJsonBody(op, raw) {
|
|
926
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
927
|
+
return { ok: false, status: 400, error: "Invalid JSON body" };
|
|
928
|
+
}
|
|
929
|
+
const body = raw;
|
|
930
|
+
const fields = {};
|
|
931
|
+
for (const [callerField, upstreamField] of Object.entries(op.forward ?? {})) {
|
|
932
|
+
if (upstreamField && body[callerField] !== void 0) fields[callerField] = body[callerField];
|
|
933
|
+
}
|
|
934
|
+
for (const name of op.required ?? []) {
|
|
935
|
+
const value = fields[name];
|
|
936
|
+
if (value === void 0 || value === null || typeof value === "string" && !value.trim()) {
|
|
937
|
+
return { ok: false, status: 400, error: `Missing \`${name}\`` };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const modelCheck = checkVoiceOpModel(op, fields);
|
|
941
|
+
if ("error" in modelCheck) return { ok: false, status: 400, ...modelCheck };
|
|
942
|
+
if (op.validate) {
|
|
943
|
+
const message = op.validate(fields);
|
|
944
|
+
if (message) return { ok: false, status: 400, error: message };
|
|
945
|
+
}
|
|
946
|
+
return { ok: true, fields };
|
|
947
|
+
}
|
|
948
|
+
function parseVoiceOpForm(op, form) {
|
|
949
|
+
const fields = {};
|
|
950
|
+
for (const callerField of Object.keys(op.forward ?? {})) {
|
|
951
|
+
const value = form.get(callerField);
|
|
952
|
+
if (value !== null) fields[callerField] = value;
|
|
953
|
+
}
|
|
954
|
+
for (const name of op.required ?? []) {
|
|
955
|
+
const value = fields[name];
|
|
956
|
+
if (!(value instanceof Blob)) {
|
|
957
|
+
return {
|
|
958
|
+
ok: false,
|
|
959
|
+
status: 400,
|
|
960
|
+
error: `Missing \`${name}\` file part`,
|
|
961
|
+
detail: value === void 0 ? `send a multipart body with a \`${name}\` file part` : `\`${name}\` must be a file part, not a text field`
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
if (value.size === 0) {
|
|
965
|
+
return { ok: false, status: 400, error: `Empty \`${name}\` file part` };
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const modelCheck = checkVoiceOpModel(op, fields);
|
|
969
|
+
if ("error" in modelCheck) return { ok: false, status: 400, ...modelCheck };
|
|
970
|
+
const forward = op.forward ?? {};
|
|
971
|
+
const pathParams = new Set(op.pathParams ?? []);
|
|
972
|
+
const out = new FormData();
|
|
973
|
+
for (const [callerField, upstreamField] of Object.entries(forward)) {
|
|
974
|
+
if (pathParams.has(callerField)) continue;
|
|
975
|
+
const value = fields[callerField];
|
|
976
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
977
|
+
if (value instanceof Blob) {
|
|
978
|
+
const name = value instanceof File ? value.name : callerField;
|
|
979
|
+
out.set(upstreamField, value, name);
|
|
980
|
+
} else if (typeof value === "string") {
|
|
981
|
+
out.set(upstreamField, value);
|
|
982
|
+
} else {
|
|
983
|
+
out.set(upstreamField, JSON.stringify(value));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const modelField = forward[MODEL_FIELD];
|
|
987
|
+
if (op.defaultModel && modelField && out.get(modelField) === null) {
|
|
988
|
+
out.set(modelField, op.defaultModel);
|
|
989
|
+
}
|
|
990
|
+
return { ok: true, form: out, fields };
|
|
991
|
+
}
|
|
992
|
+
function voiceOpUpstreamBody(op, fields) {
|
|
993
|
+
const pathParams = new Set(op.pathParams ?? []);
|
|
994
|
+
const body = {};
|
|
995
|
+
for (const [callerField, upstreamField] of Object.entries(op.forward ?? {})) {
|
|
996
|
+
if (pathParams.has(callerField)) continue;
|
|
997
|
+
const value = fields[callerField];
|
|
998
|
+
if (value === void 0 || value === null) continue;
|
|
999
|
+
body[upstreamField] = value;
|
|
1000
|
+
}
|
|
1001
|
+
return body;
|
|
1002
|
+
}
|
|
1003
|
+
function voiceOpPathParams(op, fields, query) {
|
|
1004
|
+
const out = {};
|
|
1005
|
+
for (const name of op.pathParams ?? []) {
|
|
1006
|
+
const fromBody = fields[name];
|
|
1007
|
+
const value = typeof fromBody === "string" && fromBody.length > 0 ? fromBody : query.get(name);
|
|
1008
|
+
if (value) out[name] = value;
|
|
1009
|
+
}
|
|
1010
|
+
return out;
|
|
1011
|
+
}
|
|
1012
|
+
function checkVoiceOpModel(op, fields) {
|
|
1013
|
+
const requested = fields[MODEL_FIELD];
|
|
1014
|
+
if (requested === void 0 || requested === null || requested === "") return { ok: true };
|
|
1015
|
+
if (typeof requested !== "string") {
|
|
1016
|
+
return { error: `\`${MODEL_FIELD}\` must be a string` };
|
|
1017
|
+
}
|
|
1018
|
+
if (op.models && !op.models.includes(requested)) {
|
|
1019
|
+
return {
|
|
1020
|
+
error: "unknown_model",
|
|
1021
|
+
detail: `Unknown model '${requested}' for ${op.id}`,
|
|
1022
|
+
allowed: op.models
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
return { ok: true };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// src/runtime-token.ts
|
|
1029
|
+
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.";
|
|
1030
|
+
function readRuntimeAuthSecret(env = typeof process === "undefined" ? void 0 : process.env) {
|
|
1031
|
+
const token = env?.RUNTIME_AUTH_SECRET;
|
|
1032
|
+
if (!token) return void 0;
|
|
1033
|
+
if (token.startsWith("wrt_v1_")) throw new Error(WORKSPACE_RUNTIME_TOKEN_MESSAGE);
|
|
1034
|
+
return token;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export {
|
|
1038
|
+
DEFAULT_MEMORY_BLOCK,
|
|
1039
|
+
stripMemoryBlock,
|
|
1040
|
+
extractBasePrompt,
|
|
1041
|
+
composeAgentPrompt,
|
|
1042
|
+
DEFAULT_ELEVENLABS_BASE_URL,
|
|
1043
|
+
VOICE_TTS_MODELS,
|
|
1044
|
+
VOICE_TTS_MODEL_IDS,
|
|
1045
|
+
DEFAULT_TTS_MODEL,
|
|
1046
|
+
CONVAI_SUPPORTED_TTS_MODELS,
|
|
1047
|
+
CONVAI_TTS_MODEL_FALLBACK,
|
|
1048
|
+
TTS_FALLBACK_MODEL,
|
|
1049
|
+
getVoiceTtsModel,
|
|
1050
|
+
resolveConvaiTtsModelDetailed,
|
|
1051
|
+
resolveConvaiTtsModel,
|
|
1052
|
+
isModelRelatedTtsFailure,
|
|
1053
|
+
DEFAULT_MUSIC_MODEL,
|
|
1054
|
+
VOICE_STS_MODEL_IDS,
|
|
1055
|
+
DEFAULT_STS_MODEL,
|
|
1056
|
+
VOICE_SPEECH_TO_TEXT_MODEL_IDS,
|
|
1057
|
+
DEFAULT_SPEECH_TO_TEXT_MODEL,
|
|
1058
|
+
VOICE_SOUND_GENERATION_MODEL_IDS,
|
|
1059
|
+
DEFAULT_SOUND_GENERATION_MODEL,
|
|
1060
|
+
ElevenLabsApiError,
|
|
1061
|
+
ElevenLabsClient,
|
|
1062
|
+
MEMORY_CLIENT_TOOLS,
|
|
1063
|
+
VOICE_OPS,
|
|
1064
|
+
VOICE_OP_IDS,
|
|
1065
|
+
getVoiceOp,
|
|
1066
|
+
voiceOpForwardPath,
|
|
1067
|
+
voiceOpIsMultipart,
|
|
1068
|
+
voiceOpReturnsAudio,
|
|
1069
|
+
voiceOpUpstreamPath,
|
|
1070
|
+
voiceOpCredentialError,
|
|
1071
|
+
parseVoiceOpJsonBody,
|
|
1072
|
+
parseVoiceOpForm,
|
|
1073
|
+
voiceOpUpstreamBody,
|
|
1074
|
+
voiceOpPathParams,
|
|
1075
|
+
WORKSPACE_RUNTIME_TOKEN_MESSAGE,
|
|
1076
|
+
readRuntimeAuthSecret
|
|
1077
|
+
};
|
|
1078
|
+
//# sourceMappingURL=chunk-IOFWML5L.js.map
|