@ai-ecoverse/pi-audio 1.12.0 → 1.13.0

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 CHANGED
@@ -23,6 +23,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
23
23
 
24
24
  `@ai-ecoverse/pi-audio/durable` appends a `voice.truncate` entry for a `VoiceCut`. The entry has no model messages. Its `edits` replace the target assistant message with the text heard so far and keep tool calls. The stored entry stays intact, and pi-durable derives the shorter message for the next model request. `@earendil-works/pi-durable` is an optional peer (`^1.1.0`); the root package does not import it. `VoiceCut.entryId` is the decimal string of pi-durable's numeric entry id.
25
25
 
26
+ `@ai-ecoverse/pi-audio/elevenlabs` registers ElevenLabs speech adapters when `registerElevenLabs()` is called. `scribe_v2` posts multipart audio to `/speech-to-text`. `scribe_v2_realtime` streams base64 audio on `wss://…/speech-to-text/realtime` and commits turns with `turn_prediction` (`turnDetection: 'semantic'`). `eleven_flash_v2_5` and `eleven_multilingual_v2` post JSON to `/text-to-speech/{voice}/with-timestamps` for a string and stream text on `stream-input` for an async iterable, with character alignment. `eleven_v3` and `eleven_v4` stay on the HTTP path (`textStreaming: false`). Scribe prices are $0.22 per hour for batch and $0.39 per hour for realtime.
27
+
26
28
  `@ai-ecoverse/pi-audio/xai` registers xAI speech adapters when `registerXAI()` is called. `grok-voice-transcribe-2.0` posts multipart audio to `/stt` (fields before `file`) and streams raw binary frames on `wss://…/stt` after `transcript.created`, with Smart Turn (`turnDetection: 'semantic'`). The catalog id is shared, so `audioCost` uses the streaming rate of $0.20 per hour. `grok-voice-tts-1.0` posts JSON to `/tts` for a string and streams `text.delta` / `audio.delta` on `wss://…/tts` for an async iterable (`textStreaming: true`). Voices are eve, ara, rex, sal, and leo. Codecs are mp3, pcm, wav, mulaw, and alaw.
27
29
 
28
30
  `@ai-ecoverse/pi-audio/openai` registers OpenAI and OpenRouter speech adapters when `registerOpenAI()` is called. Batch transcription posts multipart audio to `/audio/transcriptions`. OpenRouter (`provider` `openrouter`, or a base URL on `openrouter.ai`) posts JSON `input_audio` instead. Speech posts JSON to `/audio/speech` and reads the raw audio response. OpenAI can return pcm, mp3, wav, opus, or flac. OpenRouter returns pcm or mp3. Realtime transcription uses a WebSocket at `/realtime?intent=transcription` for `gpt-realtime-whisper` and `gpt-live-transcribe`. The catalog key is type, provider, and id, so the batch model keeps the id `gpt-4o-transcribe`.
