@ai-ecoverse/pi-audio 1.10.0 → 1.12.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.
@@ -0,0 +1,211 @@
1
+ import { chunkSeconds } from "../audio.js";
2
+ import {
3
+ asVoiceError,
4
+ isRecord,
5
+ numberField,
6
+ requestHeaders,
7
+ stringField
8
+ } from "../openai/wire.js";
9
+ import {
10
+ TranscriptionEventStream
11
+ } from "../transcription.js";
12
+ import { priceAudio } from "../usage.js";
13
+ import {
14
+ awaitOpen,
15
+ connectSocket,
16
+ copiedBytes,
17
+ encodingName,
18
+ messageText,
19
+ sameFormat,
20
+ socketUrl
21
+ } from "./wire.js";
22
+ function terms(options) {
23
+ const vocabulary = options?.vocabulary;
24
+ if (!vocabulary) return [];
25
+ return vocabulary;
26
+ }
27
+ function sttUrl(model, format, options) {
28
+ const url = socketUrl(model.baseUrl, "stt");
29
+ const params = new URLSearchParams();
30
+ params.set("model", model.id);
31
+ params.set("sample_rate", String(format.sampleRate));
32
+ params.set("encoding", encodingName(format.encoding));
33
+ params.set("interim_results", "true");
34
+ if (model.turnDetection === "semantic") params.set("smart_turn", "0.5");
35
+ if (model.diarization) params.set("diarize", "true");
36
+ if (options?.language) params.set("language", options.language);
37
+ for (const term of terms(options)) params.append("keyterm", term);
38
+ url.search = params.toString();
39
+ return url.toString();
40
+ }
41
+ function chosenFormat(model, chunk) {
42
+ const format = chunk === void 0 ? model.inputFormats[0] : chunk.format;
43
+ if (!format) throw new Error("No input format");
44
+ encodingName(format.encoding);
45
+ const known = model.inputFormats.some((item) => sameFormat(item, format));
46
+ if (!known) throw new Error(`Unsupported audio format ${format.encoding}`);
47
+ return format;
48
+ }
49
+ function textOr(event, key) {
50
+ const text = stringField(event, key);
51
+ if (text === void 0) return "";
52
+ return text;
53
+ }
54
+ function applyListen(event, state, push) {
55
+ const type = stringField(event, "type");
56
+ if (type === "transcript.created") {
57
+ state.ready = true;
58
+ return;
59
+ }
60
+ if (type === "transcript.partial") {
61
+ const spoken = textOr(event, "text");
62
+ if (!state.started) {
63
+ state.started = true;
64
+ const start = numberField(event, "start");
65
+ push({ type: "speech_start", at: start === void 0 ? 0 : start * 1e3 });
66
+ }
67
+ if (event.is_final === true) {
68
+ const segmentId = String(state.segment);
69
+ state.segment += 1;
70
+ state.turns.push(spoken);
71
+ push({ type: "final", segmentId, text: spoken });
72
+ if (event.speech_final === true) {
73
+ state.started = false;
74
+ push({ type: "turn_end", text: spoken, at: 0 });
75
+ }
76
+ return;
77
+ }
78
+ push({ type: "partial", segmentId: String(state.segment), text: spoken });
79
+ return;
80
+ }
81
+ if (type === "transcript.done") {
82
+ const text = stringField(event, "text");
83
+ state.doneText = text === void 0 ? state.turns.join(" ") : text;
84
+ const duration = numberField(event, "duration");
85
+ if (duration !== void 0) state.duration = duration;
86
+ return;
87
+ }
88
+ if (type === "error") {
89
+ const message = stringField(event, "message");
90
+ state.failed = true;
91
+ push({
92
+ type: "error",
93
+ reason: "error",
94
+ error: message === void 0 ? "xAI transcription failed" : message
95
+ });
96
+ }
97
+ }
98
+ function finish(model, format, state, push) {
99
+ if (state.failed || state.completed) return;
100
+ state.completed = true;
101
+ const heard = chunkSeconds({ data: new Uint8Array(state.bytes), format, timestamp: 0 });
102
+ const seconds = state.duration === void 0 ? heard : state.duration;
103
+ const text = state.doneText === void 0 ? state.turns.join(" ") : state.doneText;
104
+ push({
105
+ type: "done",
106
+ transcript: { text, usage: priceAudio(model.audioCost, { inputSeconds: seconds }) }
107
+ });
108
+ }
109
+ function listenXAI(model, audio, options) {
110
+ const stream = new TranscriptionEventStream();
111
+ void pumpListen(model, audio, options, stream);
112
+ return stream;
113
+ }
114
+ async function pumpListen(model, audio, options, stream) {
115
+ const state = {
116
+ turns: [],
117
+ segment: 0,
118
+ started: false,
119
+ bytes: 0,
120
+ opened: false,
121
+ ready: false,
122
+ clientDone: false,
123
+ failed: false,
124
+ completed: false,
125
+ duration: void 0,
126
+ doneText: void 0
127
+ };
128
+ let settled = false;
129
+ const push = (event) => {
130
+ if (settled) return;
131
+ if (event.type === "done" || event.type === "error") settled = true;
132
+ stream.push(event);
133
+ };
134
+ let socket;
135
+ const readyWait = Promise.withResolvers();
136
+ const doneWait = Promise.withResolvers();
137
+ const wake = () => {
138
+ if (state.ready || state.failed) readyWait.resolve();
139
+ if (state.failed || state.doneText !== void 0) doneWait.resolve();
140
+ };
141
+ try {
142
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
143
+ const iterator = audio[Symbol.asyncIterator]();
144
+ const first = await iterator.next();
145
+ const format = chosenFormat(model, first.done ? void 0 : first.value);
146
+ socket = connectSocket(sttUrl(model, format, options), requestHeaders(model, options));
147
+ let incoming = Promise.resolve();
148
+ socket.addEventListener("message", (event) => {
149
+ incoming = incoming.then(() => onMessage(event.data, state, push, wake));
150
+ });
151
+ socket.addEventListener("close", () => {
152
+ if (state.opened && !state.failed && !state.completed && !state.clientDone) {
153
+ state.failed = true;
154
+ push({ type: "error", reason: "error", error: "xAI transcription socket closed" });
155
+ }
156
+ wake();
157
+ if (state.clientDone) doneWait.resolve();
158
+ });
159
+ await awaitOpen(socket, "xAI transcription", (message) => {
160
+ state.failed = true;
161
+ push({ type: "error", reason: "error", error: message });
162
+ wake();
163
+ });
164
+ if (state.failed) return;
165
+ state.opened = true;
166
+ if (!state.ready) await readyWait.promise;
167
+ if (state.failed) return;
168
+ if (!first.done) sendChunk(socket, first.value, format, state);
169
+ while (!state.failed) {
170
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
171
+ const next = await iterator.next();
172
+ if (next.done) break;
173
+ sendChunk(socket, next.value, format, state);
174
+ }
175
+ if (state.failed) return;
176
+ state.clientDone = true;
177
+ socket.send(JSON.stringify({ type: "audio.done" }));
178
+ if (state.doneText === void 0) await doneWait.promise;
179
+ finish(model, format, state, push);
180
+ } catch (error) {
181
+ const voice = asVoiceError(error);
182
+ state.failed = true;
183
+ push({ type: "error", reason: voice.reason, error: voice.error });
184
+ } finally {
185
+ socket?.close();
186
+ }
187
+ }
188
+ function sendChunk(socket, chunk, format, state) {
189
+ if (!sameFormat(chunk.format, format)) {
190
+ throw new Error(`Unsupported audio format ${chunk.format.encoding}`);
191
+ }
192
+ state.bytes += chunk.data.byteLength;
193
+ socket.send(copiedBytes(chunk.data));
194
+ }
195
+ async function onMessage(data, state, push, wake) {
196
+ try {
197
+ const parsed = JSON.parse(await messageText(data));
198
+ if (!isRecord(parsed)) throw new Error("xAI transcription event was not JSON");
199
+ applyListen(parsed, state, push);
200
+ wake();
201
+ } catch (error) {
202
+ const voice = asVoiceError(error);
203
+ state.failed = true;
204
+ push({ type: "error", reason: voice.reason, error: voice.error });
205
+ wake();
206
+ }
207
+ }
208
+ export {
209
+ listenXAI
210
+ };
211
+ //# sourceMappingURL=listen.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/xai/listen.ts"],
4
+ "sourcesContent": ["import type { AudioChunk, AudioFormat } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport {\n asVoiceError,\n isRecord,\n numberField,\n requestHeaders,\n stringField,\n} from '../openai/wire.ts';\nimport {\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n awaitOpen,\n connectSocket,\n copiedBytes,\n encodingName,\n messageText,\n sameFormat,\n socketUrl,\n} from './wire.ts';\n\ninterface ListenState {\n turns: string[];\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 duration: number | undefined;\n doneText: 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 sttUrl(\n model: TranscriptionModel<string>,\n format: AudioFormat,\n options: ListenOptions | undefined\n): string {\n const url = socketUrl(model.baseUrl, 'stt');\n const params = new URLSearchParams();\n params.set('model', model.id);\n params.set('sample_rate', String(format.sampleRate));\n params.set('encoding', encodingName(format.encoding));\n params.set('interim_results', 'true');\n if (model.turnDetection === 'semantic') params.set('smart_turn', '0.5');\n if (model.diarization) params.set('diarize', 'true');\n if (options?.language) params.set('language', options.language);\n for (const term of terms(options)) params.append('keyterm', 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 encodingName(format.encoding);\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>, key: string): string {\n const text = stringField(event, key);\n if (text === undefined) return '';\n return text;\n}\n\nfunction applyListen(\n event: Record<string, unknown>,\n state: ListenState,\n push: (event: TranscriptionEvent) => void\n) {\n const type = stringField(event, 'type');\n if (type === 'transcript.created') {\n state.ready = true;\n return;\n }\n if (type === 'transcript.partial') {\n const spoken = textOr(event, 'text');\n if (!state.started) {\n state.started = true;\n const start = numberField(event, 'start');\n push({ type: 'speech_start', at: start === undefined ? 0 : start * 1000 });\n }\n if (event.is_final === true) {\n const segmentId = String(state.segment);\n state.segment += 1;\n state.turns.push(spoken);\n push({ type: 'final', segmentId, text: spoken });\n if (event.speech_final === true) {\n state.started = false;\n push({ type: 'turn_end', text: spoken, at: 0 });\n }\n return;\n }\n push({ type: 'partial', segmentId: String(state.segment), text: spoken });\n return;\n }\n if (type === 'transcript.done') {\n const text = stringField(event, 'text');\n state.doneText = text === undefined ? state.turns.join(' ') : text;\n const duration = numberField(event, 'duration');\n if (duration !== undefined) state.duration = duration;\n return;\n }\n if (type === 'error') {\n const message = stringField(event, 'message');\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'xAI 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 const seconds = state.duration === undefined ? heard : state.duration;\n const text = state.doneText === undefined ? state.turns.join(' ') : state.doneText;\n push({\n type: 'done',\n transcript: { text, usage: priceAudio(model.audioCost, { inputSeconds: seconds }) },\n });\n}\n\nexport function listenXAI(\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 segment: 0,\n started: false,\n bytes: 0,\n opened: false,\n ready: false,\n clientDone: false,\n failed: false,\n completed: false,\n duration: undefined,\n doneText: 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 = () => {\n if (state.ready || state.failed) readyWait.resolve();\n if (state.failed || state.doneText !== undefined) 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), requestHeaders(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: 'xAI transcription socket closed' });\n }\n wake();\n if (state.clientDone) doneWait.resolve();\n });\n await awaitOpen(socket, 'xAI transcription', (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 if (!state.ready) await readyWait.promise;\n if (state.failed) return;\n if (!first.done) sendChunk(socket, first.value, format, state);\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);\n }\n if (state.failed) return;\n state.clientDone = true;\n socket.send(JSON.stringify({ type: 'audio.done' }));\n if (state.doneText === undefined) 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(socket: WebSocket, chunk: AudioChunk, format: AudioFormat, state: ListenState) {\n if (!sameFormat(chunk.format, format)) {\n throw new Error(`Unsupported audio format ${chunk.format.encoding}`);\n }\n state.bytes += chunk.data.byteLength;\n socket.send(copiedBytes(chunk.data));\n}\n\nasync function onMessage(\n data: unknown,\n state: ListenState,\n push: (event: TranscriptionEvent) => void,\n wake: () => void\n) {\n try {\n const parsed = JSON.parse(await messageText(data)) as unknown;\n if (!isRecord(parsed)) throw new Error('xAI transcription event was not JSON');\n applyListen(parsed, state, push);\n 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"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,208 @@
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
+ requestHeaders,
10
+ stringField,
11
+ voiceFetch
12
+ } from "../openai/wire.js";
13
+ import { SpeechEventStream } from "../speech.js";
14
+ import { priceAudio } from "../usage.js";
15
+ import { awaitOpen, codecName, connectSocket, messageText, sameFormat, socketUrl } from "./wire.js";
16
+ function languageOf(language) {
17
+ if (language === void 0 || language.trim() === "") return "auto";
18
+ return language;
19
+ }
20
+ function resolveFormat(model, requested) {
21
+ const fallback = model.outputFormats[0];
22
+ if (!fallback) throw new Error("No output format");
23
+ if (!requested) return fallback;
24
+ const match = model.outputFormats.find((format) => sameFormat(format, requested));
25
+ if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
26
+ return match;
27
+ }
28
+ function outputFormat(format) {
29
+ const body = {
30
+ codec: codecName(format.encoding),
31
+ sample_rate: format.sampleRate
32
+ };
33
+ if (format.encoding === "mp3") body.bit_rate = 128e3;
34
+ return body;
35
+ }
36
+ function ttsUrl(baseUrl, voice, language, format) {
37
+ const url = socketUrl(baseUrl, "tts");
38
+ const params = new URLSearchParams();
39
+ params.set("language", language);
40
+ params.set("voice", voice);
41
+ params.set("codec", codecName(format.encoding));
42
+ params.set("sample_rate", String(format.sampleRate));
43
+ if (format.encoding === "mp3") params.set("bit_rate", "128000");
44
+ url.search = params.toString();
45
+ return url.toString();
46
+ }
47
+ function speakXAI(model, input, options) {
48
+ const stream = new SpeechEventStream();
49
+ void pumpSpeech(model, input, options, stream);
50
+ return stream;
51
+ }
52
+ async function pumpSpeech(model, input, options, stream) {
53
+ try {
54
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
55
+ const format = resolveFormat(model, input.format);
56
+ const language = languageOf(input.language);
57
+ if (typeof input.text === "string") {
58
+ await postSpeech(model, input.text, input.voice, language, format, options, stream);
59
+ return;
60
+ }
61
+ await streamSpeech(model, input.text, input.voice, language, format, options, stream);
62
+ } catch (error) {
63
+ const voice = asVoiceError(error);
64
+ stream.push({ type: "error", reason: voice.reason, error: voice.error });
65
+ }
66
+ }
67
+ async function postSpeech(model, text, voice, language, format, options, stream) {
68
+ const payload = {
69
+ text,
70
+ voice_id: voice,
71
+ language,
72
+ output_format: outputFormat(format)
73
+ };
74
+ const body = await applyPayload(payload, model, options);
75
+ if (!isRecord(body)) throw new Error("Speech payload was not JSON");
76
+ const response = await voiceFetch(
77
+ model,
78
+ options,
79
+ endpoint(model.baseUrl, "tts"),
80
+ JSON.stringify(body),
81
+ "application/json"
82
+ );
83
+ const data = new Uint8Array(await response.arrayBuffer());
84
+ const seconds = chunkSeconds({ data, format, timestamp: 0 });
85
+ const meters = { characters: text.length };
86
+ if (seconds > 0) meters.outputSeconds = seconds;
87
+ stream.push({ type: "start", format });
88
+ stream.push({
89
+ type: "audio",
90
+ chunk: { data, format, timestamp: 0 },
91
+ span: { itemId: model.id, start: 0, end: text.length }
92
+ });
93
+ stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
94
+ }
95
+ async function streamSpeech(model, text, voice, language, format, options, stream) {
96
+ const state = {
97
+ opened: false,
98
+ started: false,
99
+ done: false,
100
+ failed: false,
101
+ outputSeconds: 0,
102
+ characters: 0
103
+ };
104
+ let settled = false;
105
+ const push = (event) => {
106
+ if (settled) return;
107
+ if (event.type === "done" || event.type === "error") settled = true;
108
+ stream.push(event);
109
+ };
110
+ const doneWait = Promise.withResolvers();
111
+ const wake = () => {
112
+ doneWait.resolve();
113
+ };
114
+ const headers = requestHeaders(model, options);
115
+ const socket = connectSocket(ttsUrl(model.baseUrl, voice, language, format), headers);
116
+ let incoming = Promise.resolve();
117
+ socket.addEventListener("message", (event) => {
118
+ incoming = incoming.then(() => onMessage(event.data, model.id, format, state, push, wake));
119
+ });
120
+ socket.addEventListener("close", () => {
121
+ if (state.opened && !state.done && !state.failed) {
122
+ state.failed = true;
123
+ push({ type: "error", reason: "error", error: "xAI speech socket closed" });
124
+ }
125
+ wake();
126
+ });
127
+ try {
128
+ await awaitOpen(socket, "xAI speech", (message) => {
129
+ state.failed = true;
130
+ push({ type: "error", reason: "error", error: message });
131
+ wake();
132
+ });
133
+ if (state.failed) return;
134
+ state.opened = true;
135
+ state.characters = await writeText(socket, text, options?.signal);
136
+ if (state.failed) return;
137
+ socket.send(JSON.stringify({ type: "text.done" }));
138
+ if (!state.done) await doneWait.promise;
139
+ if (state.failed) return;
140
+ const meters = { characters: state.characters };
141
+ if (state.outputSeconds > 0) meters.outputSeconds = state.outputSeconds;
142
+ push({ type: "done", usage: priceAudio(model.audioCost, meters) });
143
+ } catch (error) {
144
+ const voice2 = asVoiceError(error);
145
+ state.failed = true;
146
+ push({ type: "error", reason: voice2.reason, error: voice2.error });
147
+ } finally {
148
+ socket.close();
149
+ }
150
+ }
151
+ async function writeText(socket, text, signal) {
152
+ let combined = "";
153
+ for await (const part of text) {
154
+ if (signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
155
+ combined += part;
156
+ if (part !== "") socket.send(JSON.stringify({ type: "text.delta", delta: part }));
157
+ }
158
+ return combined.length;
159
+ }
160
+ async function onMessage(data, itemId, format, state, push, wake) {
161
+ try {
162
+ const parsed = JSON.parse(await messageText(data));
163
+ if (!isRecord(parsed)) throw new Error("xAI speech event was not JSON");
164
+ applySpeech(parsed, itemId, format, state, push);
165
+ if (state.done || state.failed) wake();
166
+ } catch (error) {
167
+ const voice = asVoiceError(error);
168
+ state.failed = true;
169
+ push({ type: "error", reason: voice.reason, error: voice.error });
170
+ wake();
171
+ }
172
+ }
173
+ function applySpeech(event, itemId, format, state, push) {
174
+ const type = stringField(event, "type");
175
+ if (type === "audio.delta") {
176
+ const delta = stringField(event, "delta");
177
+ const payload = delta === void 0 ? "" : delta;
178
+ const data = decodeAudioBase64(payload);
179
+ if (!state.started) {
180
+ state.started = true;
181
+ push({ type: "start", format });
182
+ }
183
+ state.outputSeconds += chunkSeconds({ data, format, timestamp: 0 });
184
+ push({
185
+ type: "audio",
186
+ chunk: { data, format, timestamp: 0 },
187
+ span: { itemId, start: 0, end: state.characters }
188
+ });
189
+ return;
190
+ }
191
+ if (type === "audio.done") {
192
+ state.done = true;
193
+ return;
194
+ }
195
+ if (type === "error") {
196
+ const message = stringField(event, "message");
197
+ state.failed = true;
198
+ push({
199
+ type: "error",
200
+ reason: "error",
201
+ error: message === void 0 ? "xAI speech failed" : message
202
+ });
203
+ }
204
+ }
205
+ export {
206
+ speakXAI
207
+ };
208
+ //# sourceMappingURL=speak.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/xai/speak.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } 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 requestHeaders,\n stringField,\n voiceFetch,\n} from '../openai/wire.ts';\nimport { type SpeechEvent, SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport { awaitOpen, codecName, connectSocket, messageText, sameFormat, socketUrl } 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 languageOf(language: string | undefined): string {\n if (language === undefined || language.trim() === '') return 'auto';\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 outputFormat(format: AudioFormat): Record<string, number | string> {\n const body: Record<string, number | string> = {\n codec: codecName(format.encoding),\n sample_rate: format.sampleRate,\n };\n if (format.encoding === 'mp3') body.bit_rate = 128000;\n return body;\n}\n\nfunction ttsUrl(baseUrl: string, voice: string, language: string, format: AudioFormat): string {\n const url = socketUrl(baseUrl, 'tts');\n const params = new URLSearchParams();\n params.set('language', language);\n params.set('voice', voice);\n params.set('codec', codecName(format.encoding));\n params.set('sample_rate', String(format.sampleRate));\n if (format.encoding === 'mp3') params.set('bit_rate', '128000');\n url.search = params.toString();\n return url.toString();\n}\n\nexport function speakXAI(\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 const language = languageOf(input.language);\n if (typeof input.text === 'string') {\n await postSpeech(model, input.text, input.voice, language, format, options, stream);\n return;\n }\n await streamSpeech(model, input.text, input.voice, 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,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const payload = {\n text,\n voice_id: voice,\n language,\n output_format: outputFormat(format),\n };\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body)) throw new Error('Speech payload was not JSON');\n const response = await voiceFetch(\n model,\n options,\n endpoint(model.baseUrl, 'tts'),\n JSON.stringify(body),\n 'application/json'\n );\n const data = new Uint8Array(await response.arrayBuffer());\n const seconds = chunkSeconds({ data, format, timestamp: 0 });\n const meters: AudioCounters = { characters: text.length };\n if (seconds > 0) meters.outputSeconds = seconds;\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 });\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 language: 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 headers = requestHeaders(model, options);\n const socket = connectSocket(ttsUrl(model.baseUrl, voice, language, format), headers);\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: 'xAI speech socket closed' });\n }\n wake();\n });\n try {\n await awaitOpen(socket, 'xAI 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({ type: 'text.done' }));\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 for await (const part of text) {\n if (signal?.aborted) throw new VoiceRequestError('aborted', 'aborted');\n combined += part;\n if (part !== '') socket.send(JSON.stringify({ type: 'text.delta', delta: part }));\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('xAI 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 applySpeech(\n event: Record<string, unknown>,\n itemId: string,\n format: AudioFormat,\n state: SpeechState,\n push: (event: SpeechEvent) => void\n) {\n const type = stringField(event, 'type');\n if (type === 'audio.delta') {\n const delta = stringField(event, 'delta');\n const payload = delta === undefined ? '' : delta;\n const data = decodeAudioBase64(payload);\n if (!state.started) {\n state.started = true;\n push({ type: 'start', format });\n }\n state.outputSeconds += chunkSeconds({ data, format, timestamp: 0 });\n push({\n type: 'audio',\n chunk: { data, format, timestamp: 0 },\n span: { itemId, start: 0, end: state.characters },\n });\n return;\n }\n if (type === 'audio.done') {\n state.done = true;\n return;\n }\n if (type === 'error') {\n const message = stringField(event, 'message');\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'xAI speech failed' : message,\n });\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["voice"]
7
+ }
@@ -0,0 +1,113 @@
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
+ voiceFetch
13
+ } from "../openai/wire.js";
14
+ import { priceAudio } from "../usage.js";
15
+ import { audioFilename, encodeForm, formType, rawFields } from "./wire.js";
16
+ function durationOf(record) {
17
+ const duration = numberField(record, "duration");
18
+ if (duration === void 0) return 0;
19
+ return duration;
20
+ }
21
+ function textOf(record, key) {
22
+ const text = stringField(record, key);
23
+ if (text === void 0) return "";
24
+ return text;
25
+ }
26
+ function secondsOf(record, key) {
27
+ const value = numberField(record, key);
28
+ if (value === void 0) return 0;
29
+ return value;
30
+ }
31
+ function speakerOf(record) {
32
+ const value = record.speaker;
33
+ if (typeof value === "number") return String(value);
34
+ if (typeof value === "string") return value;
35
+ return void 0;
36
+ }
37
+ function wordFrom(word) {
38
+ const speaker = speakerOf(word);
39
+ const segment = {
40
+ text: textOf(word, "text"),
41
+ start: secondsOf(word, "start"),
42
+ end: secondsOf(word, "end")
43
+ };
44
+ if (speaker !== void 0) segment.speaker = speaker;
45
+ return segment;
46
+ }
47
+ function transcriptFrom(body, model) {
48
+ if (!isRecord(body)) throw new Error("Transcription response had no text");
49
+ const text = stringField(body, "text");
50
+ if (text === void 0) throw new Error("Transcription response had no text");
51
+ const language = stringField(body, "language");
52
+ const words = Array.isArray(body.words) ? body.words.filter(isRecord).map(wordFrom) : void 0;
53
+ return {
54
+ text,
55
+ ...language !== void 0 ? { language } : {},
56
+ ...words !== void 0 ? { segments: words } : {},
57
+ usage: priceAudio(model.audioCost, { inputSeconds: durationOf(body) })
58
+ };
59
+ }
60
+ function fileOf(body) {
61
+ const file = body.file;
62
+ if (!isRecord(file) || !(file.data instanceof Uint8Array) || typeof file.mimeType !== "string") {
63
+ throw new Error("Transcription payload was not a file");
64
+ }
65
+ const named = typeof file.filename === "string" ? file.filename : audioFilename(file.mimeType);
66
+ return { data: file.data, mimeType: file.mimeType, filename: named };
67
+ }
68
+ function fieldStrings(body) {
69
+ const fields = {};
70
+ for (const [key, value] of Object.entries(body)) {
71
+ if (key === "file") continue;
72
+ if (typeof value !== "string") continue;
73
+ fields[key] = value;
74
+ }
75
+ return fields;
76
+ }
77
+ async function transcribeXAI(model, input, options) {
78
+ try {
79
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
80
+ const fetchImpl = options?.fetch ?? globalThis.fetch;
81
+ const loaded = await readAudio(input, fetchImpl, requestSignal(options));
82
+ const filename = audioFilename(loaded.mimeType);
83
+ const payload = {
84
+ model: model.id,
85
+ file: { data: loaded.data, mimeType: loaded.mimeType, filename }
86
+ };
87
+ if (model.diarization) payload.diarize = "true";
88
+ const raw = rawFields(loaded.mimeType, model);
89
+ if (raw) {
90
+ payload.audio_format = raw.audio_format;
91
+ payload.sample_rate = raw.sample_rate;
92
+ }
93
+ const body = await applyPayload(payload, model, options);
94
+ if (!isRecord(body)) throw new Error("Transcription payload was not a file");
95
+ const file = fileOf(body);
96
+ const encoded = encodeForm(file, fieldStrings(body));
97
+ const response = await voiceFetch(
98
+ model,
99
+ options,
100
+ endpoint(model.baseUrl, "stt"),
101
+ encoded,
102
+ formType()
103
+ );
104
+ return transcriptFrom(await readJson(response), model);
105
+ } catch (error) {
106
+ const voice = asVoiceError(error);
107
+ throw new VoiceRequestError(voice.reason, voice.error);
108
+ }
109
+ }
110
+ export {
111
+ transcribeXAI
112
+ };
113
+ //# sourceMappingURL=transcribe.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/xai/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 voiceFetch,\n} from '../openai/wire.ts';\nimport type { AudioInput, Transcript, TranscriptSegment } from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport { audioFilename, encodeForm, formType, rawFields, type XaiFile } from './wire.ts';\n\nfunction durationOf(record: Record<string, unknown>): number {\n const duration = numberField(record, 'duration');\n if (duration === undefined) return 0;\n return duration;\n}\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 speakerOf(record: Record<string, unknown>): string | undefined {\n const value = record.speaker;\n if (typeof value === 'number') return String(value);\n if (typeof value === 'string') return value;\n return undefined;\n}\n\nfunction wordFrom(word: Record<string, unknown>): TranscriptSegment {\n const speaker = speakerOf(word);\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 transcriptFrom(body: unknown, model: TranscriptionModel<string>): Transcript {\n if (!isRecord(body)) throw new Error('Transcription response had no text');\n const text = stringField(body, 'text');\n if (text === undefined) throw new Error('Transcription response had no text');\n const language = stringField(body, 'language');\n const words = Array.isArray(body.words) ? body.words.filter(isRecord).map(wordFrom) : undefined;\n return {\n text,\n ...(language !== undefined ? { language } : {}),\n ...(words !== undefined ? { segments: words } : {}),\n usage: priceAudio(model.audioCost, { inputSeconds: durationOf(body) }),\n };\n}\n\nfunction fileOf(body: Record<string, unknown>): XaiFile {\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\nexport async function transcribeXAI(\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: model.id,\n file: { data: loaded.data, mimeType: loaded.mimeType, filename },\n };\n if (model.diarization) payload.diarize = 'true';\n const raw = rawFields(loaded.mimeType, model);\n if (raw) {\n payload.audio_format = raw.audio_format;\n payload.sample_rate = raw.sample_rate;\n }\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 voiceFetch(\n model,\n options,\n endpoint(model.baseUrl, 'stt'),\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
+ }