@@ -0,0 +1,136 @@
1
+ import {
2
+ registerVoiceModel
3
+ } from "../models.js";
4
+ import { registerSpeechApiProvider, registerTranscriptionApiProvider } from "../registry.js";
5
+ import { listenElevenLabs } from "./listen.js";
6
+ import { speakElevenLabs } from "./speak.js";
7
+ import { transcribeElevenLabs } from "./transcribe.js";
8
+ const tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
9
+ const eleven = "https://api.elevenlabs.io/v1";
10
+ const pcm16 = (sampleRate) => ({
11
+ encoding: "pcm16",
12
+ sampleRate,
13
+ channels: 1
14
+ });
15
+ const law = (encoding) => ({
16
+ encoding,
17
+ sampleRate: 8e3,
18
+ channels: 1
19
+ });
20
+ const container = (encoding, sampleRate) => ({
21
+ encoding,
22
+ sampleRate,
23
+ channels: 1
24
+ });
25
+ const voices = [
26
+ { id: "21m00Tcm4TlvDq8ikWAM", name: "Rachel" },
27
+ { id: "pNInz6obpgDQGcFmaJgB", name: "Adam" },
28
+ { id: "EXAVITQu4vr4xnSDxMaL", name: "Bella" },
29
+ { id: "ErXwobaYiN019PkySvjV", name: "Antoni" },
30
+ { id: "TxGEqnHWrfWFTfGW9XjX", name: "Josh" }
31
+ ];
32
+ const speechFormats = [
33
+ container("mp3", 44100),
34
+ container("mp3", 24e3),
35
+ container("mp3", 22050),
36
+ pcm16(16e3),
37
+ pcm16(24e3),
38
+ pcm16(8e3),
39
+ container("wav", 16e3),
40
+ law("mulaw"),
41
+ law("alaw"),
42
+ container("opus", 48e3)
43
+ ];
44
+ function speech(id, textStreaming, perThousandChars) {
45
+ return {
46
+ id,
47
+ name: id,
48
+ api: "elevenlabs-tts",
49
+ provider: "elevenlabs",
50
+ baseUrl: eleven,
51
+ type: "speech",
52
+ textStreaming,
53
+ outputFormats: speechFormats,
54
+ voices,
55
+ cloning: true,
56
+ cost: tokenCost,
57
+ audioCost: { perThousandChars }
58
+ };
59
+ }
60
+ function elevenCatalog() {
61
+ const batch = {
62
+ id: "scribe_v2",
63
+ name: "scribe_v2",
64
+ api: "elevenlabs-scribe",
65
+ provider: "elevenlabs",
66
+ baseUrl: eleven,
67
+ type: "transcription",
68
+ streaming: false,
69
+ turnDetection: "none",
70
+ inputFormats: [
71
+ container("wav", 16e3),
72
+ container("mp3", 16e3),
73
+ container("flac", 16e3),
74
+ pcm16(16e3),
75
+ law("mulaw")
76
+ ],
77
+ diarization: true,
78
+ cost: tokenCost,
79
+ audioCost: { perInputMinute: 0.22 / 60 }
80
+ };
81
+ const realtime = {
82
+ id: "scribe_v2_realtime",
83
+ name: "scribe_v2_realtime",
84
+ api: "elevenlabs-scribe",
85
+ provider: "elevenlabs",
86
+ baseUrl: eleven,
87
+ type: "transcription",
88
+ streaming: true,
89
+ turnDetection: "semantic",
90
+ inputFormats: [
91
+ pcm16(8e3),
92
+ pcm16(16e3),
93
+ pcm16(22050),
94
+ pcm16(24e3),
95
+ pcm16(44100),
96
+ pcm16(48e3),
97
+ law("mulaw")
98
+ ],
99
+ diarization: true,
100
+ cost: tokenCost,
101
+ audioCost: { perInputMinute: 0.39 / 60 }
102
+ };
103
+ return [
104
+ batch,
105
+ realtime,
106
+ speech("eleven_flash_v2_5", true, 0.05),
107
+ speech("eleven_multilingual_v2", true, 0.1),
108
+ speech("eleven_v3", false, 0.1),
109
+ speech("eleven_v4", false, 0.1)
110
+ ];
111
+ }
112
+ function registerElevenLabs() {
113
+ const models = elevenCatalog();
114
+ const unregisterModels = models.map((model) => registerVoiceModel(model));
115
+ const unregisterScribe = registerTranscriptionApiProvider({
116
+ api: "elevenlabs-scribe",
117
+ transcribe: transcribeElevenLabs,
118
+ listen: listenElevenLabs
119
+ });
120
+ const unregisterSpeech = registerSpeechApiProvider({
121
+ api: "elevenlabs-tts",
122
+ speak: speakElevenLabs
123
+ });
124
+ let released = false;
125
+ return () => {
126
+ if (released) return;
127
+ released = true;
128
+ for (const unregister of unregisterModels) unregister();
129
+ unregisterScribe();
130
+ unregisterSpeech();
131
+ };
132
+ }
133
+ export {
134
+ registerElevenLabs
135
+ };
136
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs/catalog.ts"],
4
+ "sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport {\n registerVoiceModel,\n type SpeechModel,\n type TranscriptionModel,\n type VoiceInfo,\n} from '../models.ts';\nimport { registerSpeechApiProvider, registerTranscriptionApiProvider } from '../registry.ts';\nimport { listenElevenLabs } from './listen.ts';\nimport { speakElevenLabs } from './speak.ts';\nimport { transcribeElevenLabs } from './transcribe.ts';\n\nconst tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\nconst eleven = 'https://api.elevenlabs.io/v1';\n\nconst pcm16 = (sampleRate: number): AudioFormat => ({\n encoding: 'pcm16',\n sampleRate,\n channels: 1,\n});\n\nconst law = (encoding: 'mulaw' | 'alaw'): AudioFormat => ({\n encoding,\n sampleRate: 8000,\n channels: 1,\n});\n\nconst container = (encoding: 'wav' | 'mp3' | 'flac' | 'opus', sampleRate: number): AudioFormat => ({\n encoding,\n sampleRate,\n channels: 1,\n});\n\nconst voices: VoiceInfo[] = [\n { id: '21m00Tcm4TlvDq8ikWAM', name: 'Rachel' },\n { id: 'pNInz6obpgDQGcFmaJgB', name: 'Adam' },\n { id: 'EXAVITQu4vr4xnSDxMaL', name: 'Bella' },\n { id: 'ErXwobaYiN019PkySvjV', name: 'Antoni' },\n { id: 'TxGEqnHWrfWFTfGW9XjX', name: 'Josh' },\n];\n\nconst speechFormats: AudioFormat[] = [\n container('mp3', 44100),\n container('mp3', 24000),\n container('mp3', 22050),\n pcm16(16000),\n pcm16(24000),\n pcm16(8000),\n container('wav', 16000),\n law('mulaw'),\n law('alaw'),\n container('opus', 48000),\n];\n\nfunction speech(id: string, textStreaming: boolean, perThousandChars: number): SpeechModel<string> {\n return {\n id,\n name: id,\n api: 'elevenlabs-tts',\n provider: 'elevenlabs',\n baseUrl: eleven,\n type: 'speech',\n textStreaming,\n outputFormats: speechFormats,\n voices,\n cloning: true,\n cost: tokenCost,\n audioCost: { perThousandChars },\n };\n}\n\nfunction elevenCatalog(): Array<TranscriptionModel<string> | SpeechModel<string>> {\n const batch: TranscriptionModel<string> = {\n id: 'scribe_v2',\n name: 'scribe_v2',\n api: 'elevenlabs-scribe',\n provider: 'elevenlabs',\n baseUrl: eleven,\n type: 'transcription',\n streaming: false,\n turnDetection: 'none',\n inputFormats: [\n container('wav', 16000),\n container('mp3', 16000),\n container('flac', 16000),\n pcm16(16000),\n law('mulaw'),\n ],\n diarization: true,\n cost: tokenCost,\n audioCost: { perInputMinute: 0.22 / 60 },\n };\n const realtime: TranscriptionModel<string> = {\n id: 'scribe_v2_realtime',\n name: 'scribe_v2_realtime',\n api: 'elevenlabs-scribe',\n provider: 'elevenlabs',\n baseUrl: eleven,\n type: 'transcription',\n streaming: true,\n turnDetection: 'semantic',\n inputFormats: [\n pcm16(8000),\n pcm16(16000),\n pcm16(22050),\n pcm16(24000),\n pcm16(44100),\n pcm16(48000),\n law('mulaw'),\n ],\n diarization: true,\n cost: tokenCost,\n audioCost: { perInputMinute: 0.39 / 60 },\n };\n return [\n batch,\n realtime,\n speech('eleven_flash_v2_5', true, 0.05),\n speech('eleven_multilingual_v2', true, 0.1),\n speech('eleven_v3', false, 0.1),\n speech('eleven_v4', false, 0.1),\n ];\n}\n\nexport function registerElevenLabs(): () => void {\n const models = elevenCatalog();\n const unregisterModels = models.map((model) => registerVoiceModel(model));\n const unregisterScribe = registerTranscriptionApiProvider({\n api: 'elevenlabs-scribe',\n transcribe: transcribeElevenLabs,\n listen: listenElevenLabs,\n });\n const unregisterSpeech = registerSpeechApiProvider({\n api: 'elevenlabs-tts',\n speak: speakElevenLabs,\n });\n let released = false;\n return () => {\n if (released) return;\n released = true;\n for (const unregister of unregisterModels) unregister();\n unregisterScribe();\n unregisterSpeech();\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ import { registerElevenLabs } from "./catalog.js";
2
+ export {
3
+ registerElevenLabs
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs/index.ts"],
4
+ "sourcesContent": ["export { registerElevenLabs } from './catalog.ts';\n"],
5
+ "mappings": "AAAA,SAAS,0BAA0B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,268 @@
1
+ import { chunkSeconds } from "../audio.js";
2
+ import { encodeAudioBase64 } from "../convert.js";
3
+ import { asVoiceError, isRecord, numberField, stringField } from "../openai/wire.js";
4
+ import {
5
+ TranscriptionEventStream
6
+ } from "../transcription.js";
7
+ import { priceAudio } from "../usage.js";
8
+ import {
9
+ awaitOpen,
10
+ connectSocket,
11
+ messageText,
12
+ sameFormat,
13
+ socketUrl,
14
+ sttAudioFormat,
15
+ xiHeaders
16
+ } from "./wire.js";
17
+ const failures = /* @__PURE__ */ new Set([
18
+ "error",
19
+ "auth_error",
20
+ "quota_exceeded",
21
+ "commit_throttled",
22
+ "unaccepted_terms",
23
+ "rate_limited",
24
+ "queue_overflow",
25
+ "resource_exhausted",
26
+ "session_time_limit_exceeded",
27
+ "input_error",
28
+ "invalid_request",
29
+ "chunk_size_exceeded",
30
+ "insufficient_audio_activity",
31
+ "transcriber_error"
32
+ ]);
33
+ function terms(options) {
34
+ const vocabulary = options?.vocabulary;
35
+ if (!vocabulary) return [];
36
+ return vocabulary;
37
+ }
38
+ function commitStrategy(model) {
39
+ if (model.turnDetection === "semantic") return "turn_prediction";
40
+ if (model.turnDetection === "vad") return "vad";
41
+ return "manual";
42
+ }
43
+ function sttUrl(model, format, options) {
44
+ const url = socketUrl(model.baseUrl, "speech-to-text/realtime");
45
+ const params = new URLSearchParams();
46
+ params.set("model_id", model.id);
47
+ params.set("audio_format", sttAudioFormat(format));
48
+ params.set("commit_strategy", commitStrategy(model));
49
+ params.set("include_timestamps", "true");
50
+ if (options?.language) params.set("language_code", options.language);
51
+ for (const term of terms(options)) params.append("keyterms", term);
52
+ url.search = params.toString();
53
+ return url.toString();
54
+ }
55
+ function chosenFormat(model, chunk) {
56
+ const format = chunk === void 0 ? model.inputFormats[0] : chunk.format;
57
+ if (!format) throw new Error("No input format");
58
+ sttAudioFormat(format);
59
+ const known = model.inputFormats.some((item) => sameFormat(item, format));
60
+ if (!known) throw new Error(`Unsupported audio format ${format.encoding}`);
61
+ return format;
62
+ }
63
+ function textOr(event) {
64
+ const text = stringField(event, "text");
65
+ if (text === void 0) return "";
66
+ return text;
67
+ }
68
+ function secondsOf(word, key) {
69
+ const value = numberField(word, key);
70
+ if (value === void 0) return 0;
71
+ return value;
72
+ }
73
+ function wordFrom(word) {
74
+ if (stringField(word, "type") === "spacing") return void 0;
75
+ const speaker = stringField(word, "speaker_id");
76
+ const segment = {
77
+ text: textOr(word),
78
+ start: secondsOf(word, "start"),
79
+ end: secondsOf(word, "end")
80
+ };
81
+ if (speaker !== void 0) segment.speaker = speaker;
82
+ return segment;
83
+ }
84
+ function wordsOf(event, state) {
85
+ if (!Array.isArray(event.words)) return;
86
+ for (const word of event.words) {
87
+ if (!isRecord(word)) continue;
88
+ const segment = wordFrom(word);
89
+ if (segment) state.segments.push(segment);
90
+ }
91
+ }
92
+ function emitCommit(spoken, state, push) {
93
+ if (!state.started) {
94
+ state.started = true;
95
+ push({ type: "speech_start", at: 0 });
96
+ }
97
+ const segmentId = String(state.segment);
98
+ state.segment += 1;
99
+ state.turns.push(spoken);
100
+ push({ type: "final", segmentId, text: spoken });
101
+ state.started = false;
102
+ push({ type: "turn_end", text: spoken, at: 0 });
103
+ }
104
+ function applyListen(event, state, push) {
105
+ const type = stringField(event, "message_type");
106
+ if (type === "session_started") {
107
+ state.ready = true;
108
+ return;
109
+ }
110
+ if (type === "partial_transcript") {
111
+ if (!state.started) {
112
+ state.started = true;
113
+ push({ type: "speech_start", at: 0 });
114
+ }
115
+ push({ type: "partial", segmentId: String(state.segment), text: textOr(event) });
116
+ return;
117
+ }
118
+ if (type === "committed_transcript") {
119
+ const spoken = textOr(event);
120
+ state.lastCommit = spoken;
121
+ emitCommit(spoken, state, push);
122
+ return;
123
+ }
124
+ if (type === "committed_transcript_with_timestamps") {
125
+ const spoken = textOr(event);
126
+ wordsOf(event, state);
127
+ if (state.lastCommit !== spoken) emitCommit(spoken, state, push);
128
+ state.lastCommit = void 0;
129
+ return;
130
+ }
131
+ if (type !== void 0 && failures.has(type)) {
132
+ const message = stringField(event, "error");
133
+ state.failed = true;
134
+ push({
135
+ type: "error",
136
+ reason: "error",
137
+ error: message === void 0 ? "ElevenLabs transcription failed" : message
138
+ });
139
+ }
140
+ }
141
+ function finish(model, format, state, push) {
142
+ if (state.failed || state.completed) return;
143
+ state.completed = true;
144
+ const heard = chunkSeconds({ data: new Uint8Array(state.bytes), format, timestamp: 0 });
145
+ push({
146
+ type: "done",
147
+ transcript: {
148
+ text: state.turns.join(" "),
149
+ ...state.segments.length > 0 ? { segments: state.segments } : {},
150
+ usage: priceAudio(model.audioCost, { inputSeconds: heard })
151
+ }
152
+ });
153
+ }
154
+ function listenElevenLabs(model, audio, options) {
155
+ const stream = new TranscriptionEventStream();
156
+ void pumpListen(model, audio, options, stream);
157
+ return stream;
158
+ }
159
+ async function pumpListen(model, audio, options, stream) {
160
+ const state = {
161
+ turns: [],
162
+ segments: [],
163
+ segment: 0,
164
+ started: false,
165
+ bytes: 0,
166
+ opened: false,
167
+ ready: false,
168
+ clientDone: false,
169
+ failed: false,
170
+ completed: false,
171
+ lastCommit: void 0
172
+ };
173
+ let settled = false;
174
+ const push = (event) => {
175
+ if (settled) return;
176
+ if (event.type === "done" || event.type === "error") settled = true;
177
+ stream.push(event);
178
+ };
179
+ let socket;
180
+ const readyWait = Promise.withResolvers();
181
+ const doneWait = Promise.withResolvers();
182
+ const wake = (kind) => {
183
+ if (state.ready || state.failed) readyWait.resolve();
184
+ if (state.failed) doneWait.resolve();
185
+ if (!state.clientDone) return;
186
+ if (kind === "committed_transcript" || kind === "committed_transcript_with_timestamps") {
187
+ doneWait.resolve();
188
+ }
189
+ if (kind === "close") doneWait.resolve();
190
+ };
191
+ try {
192
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
193
+ const iterator = audio[Symbol.asyncIterator]();
194
+ const first = await iterator.next();
195
+ const format = chosenFormat(model, first.done ? void 0 : first.value);
196
+ socket = connectSocket(sttUrl(model, format, options), xiHeaders(model, options));
197
+ let incoming = Promise.resolve();
198
+ socket.addEventListener("message", (event) => {
199
+ incoming = incoming.then(() => onMessage(event.data, state, push, wake));
200
+ });
201
+ socket.addEventListener("close", () => {
202
+ if (state.opened && !state.failed && !state.completed && !state.clientDone) {
203
+ state.failed = true;
204
+ push({ type: "error", reason: "error", error: "ElevenLabs transcription socket closed" });
205
+ }
206
+ wake("close");
207
+ });
208
+ await awaitOpen(socket, "ElevenLabs transcription", (message) => {
209
+ state.failed = true;
210
+ push({ type: "error", reason: "error", error: message });
211
+ wake("fail");
212
+ });
213
+ if (state.failed) return;
214
+ state.opened = true;
215
+ if (!state.ready) await readyWait.promise;
216
+ if (state.failed) return;
217
+ if (!first.done) sendChunk(socket, first.value, format, state, false);
218
+ while (!state.failed) {
219
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
220
+ const next = await iterator.next();
221
+ if (next.done) break;
222
+ sendChunk(socket, next.value, format, state, false);
223
+ }
224
+ if (state.failed) return;
225
+ sendChunk(socket, void 0, format, state, true);
226
+ state.clientDone = true;
227
+ await doneWait.promise;
228
+ finish(model, format, state, push);
229
+ } catch (error) {
230
+ const voice = asVoiceError(error);
231
+ state.failed = true;
232
+ push({ type: "error", reason: voice.reason, error: voice.error });
233
+ } finally {
234
+ socket?.close();
235
+ }
236
+ }
237
+ function sendChunk(socket, chunk, format, state, commit) {
238
+ if (chunk && !sameFormat(chunk.format, format)) {
239
+ throw new Error(`Unsupported audio format ${chunk.format.encoding}`);
240
+ }
241
+ const data = chunk ? chunk.data : new Uint8Array();
242
+ state.bytes += data.byteLength;
243
+ socket.send(
244
+ JSON.stringify({
245
+ message_type: "input_audio_chunk",
246
+ audio_base_64: encodeAudioBase64(data),
247
+ commit,
248
+ sample_rate: format.sampleRate
249
+ })
250
+ );
251
+ }
252
+ async function onMessage(data, state, push, wake) {
253
+ try {
254
+ const parsed = JSON.parse(await messageText(data));
255
+ if (!isRecord(parsed)) throw new Error("ElevenLabs transcription event was not JSON");
256
+ applyListen(parsed, state, push);
257
+ wake(stringField(parsed, "message_type"));
258
+ } catch (error) {
259
+ const voice = asVoiceError(error);
260
+ state.failed = true;
261
+ push({ type: "error", reason: voice.reason, error: voice.error });
262
+ wake("fail");
263
+ }
264
+ }
265
+ export {
266
+ listenElevenLabs
267
+ };
268
+ //# sourceMappingURL=listen.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs/listen.ts"],
4
+ "sourcesContent": ["import type { AudioChunk, AudioFormat } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport { encodeAudioBase64 } from '../convert.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport { asVoiceError, isRecord, numberField, stringField } from '../openai/wire.ts';\nimport {\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n type TranscriptSegment,\n} from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n awaitOpen,\n connectSocket,\n messageText,\n sameFormat,\n socketUrl,\n sttAudioFormat,\n xiHeaders,\n} from './wire.ts';\n\nconst failures = new Set([\n 'error',\n 'auth_error',\n 'quota_exceeded',\n 'commit_throttled',\n 'unaccepted_terms',\n 'rate_limited',\n 'queue_overflow',\n 'resource_exhausted',\n 'session_time_limit_exceeded',\n 'input_error',\n 'invalid_request',\n 'chunk_size_exceeded',\n 'insufficient_audio_activity',\n 'transcriber_error',\n]);\n\ninterface ListenState {\n turns: string[];\n segments: TranscriptSegment[];\n segment: number;\n started: boolean;\n bytes: number;\n opened: boolean;\n ready: boolean;\n clientDone: boolean;\n failed: boolean;\n completed: boolean;\n lastCommit: string | undefined;\n}\n\nfunction terms(options: ListenOptions | undefined): string[] {\n const vocabulary = options?.vocabulary;\n if (!vocabulary) return [];\n return vocabulary;\n}\n\nfunction commitStrategy(model: TranscriptionModel<string>): string {\n if (model.turnDetection === 'semantic') return 'turn_prediction';\n if (model.turnDetection === 'vad') return 'vad';\n return 'manual';\n}\n\nfunction sttUrl(\n model: TranscriptionModel<string>,\n format: AudioFormat,\n options: ListenOptions | undefined\n): string {\n const url = socketUrl(model.baseUrl, 'speech-to-text/realtime');\n const params = new URLSearchParams();\n params.set('model_id', model.id);\n params.set('audio_format', sttAudioFormat(format));\n params.set('commit_strategy', commitStrategy(model));\n params.set('include_timestamps', 'true');\n if (options?.language) params.set('language_code', options.language);\n for (const term of terms(options)) params.append('keyterms', term);\n url.search = params.toString();\n return url.toString();\n}\n\nfunction chosenFormat(\n model: TranscriptionModel<string>,\n chunk: AudioChunk | undefined\n): AudioFormat {\n const format = chunk === undefined ? model.inputFormats[0] : chunk.format;\n if (!format) throw new Error('No input format');\n sttAudioFormat(format);\n const known = model.inputFormats.some((item) => sameFormat(item, format));\n if (!known) throw new Error(`Unsupported audio format ${format.encoding}`);\n return format;\n}\n\nfunction textOr(event: Record<string, unknown>): string {\n const text = stringField(event, 'text');\n if (text === undefined) return '';\n return text;\n}\n\nfunction secondsOf(word: Record<string, unknown>, key: string): number {\n const value = numberField(word, key);\n if (value === undefined) return 0;\n return value;\n}\n\nfunction wordFrom(word: Record<string, unknown>): TranscriptSegment | undefined {\n if (stringField(word, 'type') === 'spacing') return undefined;\n const speaker = stringField(word, 'speaker_id');\n const segment: TranscriptSegment = {\n text: textOr(word),\n start: secondsOf(word, 'start'),\n end: secondsOf(word, 'end'),\n };\n if (speaker !== undefined) segment.speaker = speaker;\n return segment;\n}\n\nfunction wordsOf(event: Record<string, unknown>, state: ListenState) {\n if (!Array.isArray(event.words)) return;\n for (const word of event.words) {\n if (!isRecord(word)) continue;\n const segment = wordFrom(word);\n if (segment) state.segments.push(segment);\n }\n}\n\nfunction emitCommit(spoken: string, state: ListenState, push: (event: TranscriptionEvent) => void) {\n if (!state.started) {\n state.started = true;\n push({ type: 'speech_start', at: 0 });\n }\n const segmentId = String(state.segment);\n state.segment += 1;\n state.turns.push(spoken);\n push({ type: 'final', segmentId, text: spoken });\n state.started = false;\n push({ type: 'turn_end', text: spoken, at: 0 });\n}\n\nfunction applyListen(\n event: Record<string, unknown>,\n state: ListenState,\n push: (event: TranscriptionEvent) => void\n) {\n const type = stringField(event, 'message_type');\n if (type === 'session_started') {\n state.ready = true;\n return;\n }\n if (type === 'partial_transcript') {\n if (!state.started) {\n state.started = true;\n push({ type: 'speech_start', at: 0 });\n }\n push({ type: 'partial', segmentId: String(state.segment), text: textOr(event) });\n return;\n }\n if (type === 'committed_transcript') {\n const spoken = textOr(event);\n state.lastCommit = spoken;\n emitCommit(spoken, state, push);\n return;\n }\n if (type === 'committed_transcript_with_timestamps') {\n const spoken = textOr(event);\n wordsOf(event, state);\n if (state.lastCommit !== spoken) emitCommit(spoken, state, push);\n state.lastCommit = undefined;\n return;\n }\n if (type !== undefined && failures.has(type)) {\n const message = stringField(event, 'error');\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'ElevenLabs transcription failed' : message,\n });\n }\n}\n\nfunction finish(\n model: TranscriptionModel<string>,\n format: AudioFormat,\n state: ListenState,\n push: (event: TranscriptionEvent) => void\n) {\n if (state.failed || state.completed) return;\n state.completed = true;\n const heard = chunkSeconds({ data: new Uint8Array(state.bytes), format, timestamp: 0 });\n push({\n type: 'done',\n transcript: {\n text: state.turns.join(' '),\n ...(state.segments.length > 0 ? { segments: state.segments } : {}),\n usage: priceAudio(model.audioCost, { inputSeconds: heard }),\n },\n });\n}\n\nexport function listenElevenLabs(\n model: TranscriptionModel<string>,\n audio: AsyncIterable<AudioChunk>,\n options?: ListenOptions\n): TranscriptionEventStream {\n const stream = new TranscriptionEventStream();\n void pumpListen(model, audio, options, stream);\n return stream;\n}\n\nasync function pumpListen(\n model: TranscriptionModel<string>,\n audio: AsyncIterable<AudioChunk>,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream\n) {\n const state: ListenState = {\n turns: [],\n segments: [],\n segment: 0,\n started: false,\n bytes: 0,\n opened: false,\n ready: false,\n clientDone: false,\n failed: false,\n completed: false,\n lastCommit: undefined,\n };\n let settled = false;\n const push = (event: TranscriptionEvent) => {\n if (settled) return;\n if (event.type === 'done' || event.type === 'error') settled = true;\n stream.push(event);\n };\n let socket: WebSocket | undefined;\n const readyWait = Promise.withResolvers<void>();\n const doneWait = Promise.withResolvers<void>();\n const wake = (kind: string | undefined) => {\n if (state.ready || state.failed) readyWait.resolve();\n if (state.failed) doneWait.resolve();\n if (!state.clientDone) return;\n if (kind === 'committed_transcript' || kind === 'committed_transcript_with_timestamps') {\n doneWait.resolve();\n }\n if (kind === 'close') doneWait.resolve();\n };\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const iterator = audio[Symbol.asyncIterator]();\n const first = await iterator.next();\n const format = chosenFormat(model, first.done ? undefined : first.value);\n socket = connectSocket(sttUrl(model, format, options), xiHeaders(model, options));\n let incoming = Promise.resolve();\n socket.addEventListener('message', (event) => {\n incoming = incoming.then(() => onMessage(event.data, state, push, wake));\n });\n socket.addEventListener('close', () => {\n if (state.opened && !state.failed && !state.completed && !state.clientDone) {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: 'ElevenLabs transcription socket closed' });\n }\n wake('close');\n });\n await awaitOpen(socket, 'ElevenLabs transcription', (message) => {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: message });\n wake('fail');\n });\n if (state.failed) return;\n state.opened = true;\n if (!state.ready) await readyWait.promise;\n if (state.failed) return;\n if (!first.done) sendChunk(socket, first.value, format, state, false);\n while (!state.failed) {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const next = await iterator.next();\n if (next.done) break;\n sendChunk(socket, next.value, format, state, false);\n }\n if (state.failed) return;\n sendChunk(socket, undefined, format, state, true);\n state.clientDone = true;\n await doneWait.promise;\n finish(model, format, state, push);\n } catch (error) {\n const voice = asVoiceError(error);\n state.failed = true;\n push({ type: 'error', reason: voice.reason, error: voice.error });\n } finally {\n socket?.close();\n }\n}\n\nfunction sendChunk(\n socket: WebSocket,\n chunk: AudioChunk | undefined,\n format: AudioFormat,\n state: ListenState,\n commit: boolean\n) {\n if (chunk && !sameFormat(chunk.format, format)) {\n throw new Error(`Unsupported audio format ${chunk.format.encoding}`);\n }\n const data = chunk ? chunk.data : new Uint8Array();\n state.bytes += data.byteLength;\n socket.send(\n JSON.stringify({\n message_type: 'input_audio_chunk',\n audio_base_64: encodeAudioBase64(data),\n commit,\n sample_rate: format.sampleRate,\n })\n );\n}\n\nasync function onMessage(\n data: unknown,\n state: ListenState,\n push: (event: TranscriptionEvent) => void,\n wake: (kind: string | undefined) => void\n) {\n try {\n const parsed = JSON.parse(await messageText(data)) as unknown;\n if (!isRecord(parsed)) throw new Error('ElevenLabs transcription event was not JSON');\n applyListen(parsed, state, push);\n wake(stringField(parsed, 'message_type'));\n } catch (error) {\n const voice = asVoiceError(error);\n state.failed = true;\n push({ type: 'error', reason: voice.reason, error: voice.error });\n wake('fail');\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,251 @@
1
+ import { chunkSeconds } from "../audio.js";
2
+ import { decodeAudioBase64 } from "../convert.js";
3
+ import { VoiceRequestError } from "../errors.js";
4
+ import {
5
+ applyPayload,
6
+ asVoiceError,
7
+ endpoint,
8
+ isRecord,
9
+ readJson,
10
+ requestSignal,
11
+ stringField
12
+ } from "../openai/wire.js";
13
+ import { SpeechEventStream } from "../speech.js";
14
+ import { priceAudio } from "../usage.js";
15
+ import {
16
+ alignmentFrom,
17
+ awaitOpen,
18
+ connectSocket,
19
+ messageText,
20
+ outputFormatName,
21
+ sameFormat,
22
+ socketUrl,
23
+ xiHeaders
24
+ } from "./wire.js";
25
+ function languageCode(language) {
26
+ if (language === void 0) return void 0;
27
+ if (language.trim() === "") return void 0;
28
+ return language;
29
+ }
30
+ function resolveFormat(model, requested) {
31
+ const fallback = model.outputFormats[0];
32
+ if (!fallback) throw new Error("No output format");
33
+ if (!requested) return fallback;
34
+ const match = model.outputFormats.find((format) => sameFormat(format, requested));
35
+ if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
36
+ return match;
37
+ }
38
+ function speechBody(model, text, language) {
39
+ const body = { text, model_id: model.id };
40
+ const code = languageCode(language);
41
+ if (code !== void 0 && model.id !== "eleven_multilingual_v2") body.language_code = code;
42
+ return body;
43
+ }
44
+ function speechUrl(model, voice, format) {
45
+ const url = socketUrl(model.baseUrl, `text-to-speech/${encodeURIComponent(voice)}/stream-input`);
46
+ const params = new URLSearchParams();
47
+ params.set("model_id", model.id);
48
+ params.set("output_format", outputFormatName(format));
49
+ params.set("sync_alignment", "true");
50
+ url.search = params.toString();
51
+ return url.toString();
52
+ }
53
+ async function readText(text, signal) {
54
+ if (typeof text === "string") return text;
55
+ let combined = "";
56
+ for await (const part of text) {
57
+ if (signal?.aborted) throw new DOMException("aborted", "AbortError");
58
+ combined += part;
59
+ }
60
+ return combined;
61
+ }
62
+ async function xiFetch(model, options, url, body) {
63
+ const headers = xiHeaders(model, options);
64
+ headers["Content-Type"] = "application/json";
65
+ const fetchImpl = options?.fetch ?? globalThis.fetch;
66
+ const response = await fetchImpl(url, {
67
+ method: "POST",
68
+ headers,
69
+ body,
70
+ signal: requestSignal(options)
71
+ });
72
+ await options?.onResponse?.({ status: response.status, headers: {} }, model);
73
+ if (!response.ok) {
74
+ const text = await response.text();
75
+ throw new Error(text === "" ? `HTTP ${response.status}` : text);
76
+ }
77
+ return response;
78
+ }
79
+ function speakElevenLabs(model, input, options) {
80
+ const stream = new SpeechEventStream();
81
+ void pumpSpeech(model, input, options, stream);
82
+ return stream;
83
+ }
84
+ async function pumpSpeech(model, input, options, stream) {
85
+ try {
86
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
87
+ const format = resolveFormat(model, input.format);
88
+ if (model.textStreaming && typeof input.text !== "string") {
89
+ await streamSpeech(model, input.text, input.voice, format, options, stream);
90
+ return;
91
+ }
92
+ const text = await readText(input.text, options?.signal);
93
+ await postSpeech(model, text, input.voice, input.language, format, options, stream);
94
+ } catch (error) {
95
+ const voice = asVoiceError(error);
96
+ stream.push({ type: "error", reason: voice.reason, error: voice.error });
97
+ }
98
+ }
99
+ async function postSpeech(model, text, voice, language, format, options, stream) {
100
+ const payload = speechBody(model, text, language);
101
+ const body = await applyPayload(payload, model, options);
102
+ if (!isRecord(body)) throw new Error("Speech payload was not JSON");
103
+ const query = `output_format=${outputFormatName(format)}`;
104
+ const path = `text-to-speech/${encodeURIComponent(voice)}/with-timestamps?${query}`;
105
+ const response = await xiFetch(
106
+ model,
107
+ options,
108
+ endpoint(model.baseUrl, path),
109
+ JSON.stringify(body)
110
+ );
111
+ const parsed = await readJson(response);
112
+ if (!isRecord(parsed)) throw new Error("Speech response had no audio");
113
+ const audio = stringField(parsed, "audio_base64");
114
+ if (audio === void 0) throw new Error("Speech response had no audio");
115
+ const data = decodeAudioBase64(audio);
116
+ const seconds = chunkSeconds({ data, format, timestamp: 0 });
117
+ const meters = { characters: text.length };
118
+ if (seconds > 0) meters.outputSeconds = seconds;
119
+ const alignment = alignmentFrom(parsed.alignment);
120
+ stream.push({ type: "start", format });
121
+ stream.push({
122
+ type: "audio",
123
+ chunk: { data, format, timestamp: 0 },
124
+ span: { itemId: model.id, start: 0, end: text.length },
125
+ ...alignment ? { alignment } : {}
126
+ });
127
+ stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
128
+ }
129
+ async function streamSpeech(model, text, voice, format, options, stream) {
130
+ const state = {
131
+ opened: false,
132
+ started: false,
133
+ done: false,
134
+ failed: false,
135
+ outputSeconds: 0,
136
+ characters: 0
137
+ };
138
+ let settled = false;
139
+ const push = (event) => {
140
+ if (settled) return;
141
+ if (event.type === "done" || event.type === "error") settled = true;
142
+ stream.push(event);
143
+ };
144
+ const doneWait = Promise.withResolvers();
145
+ const wake = () => {
146
+ doneWait.resolve();
147
+ };
148
+ const socket = connectSocket(speechUrl(model, voice, format), xiHeaders(model, options));
149
+ let incoming = Promise.resolve();
150
+ socket.addEventListener("message", (event) => {
151
+ incoming = incoming.then(() => onMessage(event.data, model.id, format, state, push, wake));
152
+ });
153
+ socket.addEventListener("close", () => {
154
+ if (state.opened && !state.done && !state.failed) {
155
+ state.failed = true;
156
+ push({ type: "error", reason: "error", error: "ElevenLabs speech socket closed" });
157
+ }
158
+ wake();
159
+ });
160
+ try {
161
+ await awaitOpen(socket, "ElevenLabs speech", (message) => {
162
+ state.failed = true;
163
+ push({ type: "error", reason: "error", error: message });
164
+ wake();
165
+ });
166
+ if (state.failed) return;
167
+ state.opened = true;
168
+ state.characters = await writeText(socket, text, options?.signal);
169
+ if (state.failed) return;
170
+ socket.send(JSON.stringify({ text: " ", flush: true }));
171
+ socket.send(JSON.stringify({ text: "" }));
172
+ if (!state.done) await doneWait.promise;
173
+ if (state.failed) return;
174
+ const meters = { characters: state.characters };
175
+ if (state.outputSeconds > 0) meters.outputSeconds = state.outputSeconds;
176
+ push({ type: "done", usage: priceAudio(model.audioCost, meters) });
177
+ } catch (error) {
178
+ const voice2 = asVoiceError(error);
179
+ state.failed = true;
180
+ push({ type: "error", reason: voice2.reason, error: voice2.error });
181
+ } finally {
182
+ socket.close();
183
+ }
184
+ }
185
+ async function writeText(socket, text, signal) {
186
+ let combined = "";
187
+ socket.send(
188
+ JSON.stringify({
189
+ text: " ",
190
+ generation_config: { chunk_length_schedule: [50, 120, 160, 290] }
191
+ })
192
+ );
193
+ for await (const part of text) {
194
+ if (signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
195
+ combined += part;
196
+ if (part === "") continue;
197
+ const delta = part.endsWith(" ") ? part : `${part} `;
198
+ socket.send(JSON.stringify({ text: delta }));
199
+ }
200
+ return combined.length;
201
+ }
202
+ async function onMessage(data, itemId, format, state, push, wake) {
203
+ try {
204
+ const parsed = JSON.parse(await messageText(data));
205
+ if (!isRecord(parsed)) throw new Error("ElevenLabs speech event was not JSON");
206
+ applySpeech(parsed, itemId, format, state, push);
207
+ if (state.done || state.failed) wake();
208
+ } catch (error) {
209
+ const voice = asVoiceError(error);
210
+ state.failed = true;
211
+ push({ type: "error", reason: voice.reason, error: voice.error });
212
+ wake();
213
+ }
214
+ }
215
+ function audioEvent(itemId, format, data, end, alignment) {
216
+ return {
217
+ type: "audio",
218
+ chunk: { data, format, timestamp: 0 },
219
+ span: { itemId, start: 0, end },
220
+ ...alignment ? { alignment } : {}
221
+ };
222
+ }
223
+ function applySpeech(event, itemId, format, state, push) {
224
+ if ("error" in event) {
225
+ const message = stringField(event, "error");
226
+ state.failed = true;
227
+ push({
228
+ type: "error",
229
+ reason: "error",
230
+ error: message === void 0 ? "ElevenLabs speech failed" : message
231
+ });
232
+ return;
233
+ }
234
+ if (event.isFinal === true) {
235
+ state.done = true;
236
+ return;
237
+ }
238
+ const audio = stringField(event, "audio");
239
+ if (audio === void 0 || audio === "") return;
240
+ const data = decodeAudioBase64(audio);
241
+ if (!state.started) {
242
+ state.started = true;
243
+ push({ type: "start", format });
244
+ }
245
+ state.outputSeconds += chunkSeconds({ data, format, timestamp: 0 });
246
+ push(audioEvent(itemId, format, data, state.characters, alignmentFrom(event.alignment)));
247
+ }
248
+ export {
249
+ speakElevenLabs
250
+ };
251
+ //# sourceMappingURL=speak.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs/speak.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat, CharAlignment } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport { decodeAudioBase64 } from '../convert.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { SpeechModel } from '../models.ts';\nimport {\n applyPayload,\n asVoiceError,\n endpoint,\n isRecord,\n readJson,\n requestSignal,\n stringField,\n} from '../openai/wire.ts';\nimport { type SpeechEvent, SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport {\n alignmentFrom,\n awaitOpen,\n connectSocket,\n messageText,\n outputFormatName,\n sameFormat,\n socketUrl,\n xiHeaders,\n} from './wire.ts';\n\ninterface SpeechState {\n opened: boolean;\n started: boolean;\n done: boolean;\n failed: boolean;\n outputSeconds: number;\n characters: number;\n}\n\nfunction languageCode(language: string | undefined): string | undefined {\n if (language === undefined) return undefined;\n if (language.trim() === '') return undefined;\n return language;\n}\n\nfunction resolveFormat(\n model: SpeechModel<string>,\n requested: AudioFormat | undefined\n): AudioFormat {\n const fallback = model.outputFormats[0];\n if (!fallback) throw new Error('No output format');\n if (!requested) return fallback;\n const match = model.outputFormats.find((format) => sameFormat(format, requested));\n if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);\n return match;\n}\n\nfunction speechBody(\n model: SpeechModel<string>,\n text: string,\n language: string | undefined\n): Record<string, unknown> {\n const body: Record<string, unknown> = { text, model_id: model.id };\n const code = languageCode(language);\n if (code !== undefined && model.id !== 'eleven_multilingual_v2') body.language_code = code;\n return body;\n}\n\nfunction speechUrl(model: SpeechModel<string>, voice: string, format: AudioFormat): string {\n const url = socketUrl(model.baseUrl, `text-to-speech/${encodeURIComponent(voice)}/stream-input`);\n const params = new URLSearchParams();\n params.set('model_id', model.id);\n params.set('output_format', outputFormatName(format));\n params.set('sync_alignment', 'true');\n url.search = params.toString();\n return url.toString();\n}\n\nasync function readText(\n text: string | AsyncIterable<string>,\n signal: AbortSignal | undefined\n): Promise<string> {\n if (typeof text === 'string') return text;\n let combined = '';\n for await (const part of text) {\n if (signal?.aborted) throw new DOMException('aborted', 'AbortError');\n combined += part;\n }\n return combined;\n}\n\nasync function xiFetch(\n model: SpeechModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: string\n): Promise<Response> {\n const headers = xiHeaders(model, options);\n headers['Content-Type'] = 'application/json';\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n signal: requestSignal(options),\n });\n await options?.onResponse?.({ status: response.status, headers: {} }, model as never);\n if (!response.ok) {\n const text = await response.text();\n throw new Error(text === '' ? `HTTP ${response.status}` : text);\n }\n return response;\n}\n\nexport function speakElevenLabs(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n void pumpSpeech(model, input, options, stream);\n return stream;\n}\n\nasync function pumpSpeech(\n model: SpeechModel<string>,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = resolveFormat(model, input.format);\n if (model.textStreaming && typeof input.text !== 'string') {\n await streamSpeech(model, input.text, input.voice, format, options, stream);\n return;\n }\n const text = await readText(input.text, options?.signal);\n await postSpeech(model, text, input.voice, input.language, format, options, stream);\n } catch (error) {\n const voice = asVoiceError(error);\n stream.push({ type: 'error', reason: voice.reason, error: voice.error });\n }\n}\n\nasync function postSpeech(\n model: SpeechModel<string>,\n text: string,\n voice: string,\n language: string | undefined,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const payload = speechBody(model, text, language);\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body)) throw new Error('Speech payload was not JSON');\n const query = `output_format=${outputFormatName(format)}`;\n const path = `text-to-speech/${encodeURIComponent(voice)}/with-timestamps?${query}`;\n const response = await xiFetch(\n model,\n options,\n endpoint(model.baseUrl, path),\n JSON.stringify(body)\n );\n const parsed = await readJson(response);\n if (!isRecord(parsed)) throw new Error('Speech response had no audio');\n const audio = stringField(parsed, 'audio_base64');\n if (audio === undefined) throw new Error('Speech response had no audio');\n const data = decodeAudioBase64(audio);\n const seconds = chunkSeconds({ data, format, timestamp: 0 });\n const meters: AudioCounters = { characters: text.length };\n if (seconds > 0) meters.outputSeconds = seconds;\n const alignment = alignmentFrom(parsed.alignment);\n stream.push({ type: 'start', format });\n stream.push({\n type: 'audio',\n chunk: { data, format, timestamp: 0 },\n span: { itemId: model.id, start: 0, end: text.length },\n ...(alignment ? { alignment } : {}),\n });\n stream.push({ type: 'done', usage: priceAudio(model.audioCost, meters) });\n}\n\nasync function streamSpeech(\n model: SpeechModel<string>,\n text: AsyncIterable<string>,\n voice: string,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const state: SpeechState = {\n opened: false,\n started: false,\n done: false,\n failed: false,\n outputSeconds: 0,\n characters: 0,\n };\n let settled = false;\n const push = (event: SpeechEvent) => {\n if (settled) return;\n if (event.type === 'done' || event.type === 'error') settled = true;\n stream.push(event);\n };\n const doneWait = Promise.withResolvers<void>();\n const wake = () => {\n doneWait.resolve();\n };\n const socket = connectSocket(speechUrl(model, voice, format), xiHeaders(model, options));\n let incoming = Promise.resolve();\n socket.addEventListener('message', (event) => {\n incoming = incoming.then(() => onMessage(event.data, model.id, format, state, push, wake));\n });\n socket.addEventListener('close', () => {\n if (state.opened && !state.done && !state.failed) {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: 'ElevenLabs speech socket closed' });\n }\n wake();\n });\n try {\n await awaitOpen(socket, 'ElevenLabs speech', (message) => {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: message });\n wake();\n });\n if (state.failed) return;\n state.opened = true;\n state.characters = await writeText(socket, text, options?.signal);\n if (state.failed) return;\n socket.send(JSON.stringify({ text: ' ', flush: true }));\n socket.send(JSON.stringify({ text: '' }));\n if (!state.done) await doneWait.promise;\n if (state.failed) return;\n const meters: AudioCounters = { characters: state.characters };\n if (state.outputSeconds > 0) meters.outputSeconds = state.outputSeconds;\n push({ type: 'done', usage: priceAudio(model.audioCost, meters) });\n } catch (error) {\n const voice = asVoiceError(error);\n state.failed = true;\n push({ type: 'error', reason: voice.reason, error: voice.error });\n } finally {\n socket.close();\n }\n}\n\nasync function writeText(\n socket: WebSocket,\n text: AsyncIterable<string>,\n signal: AbortSignal | undefined\n): Promise<number> {\n let combined = '';\n socket.send(\n JSON.stringify({\n text: ' ',\n generation_config: { chunk_length_schedule: [50, 120, 160, 290] },\n })\n );\n for await (const part of text) {\n if (signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n combined += part;\n if (part === '') continue;\n const delta = part.endsWith(' ') ? part : `${part} `;\n socket.send(JSON.stringify({ text: delta }));\n }\n return combined.length;\n}\n\nasync function onMessage(\n data: unknown,\n itemId: string,\n format: AudioFormat,\n state: SpeechState,\n push: (event: SpeechEvent) => void,\n wake: () => void\n) {\n try {\n const parsed = JSON.parse(await messageText(data)) as unknown;\n if (!isRecord(parsed)) throw new Error('ElevenLabs speech event was not JSON');\n applySpeech(parsed, itemId, format, state, push);\n if (state.done || state.failed) wake();\n } catch (error) {\n const voice = asVoiceError(error);\n state.failed = true;\n push({ type: 'error', reason: voice.reason, error: voice.error });\n wake();\n }\n}\n\nfunction audioEvent(\n itemId: string,\n format: AudioFormat,\n data: Uint8Array,\n end: number,\n alignment: CharAlignment | undefined\n): SpeechEvent {\n return {\n type: 'audio',\n chunk: { data, format, timestamp: 0 },\n span: { itemId, start: 0, end },\n ...(alignment ? { alignment } : {}),\n };\n}\n\nfunction applySpeech(\n event: Record<string, unknown>,\n itemId: string,\n format: AudioFormat,\n state: SpeechState,\n push: (event: SpeechEvent) => void\n) {\n if ('error' in event) {\n const message = stringField(event, 'error');\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'ElevenLabs speech failed' : message,\n });\n return;\n }\n if (event.isFinal === true) {\n state.done = true;\n return;\n }\n const audio = stringField(event, 'audio');\n if (audio === undefined || audio === '') return;\n const data = decodeAudioBase64(audio);\n if (!state.started) {\n state.started = true;\n push({ type: 'start', format });\n }\n state.outputSeconds += chunkSeconds({ data, format, timestamp: 0 });\n push(audioEvent(itemId, format, data, state.characters, alignmentFrom(event.alignment)));\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["voice"]
7
+ }
@@ -0,0 +1,132 @@
1
+ import { VoiceRequestError } from "../errors.js";
2
+ import {
3
+ applyPayload,
4
+ asVoiceError,
5
+ endpoint,
6
+ isRecord,
7
+ numberField,
8
+ readAudio,
9
+ readJson,
10
+ requestSignal,
11
+ stringField
12
+ } from "../openai/wire.js";
13
+ import { priceAudio } from "../usage.js";
14
+ import { audioFilename, encodeForm, formType, pcmS16, xiHeaders } from "./wire.js";
15
+ function textOf(record, key) {
16
+ const text = stringField(record, key);
17
+ if (text === void 0) return "";
18
+ return text;
19
+ }
20
+ function secondsOf(record, key) {
21
+ const value = numberField(record, key);
22
+ if (value === void 0) return 0;
23
+ return value;
24
+ }
25
+ function wordFrom(word) {
26
+ const kind = stringField(word, "type");
27
+ if (kind === "spacing") return void 0;
28
+ const speaker = stringField(word, "speaker_id");
29
+ const segment = {
30
+ text: textOf(word, "text"),
31
+ start: secondsOf(word, "start"),
32
+ end: secondsOf(word, "end")
33
+ };
34
+ if (speaker !== void 0) segment.speaker = speaker;
35
+ return segment;
36
+ }
37
+ function wordsFrom(value) {
38
+ if (!Array.isArray(value)) return [];
39
+ const segments = [];
40
+ for (const word of value) {
41
+ if (!isRecord(word)) continue;
42
+ const segment = wordFrom(word);
43
+ if (segment) segments.push(segment);
44
+ }
45
+ return segments;
46
+ }
47
+ function transcriptFrom(body, model) {
48
+ if (!isRecord(body)) throw new Error("Transcription response had no text");
49
+ const direct = stringField(body, "text");
50
+ const channels = Array.isArray(body.transcripts) ? body.transcripts.filter(isRecord) : [];
51
+ const joined = channels.map((channel) => textOf(channel, "text")).join(" ");
52
+ const text = direct === void 0 ? joined : direct;
53
+ if (direct === void 0 && channels.length === 0)
54
+ throw new Error("Transcription response had no text");
55
+ const language = stringField(body, "language_code");
56
+ const words = direct === void 0 ? channels.flatMap((channel) => wordsFrom(channel.words)) : wordsFrom(body.words);
57
+ const duration = numberField(body, "audio_duration_secs");
58
+ return {
59
+ text,
60
+ ...language !== void 0 ? { language } : {},
61
+ ...words.length > 0 ? { segments: words } : {},
62
+ usage: priceAudio(model.audioCost, { inputSeconds: duration === void 0 ? 0 : duration })
63
+ };
64
+ }
65
+ function fileOf(body) {
66
+ const file = body.file;
67
+ if (!isRecord(file) || !(file.data instanceof Uint8Array) || typeof file.mimeType !== "string") {
68
+ throw new Error("Transcription payload was not a file");
69
+ }
70
+ const named = typeof file.filename === "string" ? file.filename : audioFilename(file.mimeType);
71
+ return { data: file.data, mimeType: file.mimeType, filename: named };
72
+ }
73
+ function fieldStrings(body) {
74
+ const fields = {};
75
+ for (const [key, value] of Object.entries(body)) {
76
+ if (key === "file") continue;
77
+ if (typeof value !== "string") continue;
78
+ fields[key] = value;
79
+ }
80
+ return fields;
81
+ }
82
+ async function xiFetch(model, options, url, body, contentType) {
83
+ const headers = xiHeaders(model, options);
84
+ headers["Content-Type"] = contentType;
85
+ const fetchImpl = options?.fetch ?? globalThis.fetch;
86
+ const response = await fetchImpl(url, {
87
+ method: "POST",
88
+ headers,
89
+ body,
90
+ signal: requestSignal(options)
91
+ });
92
+ await options?.onResponse?.({ status: response.status, headers: {} }, model);
93
+ if (!response.ok) {
94
+ const text = await response.text();
95
+ throw new Error(text === "" ? `HTTP ${response.status}` : text);
96
+ }
97
+ return response;
98
+ }
99
+ async function transcribeElevenLabs(model, input, options) {
100
+ try {
101
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
102
+ const fetchImpl = options?.fetch ?? globalThis.fetch;
103
+ const loaded = await readAudio(input, fetchImpl, requestSignal(options));
104
+ const filename = audioFilename(loaded.mimeType);
105
+ const payload = {
106
+ model_id: model.id,
107
+ timestamps_granularity: "word",
108
+ file: { data: loaded.data, mimeType: loaded.mimeType, filename }
109
+ };
110
+ if (model.diarization) payload.diarize = "true";
111
+ if (pcmS16(loaded.mimeType)) payload.file_format = "pcm_s16le_16";
112
+ const body = await applyPayload(payload, model, options);
113
+ if (!isRecord(body)) throw new Error("Transcription payload was not a file");
114
+ const file = fileOf(body);
115
+ const encoded = encodeForm(file, fieldStrings(body));
116
+ const response = await xiFetch(
117
+ model,
118
+ options,
119
+ endpoint(model.baseUrl, "speech-to-text"),
120
+ encoded,
121
+ formType()
122
+ );
123
+ return transcriptFrom(await readJson(response), model);
124
+ } catch (error) {
125
+ const voice = asVoiceError(error);
126
+ throw new VoiceRequestError(voice.reason, voice.error);
127
+ }
128
+ }
129
+ export {
130
+ transcribeElevenLabs
131
+ };
132
+ //# sourceMappingURL=transcribe.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs/transcribe.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport {\n applyPayload,\n asVoiceError,\n endpoint,\n isRecord,\n numberField,\n readAudio,\n readJson,\n requestSignal,\n stringField,\n} from '../openai/wire.ts';\nimport type { AudioInput, Transcript, TranscriptSegment } from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport { audioFilename, type ElevenFile, encodeForm, formType, pcmS16, xiHeaders } from './wire.ts';\n\nfunction textOf(record: Record<string, unknown>, key: string): string {\n const text = stringField(record, key);\n if (text === undefined) return '';\n return text;\n}\n\nfunction secondsOf(record: Record<string, unknown>, key: string): number {\n const value = numberField(record, key);\n if (value === undefined) return 0;\n return value;\n}\n\nfunction wordFrom(word: Record<string, unknown>): TranscriptSegment | undefined {\n const kind = stringField(word, 'type');\n if (kind === 'spacing') return undefined;\n const speaker = stringField(word, 'speaker_id');\n const segment: TranscriptSegment = {\n text: textOf(word, 'text'),\n start: secondsOf(word, 'start'),\n end: secondsOf(word, 'end'),\n };\n if (speaker !== undefined) segment.speaker = speaker;\n return segment;\n}\n\nfunction wordsFrom(value: unknown): TranscriptSegment[] {\n if (!Array.isArray(value)) return [];\n const segments: TranscriptSegment[] = [];\n for (const word of value) {\n if (!isRecord(word)) continue;\n const segment = wordFrom(word);\n if (segment) segments.push(segment);\n }\n return segments;\n}\n\nfunction transcriptFrom(body: unknown, model: TranscriptionModel<string>): Transcript {\n if (!isRecord(body)) throw new Error('Transcription response had no text');\n const direct = stringField(body, 'text');\n const channels = Array.isArray(body.transcripts) ? body.transcripts.filter(isRecord) : [];\n const joined = channels.map((channel) => textOf(channel, 'text')).join(' ');\n const text = direct === undefined ? joined : direct;\n if (direct === undefined && channels.length === 0)\n throw new Error('Transcription response had no text');\n const language = stringField(body, 'language_code');\n const words =\n direct === undefined\n ? channels.flatMap((channel) => wordsFrom(channel.words))\n : wordsFrom(body.words);\n const duration = numberField(body, 'audio_duration_secs');\n return {\n text,\n ...(language !== undefined ? { language } : {}),\n ...(words.length > 0 ? { segments: words } : {}),\n usage: priceAudio(model.audioCost, { inputSeconds: duration === undefined ? 0 : duration }),\n };\n}\n\nfunction fileOf(body: Record<string, unknown>): ElevenFile {\n const file = body.file;\n if (!isRecord(file) || !(file.data instanceof Uint8Array) || typeof file.mimeType !== 'string') {\n throw new Error('Transcription payload was not a file');\n }\n const named = typeof file.filename === 'string' ? file.filename : audioFilename(file.mimeType);\n return { data: file.data, mimeType: file.mimeType, filename: named };\n}\n\nfunction fieldStrings(body: Record<string, unknown>): Record<string, string> {\n const fields: Record<string, string> = {};\n for (const [key, value] of Object.entries(body)) {\n if (key === 'file') continue;\n if (typeof value !== 'string') continue;\n fields[key] = value;\n }\n return fields;\n}\n\nasync function xiFetch(\n model: TranscriptionModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: BodyInit,\n contentType: string\n): Promise<Response> {\n const headers = xiHeaders(model, options);\n headers['Content-Type'] = contentType;\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n signal: requestSignal(options),\n });\n await options?.onResponse?.({ status: response.status, headers: {} }, model as never);\n if (!response.ok) {\n const text = await response.text();\n throw new Error(text === '' ? `HTTP ${response.status}` : text);\n }\n return response;\n}\n\nexport async function transcribeElevenLabs(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const loaded = await readAudio(input, fetchImpl, requestSignal(options));\n const filename = audioFilename(loaded.mimeType);\n const payload: Record<string, unknown> = {\n model_id: model.id,\n timestamps_granularity: 'word',\n file: { data: loaded.data, mimeType: loaded.mimeType, filename },\n };\n if (model.diarization) payload.diarize = 'true';\n if (pcmS16(loaded.mimeType)) payload.file_format = 'pcm_s16le_16';\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body)) throw new Error('Transcription payload was not a file');\n const file = fileOf(body);\n const encoded = encodeForm(file, fieldStrings(body));\n const response = await xiFetch(\n model,\n options,\n endpoint(model.baseUrl, 'speech-to-text'),\n encoded,\n formType()\n );\n return transcriptFrom(await readJson(response), model);\n } catch (error) {\n const voice = asVoiceError(error);\n throw new VoiceRequestError(voice.reason, voice.error);\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,215 @@
1
+ import { isRecord } from "../openai/wire.js";
2
+ const boundary = "pi-audio-elevenlabs-boundary";
3
+ const filenames = {
4
+ "audio/wav": "audio.wav",
5
+ "audio/wave": "audio.wav",
6
+ "audio/x-wav": "audio.wav",
7
+ "audio/mpeg": "audio.mp3",
8
+ "audio/mp3": "audio.mp3",
9
+ "audio/flac": "audio.flac",
10
+ "audio/ogg": "audio.ogg",
11
+ "audio/opus": "audio.opus",
12
+ "audio/pcm": "audio.pcm",
13
+ "audio/l16": "audio.pcm",
14
+ "audio/mulaw": "audio.mulaw",
15
+ "audio/basic": "audio.mulaw",
16
+ "audio/alaw": "audio.alaw",
17
+ "audio/mp4": "audio.mp4",
18
+ "audio/m4a": "audio.m4a",
19
+ "audio/aac": "audio.aac",
20
+ "audio/webm": "audio.webm"
21
+ };
22
+ const sttFormats = /* @__PURE__ */ new Set([
23
+ "pcm_8000",
24
+ "pcm_16000",
25
+ "pcm_22050",
26
+ "pcm_24000",
27
+ "pcm_44100",
28
+ "pcm_48000",
29
+ "ulaw_8000"
30
+ ]);
31
+ function mediaType(mimeType) {
32
+ const type = mimeType.split(";")[0]?.trim().toLowerCase();
33
+ if (!type) throw new Error(`Unsupported audio type ${mimeType}`);
34
+ return type;
35
+ }
36
+ function audioFilename(mimeType) {
37
+ const name = filenames[mediaType(mimeType)];
38
+ if (!name) throw new Error(`Unsupported audio type ${mimeType}`);
39
+ return name;
40
+ }
41
+ function parameter(mimeType, name) {
42
+ for (const part of mimeType.split(";").slice(1)) {
43
+ const pieces = part.split("=");
44
+ const key = pieces[0]?.trim().toLowerCase();
45
+ const value = pieces[1]?.trim();
46
+ if (key === name && value) return value;
47
+ }
48
+ return void 0;
49
+ }
50
+ function pcmS16(mimeType) {
51
+ const type = mediaType(mimeType);
52
+ if (type !== "audio/pcm" && type !== "audio/l16") return false;
53
+ const rate = parameter(mimeType, "rate");
54
+ if (rate === void 0) return true;
55
+ return rate === "16000";
56
+ }
57
+ function encodeForm(file, fields) {
58
+ const chunks = [];
59
+ const encoder = new TextEncoder();
60
+ const write = (value) => {
61
+ chunks.push(encoder.encode(value));
62
+ };
63
+ for (const [name, value] of Object.entries(fields)) {
64
+ write(`--${boundary}\r
65
+ Content-Disposition: form-data; name="${name}"\r
66
+ \r
67
+ ${value}\r
68
+ `);
69
+ }
70
+ const header = `--${boundary}\r
71
+ Content-Disposition: form-data; name="file"; filename="${file.filename}"\r
72
+ Content-Type: ${file.mimeType}\r
73
+ \r
74
+ `;
75
+ write(header);
76
+ chunks.push(file.data);
77
+ write(`\r
78
+ --${boundary}--\r
79
+ `);
80
+ const body = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0));
81
+ let offset = 0;
82
+ for (const chunk of chunks) {
83
+ body.set(chunk, offset);
84
+ offset += chunk.length;
85
+ }
86
+ return body.buffer;
87
+ }
88
+ function formType() {
89
+ return `multipart/form-data; boundary=${boundary}`;
90
+ }
91
+ function headerValue(headers, name) {
92
+ if (!headers) return void 0;
93
+ const expected = name.toLowerCase();
94
+ for (const [key, value] of Object.entries(headers)) {
95
+ if (key.toLowerCase() === expected && value !== null && value.trim() !== "") return value;
96
+ }
97
+ return void 0;
98
+ }
99
+ function xiHeaders(model, options) {
100
+ const headers = { ...model.headers };
101
+ const extra = options?.headers;
102
+ if (extra) {
103
+ for (const [key, value] of Object.entries(extra)) {
104
+ if (value === null) delete headers[key];
105
+ else headers[key] = value;
106
+ }
107
+ }
108
+ if (headerValue(headers, "xi-api-key") === void 0) {
109
+ const apiKey = options?.apiKey;
110
+ if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
111
+ headers["xi-api-key"] = apiKey;
112
+ }
113
+ return headers;
114
+ }
115
+ function socketUrl(baseUrl, path) {
116
+ const url = new URL(baseUrl);
117
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
118
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/${path}`;
119
+ url.search = "";
120
+ return url;
121
+ }
122
+ function outputFormatName(format) {
123
+ if (format.encoding === "mp3" && format.sampleRate === 44100) return "mp3_44100_128";
124
+ if (format.encoding === "mp3" && format.sampleRate === 24e3) return "mp3_24000_48";
125
+ if (format.encoding === "mp3" && format.sampleRate === 22050) return "mp3_22050_32";
126
+ if (format.encoding === "pcm16") return `pcm_${format.sampleRate}`;
127
+ if (format.encoding === "wav") return `wav_${format.sampleRate}`;
128
+ if (format.encoding === "mulaw") return "ulaw_8000";
129
+ if (format.encoding === "alaw") return "alaw_8000";
130
+ if (format.encoding === "opus") return "opus_48000_128";
131
+ throw new Error(`Unsupported speech format ${format.encoding}`);
132
+ }
133
+ function sttAudioFormat(format) {
134
+ const name = format.encoding === "mulaw" ? "ulaw_8000" : `pcm_${format.sampleRate}`;
135
+ if (format.encoding !== "mulaw" && format.encoding !== "pcm16") {
136
+ throw new Error(`Unsupported audio format ${format.encoding}`);
137
+ }
138
+ if (!sttFormats.has(name)) throw new Error(`Unsupported audio format ${format.encoding}`);
139
+ return name;
140
+ }
141
+ function connectSocket(url, headers) {
142
+ const Socket = WebSocket;
143
+ return new Socket(url, { headers });
144
+ }
145
+ function awaitOpen(socket, label, fail) {
146
+ return new Promise((resolve) => {
147
+ let opened = false;
148
+ socket.addEventListener("open", () => {
149
+ opened = true;
150
+ resolve();
151
+ });
152
+ socket.addEventListener("error", () => {
153
+ fail(`${label} socket failed`);
154
+ if (!opened) resolve();
155
+ });
156
+ socket.addEventListener("close", () => {
157
+ if (!opened) {
158
+ fail(`${label} socket closed`);
159
+ resolve();
160
+ }
161
+ });
162
+ });
163
+ }
164
+ async function messageText(data) {
165
+ if (typeof data === "string") return data;
166
+ if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
167
+ if (data instanceof Uint8Array) return new TextDecoder().decode(data);
168
+ if (typeof Blob !== "undefined" && data instanceof Blob) return data.text();
169
+ throw new Error("Unsupported socket message");
170
+ }
171
+ function sameFormat(left, right) {
172
+ return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
173
+ }
174
+ function numberList(value) {
175
+ if (!Array.isArray(value)) return void 0;
176
+ const numbers = [];
177
+ for (const item of value) {
178
+ if (typeof item === "number") numbers.push(item);
179
+ }
180
+ return numbers;
181
+ }
182
+ function durationMs(start, end) {
183
+ if (start === void 0 || end === void 0) return 0;
184
+ return (end - start) * 1e3;
185
+ }
186
+ function alignmentFrom(value) {
187
+ if (!isRecord(value)) return void 0;
188
+ const startMs = numberList(value.charStartTimesMs);
189
+ const durationMsList = numberList(value.charDurationsMs);
190
+ if (startMs && durationMsList) return { startMs, durationMs: durationMsList };
191
+ const starts = numberList(value.character_start_times_seconds);
192
+ const ends = numberList(value.character_end_times_seconds);
193
+ if (!starts || !ends) return void 0;
194
+ return {
195
+ startMs: starts.map((start) => start * 1e3),
196
+ durationMs: starts.map((start, index) => durationMs(start, ends[index]))
197
+ };
198
+ }
199
+ export {
200
+ alignmentFrom,
201
+ audioFilename,
202
+ awaitOpen,
203
+ connectSocket,
204
+ encodeForm,
205
+ formType,
206
+ mediaType,
207
+ messageText,
208
+ outputFormatName,
209
+ pcmS16,
210
+ sameFormat,
211
+ socketUrl,
212
+ sttAudioFormat,
213
+ xiHeaders
214
+ };
215
+ //# sourceMappingURL=wire.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/elevenlabs/wire.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat, CharAlignment } from '../audio.ts';\nimport { isRecord } from '../openai/wire.ts';\n\nconst boundary = 'pi-audio-elevenlabs-boundary';\n\nconst filenames: Record<string, string> = {\n 'audio/wav': 'audio.wav',\n 'audio/wave': 'audio.wav',\n 'audio/x-wav': 'audio.wav',\n 'audio/mpeg': 'audio.mp3',\n 'audio/mp3': 'audio.mp3',\n 'audio/flac': 'audio.flac',\n 'audio/ogg': 'audio.ogg',\n 'audio/opus': 'audio.opus',\n 'audio/pcm': 'audio.pcm',\n 'audio/l16': 'audio.pcm',\n 'audio/mulaw': 'audio.mulaw',\n 'audio/basic': 'audio.mulaw',\n 'audio/alaw': 'audio.alaw',\n 'audio/mp4': 'audio.mp4',\n 'audio/m4a': 'audio.m4a',\n 'audio/aac': 'audio.aac',\n 'audio/webm': 'audio.webm',\n};\n\nconst sttFormats = new Set([\n 'pcm_8000',\n 'pcm_16000',\n 'pcm_22050',\n 'pcm_24000',\n 'pcm_44100',\n 'pcm_48000',\n 'ulaw_8000',\n]);\n\nexport interface ElevenFile {\n data: Uint8Array;\n mimeType: string;\n filename: string;\n}\n\ninterface HeaderModel {\n provider: string;\n headers?: Record<string, string>;\n}\n\nexport function mediaType(mimeType: string): string {\n const type = mimeType.split(';')[0]?.trim().toLowerCase();\n if (!type) throw new Error(`Unsupported audio type ${mimeType}`);\n return type;\n}\n\nexport function audioFilename(mimeType: string): string {\n const name = filenames[mediaType(mimeType)];\n if (!name) throw new Error(`Unsupported audio type ${mimeType}`);\n return name;\n}\n\nfunction parameter(mimeType: string, name: string): string | undefined {\n for (const part of mimeType.split(';').slice(1)) {\n const pieces = part.split('=');\n const key = pieces[0]?.trim().toLowerCase();\n const value = pieces[1]?.trim();\n if (key === name && value) return value;\n }\n return undefined;\n}\n\nexport function pcmS16(mimeType: string): boolean {\n const type = mediaType(mimeType);\n if (type !== 'audio/pcm' && type !== 'audio/l16') return false;\n const rate = parameter(mimeType, 'rate');\n if (rate === undefined) return true;\n return rate === '16000';\n}\n\nexport function encodeForm(file: ElevenFile, fields: Record<string, string>): ArrayBuffer {\n const chunks: Uint8Array[] = [];\n const encoder = new TextEncoder();\n const write = (value: string) => {\n chunks.push(encoder.encode(value));\n };\n for (const [name, value] of Object.entries(fields)) {\n write(`--${boundary}\\r\\nContent-Disposition: form-data; name=\"${name}\"\\r\\n\\r\\n${value}\\r\\n`);\n }\n const header =\n `--${boundary}\\r\\nContent-Disposition: form-data; name=\"file\"; filename=\"${file.filename}\"\\r\\n` +\n `Content-Type: ${file.mimeType}\\r\\n\\r\\n`;\n write(header);\n chunks.push(file.data);\n write(`\\r\\n--${boundary}--\\r\\n`);\n const body = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0));\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.length;\n }\n return body.buffer;\n}\n\nexport function formType(): string {\n return `multipart/form-data; boundary=${boundary}`;\n}\n\nfunction headerValue(\n headers: Record<string, string | null> | undefined,\n name: string\n): string | undefined {\n if (!headers) return undefined;\n const expected = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === expected && value !== null && value.trim() !== '') return value;\n }\n return undefined;\n}\n\nexport function xiHeaders(\n model: HeaderModel,\n options: ProviderRequestOptions | undefined\n): Record<string, string> {\n const headers: Record<string, string> = { ...model.headers };\n const extra = options?.headers;\n if (extra) {\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n }\n if (headerValue(headers, 'xi-api-key') === undefined) {\n const apiKey = options?.apiKey;\n if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);\n headers['xi-api-key'] = apiKey;\n }\n return headers;\n}\n\nexport function socketUrl(baseUrl: string, path: string): URL {\n const url = new URL(baseUrl);\n url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';\n url.pathname = `${url.pathname.replace(/\\/$/, '')}/${path}`;\n url.search = '';\n return url;\n}\n\nexport function outputFormatName(format: AudioFormat): string {\n if (format.encoding === 'mp3' && format.sampleRate === 44100) return 'mp3_44100_128';\n if (format.encoding === 'mp3' && format.sampleRate === 24000) return 'mp3_24000_48';\n if (format.encoding === 'mp3' && format.sampleRate === 22050) return 'mp3_22050_32';\n if (format.encoding === 'pcm16') return `pcm_${format.sampleRate}`;\n if (format.encoding === 'wav') return `wav_${format.sampleRate}`;\n if (format.encoding === 'mulaw') return 'ulaw_8000';\n if (format.encoding === 'alaw') return 'alaw_8000';\n if (format.encoding === 'opus') return 'opus_48000_128';\n throw new Error(`Unsupported speech format ${format.encoding}`);\n}\n\nexport function sttAudioFormat(format: AudioFormat): string {\n const name = format.encoding === 'mulaw' ? 'ulaw_8000' : `pcm_${format.sampleRate}`;\n if (format.encoding !== 'mulaw' && format.encoding !== 'pcm16') {\n throw new Error(`Unsupported audio format ${format.encoding}`);\n }\n if (!sttFormats.has(name)) throw new Error(`Unsupported audio format ${format.encoding}`);\n return name;\n}\n\nexport function connectSocket(url: string, headers: Record<string, string>): WebSocket {\n const Socket = WebSocket as unknown as new (\n address: string,\n init: { headers: Record<string, string> }\n ) => WebSocket;\n return new Socket(url, { headers });\n}\n\nexport function awaitOpen(\n socket: WebSocket,\n label: string,\n fail: (message: string) => void\n): Promise<void> {\n return new Promise((resolve) => {\n let opened = false;\n socket.addEventListener('open', () => {\n opened = true;\n resolve();\n });\n socket.addEventListener('error', () => {\n fail(`${label} socket failed`);\n if (!opened) resolve();\n });\n socket.addEventListener('close', () => {\n if (!opened) {\n fail(`${label} socket closed`);\n resolve();\n }\n });\n });\n}\n\nexport async function messageText(data: unknown): Promise<string> {\n if (typeof data === 'string') return data;\n if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);\n if (data instanceof Uint8Array) return new TextDecoder().decode(data);\n if (typeof Blob !== 'undefined' && data instanceof Blob) return data.text();\n throw new Error('Unsupported socket message');\n}\n\nexport function sameFormat(left: AudioFormat, right: AudioFormat): boolean {\n return (\n left.encoding === right.encoding &&\n left.sampleRate === right.sampleRate &&\n left.channels === right.channels\n );\n}\n\nfunction numberList(value: unknown): number[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const numbers: number[] = [];\n for (const item of value) {\n if (typeof item === 'number') numbers.push(item);\n }\n return numbers;\n}\n\nfunction durationMs(start: number | undefined, end: number | undefined): number {\n if (start === undefined || end === undefined) return 0;\n return (end - start) * 1000;\n}\n\nexport function alignmentFrom(value: unknown): CharAlignment | undefined {\n if (!isRecord(value)) return undefined;\n const startMs = numberList(value.charStartTimesMs);\n const durationMsList = numberList(value.charDurationsMs);\n if (startMs && durationMsList) return { startMs, durationMs: durationMsList };\n const starts = numberList(value.character_start_times_seconds);\n const ends = numberList(value.character_end_times_seconds);\n if (!starts || !ends) return undefined;\n return {\n startMs: starts.map((start) => start * 1000),\n durationMs: starts.map((start, index) => durationMs(start, ends[index])),\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ export declare function registerElevenLabs(): () => void;
@@ -0,0 +1 @@
1
+ export { registerElevenLabs } from './catalog.ts';
@@ -0,0 +1,4 @@
1
+ import type { AudioChunk } from '../audio.ts';
2
+ import type { TranscriptionModel } from '../models.ts';
3
+ import { type ListenOptions, TranscriptionEventStream } from '../transcription.ts';
4
+ export declare function listenElevenLabs(model: TranscriptionModel<string>, audio: AsyncIterable<AudioChunk>, options?: ListenOptions): TranscriptionEventStream;
@@ -0,0 +1,4 @@
1
+ import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import type { SpeechModel } from '../models.ts';
3
+ import { SpeechEventStream, type SpeechInput } from '../speech.ts';
4
+ export declare function speakElevenLabs(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): SpeechEventStream;
@@ -0,0 +1,4 @@
1
+ import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import type { TranscriptionModel } from '../models.ts';
3
+ import type { AudioInput, Transcript } from '../transcription.ts';
4
+ export declare function transcribeElevenLabs(model: TranscriptionModel<string>, input: AudioInput, options?: ProviderRequestOptions): Promise<Transcript>;
@@ -0,0 +1,26 @@
1
+ import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import type { AudioFormat, CharAlignment } from '../audio.ts';
3
+ export interface ElevenFile {
4
+ data: Uint8Array;
5
+ mimeType: string;
6
+ filename: string;
7
+ }
8
+ interface HeaderModel {
9
+ provider: string;
10
+ headers?: Record<string, string>;
11
+ }
12
+ export declare function mediaType(mimeType: string): string;
13
+ export declare function audioFilename(mimeType: string): string;
14
+ export declare function pcmS16(mimeType: string): boolean;
15
+ export declare function encodeForm(file: ElevenFile, fields: Record<string, string>): ArrayBuffer;
16
+ export declare function formType(): string;
17
+ export declare function xiHeaders(model: HeaderModel, options: ProviderRequestOptions | undefined): Record<string, string>;
18
+ export declare function socketUrl(baseUrl: string, path: string): URL;
19
+ export declare function outputFormatName(format: AudioFormat): string;
20
+ export declare function sttAudioFormat(format: AudioFormat): string;
21
+ export declare function connectSocket(url: string, headers: Record<string, string>): WebSocket;
22
+ export declare function awaitOpen(socket: WebSocket, label: string, fail: (message: string) => void): Promise<void>;
23
+ export declare function messageText(data: unknown): Promise<string>;
24
+ export declare function sameFormat(left: AudioFormat, right: AudioFormat): boolean;
25
+ export declare function alignmentFrom(value: unknown): CharAlignment | undefined;
26
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -46,6 +46,10 @@
46
46
  "types": "./dist/types/xai/index.d.ts",
47
47
  "default": "./dist/xai/index.js"
48
48
  },
49
+ "./elevenlabs": {
50
+ "types": "./dist/types/elevenlabs/index.d.ts",
51
+ "default": "./dist/elevenlabs/index.js"
52
+ },
49
53
  "./durable": {
50
54
  "types": "./dist/types/durable/index.d.ts",
51
55
  "default": "./dist/durable/index.js"