@ai-ecoverse/pi-audio 1.4.0 → 1.6.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 +8 -0
- package/dist/audio.js +25 -0
- package/dist/audio.js.map +3 -3
- package/dist/faux/realtime.js +57 -12
- package/dist/faux/realtime.js.map +2 -2
- package/dist/index.js +5 -0
- package/dist/index.js.map +2 -2
- package/dist/openai/catalog.js +126 -0
- package/dist/openai/catalog.js.map +7 -0
- package/dist/openai/index.js +5 -0
- package/dist/openai/index.js.map +7 -0
- package/dist/openai/listen.js +200 -0
- package/dist/openai/listen.js.map +7 -0
- package/dist/openai/speak.js +91 -0
- package/dist/openai/speak.js.map +7 -0
- package/dist/openai/transcribe.js +112 -0
- package/dist/openai/transcribe.js.map +7 -0
- package/dist/openai/wire.js +201 -0
- package/dist/openai/wire.js.map +7 -0
- package/dist/realtime.js +93 -0
- package/dist/realtime.js.map +3 -3
- package/dist/registry.js +9 -0
- package/dist/registry.js.map +2 -2
- package/dist/types/audio.d.ts +2 -0
- package/dist/types/faux/realtime.d.ts +6 -2
- package/dist/types/index.d.ts +2 -1
- package/dist/types/openai/catalog.d.ts +1 -0
- package/dist/types/openai/index.d.ts +1 -0
- package/dist/types/openai/listen.d.ts +4 -0
- package/dist/types/openai/speak.d.ts +4 -0
- package/dist/types/openai/transcribe.d.ts +4 -0
- package/dist/types/openai/wire.d.ts +52 -0
- package/dist/types/realtime.d.ts +38 -2
- package/package.json +5 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { encodeAudioBase64 } from "../convert.js";
|
|
2
|
+
import {
|
|
3
|
+
TranscriptionEventStream
|
|
4
|
+
} from "../transcription.js";
|
|
5
|
+
import { priceAudio } from "../usage.js";
|
|
6
|
+
import {
|
|
7
|
+
asVoiceError,
|
|
8
|
+
isRecord,
|
|
9
|
+
numberField,
|
|
10
|
+
realtimeUrl,
|
|
11
|
+
requestHeaders,
|
|
12
|
+
stringField
|
|
13
|
+
} from "./wire.js";
|
|
14
|
+
function turnDetection(model) {
|
|
15
|
+
if (model.turnDetection === "vad") return { type: "server_vad" };
|
|
16
|
+
if (model.turnDetection === "semantic") return { type: "semantic_vad" };
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function sessionBody(model, options, format) {
|
|
20
|
+
const transcription = { model: model.id };
|
|
21
|
+
if (options?.language) {
|
|
22
|
+
transcription.language = options.language;
|
|
23
|
+
transcription.languages = [options.language];
|
|
24
|
+
}
|
|
25
|
+
if (options?.vocabulary && options.vocabulary.length > 0)
|
|
26
|
+
transcription.keywords = options.vocabulary;
|
|
27
|
+
return {
|
|
28
|
+
type: "session.update",
|
|
29
|
+
session: {
|
|
30
|
+
type: "transcription",
|
|
31
|
+
audio: {
|
|
32
|
+
input: {
|
|
33
|
+
format: { type: "audio/pcm", rate: format.sampleRate },
|
|
34
|
+
transcription,
|
|
35
|
+
turn_detection: turnDetection(model)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function matches(chunk, format) {
|
|
42
|
+
return chunk.format.encoding === format.encoding && chunk.format.sampleRate === format.sampleRate && chunk.format.channels === format.channels;
|
|
43
|
+
}
|
|
44
|
+
async function messageText(data) {
|
|
45
|
+
if (typeof data === "string") return data;
|
|
46
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
47
|
+
if (data instanceof Uint8Array) return new TextDecoder().decode(data);
|
|
48
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return data.text();
|
|
49
|
+
throw new Error("Unsupported realtime message");
|
|
50
|
+
}
|
|
51
|
+
function applyEvent(event, state, push) {
|
|
52
|
+
const type = stringField(event, "type");
|
|
53
|
+
if (type === "input_audio_buffer.speech_started") {
|
|
54
|
+
const at = numberField(event, "audio_start_ms");
|
|
55
|
+
push({ type: "speech_start", at: at === void 0 ? 0 : at });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (type === "conversation.item.input_audio_transcription.delta") {
|
|
59
|
+
const item = stringField(event, "item_id");
|
|
60
|
+
state.segmentId = item === void 0 ? "0" : item;
|
|
61
|
+
const delta = stringField(event, "delta");
|
|
62
|
+
state.partial += delta === void 0 ? "" : delta;
|
|
63
|
+
push({ type: "partial", segmentId: state.segmentId, text: state.partial });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (type === "conversation.item.input_audio_transcription.completed") {
|
|
67
|
+
const transcript = stringField(event, "transcript");
|
|
68
|
+
const text = transcript === void 0 ? state.partial : transcript;
|
|
69
|
+
state.turns.push(text);
|
|
70
|
+
state.partial = "";
|
|
71
|
+
push({ type: "final", segmentId: state.segmentId, text });
|
|
72
|
+
push({ type: "turn_end", text, at: 0 });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (type === "error") {
|
|
76
|
+
const detail = event.error;
|
|
77
|
+
const message = isRecord(detail) ? stringField(detail, "message") : void 0;
|
|
78
|
+
state.failed = true;
|
|
79
|
+
push({
|
|
80
|
+
type: "error",
|
|
81
|
+
reason: "error",
|
|
82
|
+
error: message === void 0 ? "Realtime transcription failed" : message
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function listenOpenAI(model, audio, options) {
|
|
87
|
+
const stream = new TranscriptionEventStream();
|
|
88
|
+
void pumpListen(model, audio, options, stream);
|
|
89
|
+
return stream;
|
|
90
|
+
}
|
|
91
|
+
function openSocket(socket, fail) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
let opened = false;
|
|
94
|
+
socket.addEventListener("open", () => {
|
|
95
|
+
opened = true;
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
socket.addEventListener("error", () => {
|
|
99
|
+
fail("OpenAI realtime transcription socket failed");
|
|
100
|
+
if (!opened) resolve();
|
|
101
|
+
});
|
|
102
|
+
socket.addEventListener("close", () => {
|
|
103
|
+
if (!opened) {
|
|
104
|
+
fail("OpenAI realtime transcription socket closed");
|
|
105
|
+
resolve();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async function pumpListen(model, audio, options, stream) {
|
|
111
|
+
const state = {
|
|
112
|
+
turns: [],
|
|
113
|
+
partial: "",
|
|
114
|
+
segmentId: "0",
|
|
115
|
+
bytes: 0,
|
|
116
|
+
audioDone: false,
|
|
117
|
+
failed: false
|
|
118
|
+
};
|
|
119
|
+
let finished = false;
|
|
120
|
+
const push = (event) => {
|
|
121
|
+
if (finished) return;
|
|
122
|
+
if (event.type === "done" || event.type === "error") finished = true;
|
|
123
|
+
stream.push(event);
|
|
124
|
+
};
|
|
125
|
+
let socket;
|
|
126
|
+
let wake;
|
|
127
|
+
const wait = new Promise((resolve) => {
|
|
128
|
+
wake = resolve;
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
132
|
+
const format = model.inputFormats[0];
|
|
133
|
+
if (!format) throw new Error("No input format");
|
|
134
|
+
requestHeaders(model, options);
|
|
135
|
+
const apiKey = options?.apiKey;
|
|
136
|
+
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
|
|
137
|
+
socket = new WebSocket(realtimeUrl(model.baseUrl), [
|
|
138
|
+
"realtime",
|
|
139
|
+
`openai-insecure-api-key.${apiKey}`
|
|
140
|
+
]);
|
|
141
|
+
let incoming = Promise.resolve();
|
|
142
|
+
socket.addEventListener("message", (event) => {
|
|
143
|
+
incoming = incoming.then(() => onMessage(event.data, state, push, wake));
|
|
144
|
+
});
|
|
145
|
+
socket.addEventListener("close", () => wake?.());
|
|
146
|
+
await openSocket(socket, (message) => {
|
|
147
|
+
push({ type: "error", reason: "error", error: message });
|
|
148
|
+
wake?.();
|
|
149
|
+
});
|
|
150
|
+
if (finished) return;
|
|
151
|
+
socket.send(JSON.stringify(sessionBody(model, options, format)));
|
|
152
|
+
for await (const chunk of audio) {
|
|
153
|
+
if (finished) return;
|
|
154
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
155
|
+
if (!matches(chunk, format))
|
|
156
|
+
throw new Error(`Unsupported audio format ${chunk.format.encoding}`);
|
|
157
|
+
state.bytes += chunk.data.length;
|
|
158
|
+
socket.send(
|
|
159
|
+
JSON.stringify({ type: "input_audio_buffer.append", audio: encodeAudioBase64(chunk.data) })
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
state.audioDone = true;
|
|
163
|
+
if (model.turnDetection === "none") {
|
|
164
|
+
socket.send(JSON.stringify({ type: "input_audio_buffer.commit" }));
|
|
165
|
+
}
|
|
166
|
+
if (!finished && (model.turnDetection === "none" || state.turns.length === 0)) await wait;
|
|
167
|
+
if (finished) return;
|
|
168
|
+
const width = format.sampleRate * format.channels * 2;
|
|
169
|
+
const inputSeconds = width > 0 ? state.bytes / width : 0;
|
|
170
|
+
push({
|
|
171
|
+
type: "done",
|
|
172
|
+
transcript: {
|
|
173
|
+
text: state.turns.join(" "),
|
|
174
|
+
usage: priceAudio(model.audioCost, { inputSeconds })
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
} catch (error) {
|
|
178
|
+
const voice = asVoiceError(error);
|
|
179
|
+
push({ type: "error", reason: voice.reason, error: voice.error });
|
|
180
|
+
} finally {
|
|
181
|
+
socket?.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function onMessage(data, state, push, wake) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(await messageText(data));
|
|
187
|
+
if (!isRecord(parsed)) throw new Error("Realtime transcription event was not JSON");
|
|
188
|
+
applyEvent(parsed, state, push);
|
|
189
|
+
if (state.failed || state.audioDone && state.turns.length > 0) wake?.();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const voice = asVoiceError(error);
|
|
192
|
+
state.failed = true;
|
|
193
|
+
push({ type: "error", reason: voice.reason, error: voice.error });
|
|
194
|
+
wake?.();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export {
|
|
198
|
+
listenOpenAI
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=listen.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/openai/listen.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioChunk, AudioFormat } from '../audio.ts';\nimport { encodeAudioBase64 } from '../convert.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport {\n type ListenOptions,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n asVoiceError,\n isRecord,\n numberField,\n realtimeUrl,\n requestHeaders,\n stringField,\n} from './wire.ts';\n\ninterface TurnState {\n turns: string[];\n partial: string;\n segmentId: string;\n bytes: number;\n audioDone: boolean;\n failed: boolean;\n}\n\nfunction turnDetection(model: TranscriptionModel<string>): Record<string, unknown> | null {\n if (model.turnDetection === 'vad') return { type: 'server_vad' };\n if (model.turnDetection === 'semantic') return { type: 'semantic_vad' };\n return null;\n}\n\nfunction sessionBody(\n model: TranscriptionModel<string>,\n options: ListenOptions | undefined,\n format: AudioFormat\n) {\n const transcription: Record<string, unknown> = { model: model.id };\n if (options?.language) {\n transcription.language = options.language;\n transcription.languages = [options.language];\n }\n if (options?.vocabulary && options.vocabulary.length > 0)\n transcription.keywords = options.vocabulary;\n return {\n type: 'session.update',\n session: {\n type: 'transcription',\n audio: {\n input: {\n format: { type: 'audio/pcm', rate: format.sampleRate },\n transcription,\n turn_detection: turnDetection(model),\n },\n },\n },\n };\n}\n\nfunction matches(chunk: AudioChunk, format: AudioFormat): boolean {\n return (\n chunk.format.encoding === format.encoding &&\n chunk.format.sampleRate === format.sampleRate &&\n chunk.format.channels === format.channels\n );\n}\n\nasync 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 realtime message');\n}\n\nfunction applyEvent(\n event: Record<string, unknown>,\n state: TurnState,\n push: (event: TranscriptionEvent) => void\n) {\n const type = stringField(event, 'type');\n if (type === 'input_audio_buffer.speech_started') {\n const at = numberField(event, 'audio_start_ms');\n push({ type: 'speech_start', at: at === undefined ? 0 : at });\n return;\n }\n if (type === 'conversation.item.input_audio_transcription.delta') {\n const item = stringField(event, 'item_id');\n state.segmentId = item === undefined ? '0' : item;\n const delta = stringField(event, 'delta');\n state.partial += delta === undefined ? '' : delta;\n push({ type: 'partial', segmentId: state.segmentId, text: state.partial });\n return;\n }\n if (type === 'conversation.item.input_audio_transcription.completed') {\n const transcript = stringField(event, 'transcript');\n const text = transcript === undefined ? state.partial : transcript;\n state.turns.push(text);\n state.partial = '';\n push({ type: 'final', segmentId: state.segmentId, text });\n push({ type: 'turn_end', text, at: 0 });\n return;\n }\n if (type === 'error') {\n const detail = event.error;\n const message = isRecord(detail) ? stringField(detail, 'message') : undefined;\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'Realtime transcription failed' : message,\n });\n }\n}\n\nexport function listenOpenAI(\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\nfunction openSocket(socket: WebSocket, fail: (message: string) => void): 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('OpenAI realtime transcription socket failed');\n if (!opened) resolve();\n });\n socket.addEventListener('close', () => {\n if (!opened) {\n fail('OpenAI realtime transcription socket closed');\n resolve();\n }\n });\n });\n}\n\nasync function pumpListen(\n model: TranscriptionModel<string>,\n audio: AsyncIterable<AudioChunk>,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream\n) {\n const state: TurnState = {\n turns: [],\n partial: '',\n segmentId: '0',\n bytes: 0,\n audioDone: false,\n failed: false,\n };\n let finished = false;\n const push = (event: TranscriptionEvent) => {\n if (finished) return;\n if (event.type === 'done' || event.type === 'error') finished = true;\n stream.push(event);\n };\n let socket: WebSocket | undefined;\n let wake: (() => void) | undefined;\n const wait = new Promise<void>((resolve) => {\n wake = resolve;\n });\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = model.inputFormats[0];\n if (!format) throw new Error('No input format');\n requestHeaders(model, options);\n const apiKey = options?.apiKey;\n if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);\n socket = new WebSocket(realtimeUrl(model.baseUrl), [\n 'realtime',\n `openai-insecure-api-key.${apiKey}`,\n ]);\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', () => wake?.());\n await openSocket(socket, (message) => {\n push({ type: 'error', reason: 'error', error: message });\n wake?.();\n });\n if (finished) return;\n socket.send(JSON.stringify(sessionBody(model, options, format)));\n for await (const chunk of audio) {\n if (finished) return;\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n if (!matches(chunk, format))\n throw new Error(`Unsupported audio format ${chunk.format.encoding}`);\n state.bytes += chunk.data.length;\n socket.send(\n JSON.stringify({ type: 'input_audio_buffer.append', audio: encodeAudioBase64(chunk.data) })\n );\n }\n state.audioDone = true;\n if (model.turnDetection === 'none') {\n socket.send(JSON.stringify({ type: 'input_audio_buffer.commit' }));\n }\n if (!finished && (model.turnDetection === 'none' || state.turns.length === 0)) await wait;\n if (finished) return;\n const width = format.sampleRate * format.channels * 2;\n const inputSeconds = width > 0 ? state.bytes / width : 0;\n push({\n type: 'done',\n transcript: {\n text: state.turns.join(' '),\n usage: priceAudio(model.audioCost, { inputSeconds }),\n },\n });\n } catch (error) {\n const voice = asVoiceError(error);\n push({ type: 'error', reason: voice.reason, error: voice.error });\n } finally {\n socket?.close();\n }\n}\n\nasync function onMessage(\n data: unknown,\n state: TurnState,\n push: (event: TranscriptionEvent) => void,\n wake: (() => void) | undefined\n) {\n try {\n const parsed = JSON.parse(await messageText(data)) as unknown;\n if (!isRecord(parsed)) throw new Error('Realtime transcription event was not JSON');\n applyEvent(parsed, state, push);\n if (state.failed || (state.audioDone && state.turns.length > 0)) 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,91 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
2
|
+
import { SpeechEventStream } from "../speech.js";
|
|
3
|
+
import { priceAudio } from "../usage.js";
|
|
4
|
+
import {
|
|
5
|
+
applyPayload,
|
|
6
|
+
asVoiceError,
|
|
7
|
+
endpoint,
|
|
8
|
+
isOpenRouter,
|
|
9
|
+
isRecord,
|
|
10
|
+
voiceFetch
|
|
11
|
+
} from "./wire.js";
|
|
12
|
+
function sameFormat(left, right) {
|
|
13
|
+
return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
|
|
14
|
+
}
|
|
15
|
+
function resolveSpeechFormat(model, requested) {
|
|
16
|
+
const fallback = model.outputFormats[0];
|
|
17
|
+
if (!fallback) throw new Error("No output format");
|
|
18
|
+
if (!requested) return fallback;
|
|
19
|
+
const match = model.outputFormats.find((format) => sameFormat(format, requested));
|
|
20
|
+
if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
|
|
21
|
+
return match;
|
|
22
|
+
}
|
|
23
|
+
function speechResponseFormat(encoding, openRouter) {
|
|
24
|
+
if (encoding === "pcm16") return "pcm";
|
|
25
|
+
if (encoding === "mp3") return "mp3";
|
|
26
|
+
if (openRouter) throw new Error(`OpenRouter speech cannot return ${encoding}`);
|
|
27
|
+
if (encoding === "wav" || encoding === "opus" || encoding === "flac") return encoding;
|
|
28
|
+
throw new Error(`OpenAI speech cannot return ${encoding}`);
|
|
29
|
+
}
|
|
30
|
+
function outputSeconds(format, byteLength) {
|
|
31
|
+
if (format.encoding !== "pcm16") return void 0;
|
|
32
|
+
const width = format.sampleRate * format.channels * 2;
|
|
33
|
+
return width > 0 ? byteLength / width : 0;
|
|
34
|
+
}
|
|
35
|
+
async function readText(text, signal) {
|
|
36
|
+
if (typeof text === "string") return text;
|
|
37
|
+
let combined = "";
|
|
38
|
+
for await (const part of text) {
|
|
39
|
+
if (signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
|
|
40
|
+
combined += part;
|
|
41
|
+
}
|
|
42
|
+
return combined;
|
|
43
|
+
}
|
|
44
|
+
function speakOpenAI(model, input, options) {
|
|
45
|
+
const stream = new SpeechEventStream();
|
|
46
|
+
void pumpSpeech(model, input, options, stream);
|
|
47
|
+
return stream;
|
|
48
|
+
}
|
|
49
|
+
async function pumpSpeech(model, input, options, stream) {
|
|
50
|
+
try {
|
|
51
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
52
|
+
const format = resolveSpeechFormat(model, input.format);
|
|
53
|
+
const openRouter = isOpenRouter(model);
|
|
54
|
+
const responseFormat = speechResponseFormat(format.encoding, openRouter);
|
|
55
|
+
const text = await readText(input.text, options?.signal);
|
|
56
|
+
const payload = {
|
|
57
|
+
model: model.id,
|
|
58
|
+
input: text,
|
|
59
|
+
voice: input.voice,
|
|
60
|
+
response_format: responseFormat
|
|
61
|
+
};
|
|
62
|
+
if (input.instructions !== void 0) payload.instructions = input.instructions;
|
|
63
|
+
const body = await applyPayload(payload, model, options);
|
|
64
|
+
if (!isRecord(body)) throw new Error("Speech payload was not JSON");
|
|
65
|
+
const response = await voiceFetch(
|
|
66
|
+
model,
|
|
67
|
+
options,
|
|
68
|
+
endpoint(model.baseUrl, "audio/speech"),
|
|
69
|
+
JSON.stringify(body),
|
|
70
|
+
"application/json"
|
|
71
|
+
);
|
|
72
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
73
|
+
const seconds = outputSeconds(format, data.length);
|
|
74
|
+
const meters = { characters: text.length };
|
|
75
|
+
if (seconds !== void 0) meters.outputSeconds = seconds;
|
|
76
|
+
stream.push({ type: "start", format });
|
|
77
|
+
stream.push({
|
|
78
|
+
type: "audio",
|
|
79
|
+
chunk: { data, format, timestamp: 0 },
|
|
80
|
+
span: { itemId: model.id, start: 0, end: text.length }
|
|
81
|
+
});
|
|
82
|
+
stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const voice = asVoiceError(error);
|
|
85
|
+
stream.push({ type: "error", reason: voice.reason, error: voice.error });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export {
|
|
89
|
+
speakOpenAI
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=speak.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/openai/speak.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { SpeechModel } from '../models.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport {\n applyPayload,\n asVoiceError,\n endpoint,\n isOpenRouter,\n isRecord,\n voiceFetch,\n} from './wire.ts';\n\nfunction 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 resolveSpeechFormat(\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 speechResponseFormat(encoding: AudioFormat['encoding'], openRouter: boolean): string {\n if (encoding === 'pcm16') return 'pcm';\n if (encoding === 'mp3') return 'mp3';\n if (openRouter) throw new Error(`OpenRouter speech cannot return ${encoding}`);\n if (encoding === 'wav' || encoding === 'opus' || encoding === 'flac') return encoding;\n throw new Error(`OpenAI speech cannot return ${encoding}`);\n}\n\nfunction outputSeconds(format: AudioFormat, byteLength: number): number | undefined {\n if (format.encoding !== 'pcm16') return undefined;\n const width = format.sampleRate * format.channels * 2;\n return width > 0 ? byteLength / width : 0;\n}\n\nasync function readText(\n text: SpeechInput['text'],\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 VoiceRequestError('aborted', 'aborted');\n combined += part;\n }\n return combined;\n}\n\nexport function speakOpenAI(\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 = resolveSpeechFormat(model, input.format);\n const openRouter = isOpenRouter(model);\n const responseFormat = speechResponseFormat(format.encoding, openRouter);\n const text = await readText(input.text, options?.signal);\n const payload: Record<string, unknown> = {\n model: model.id,\n input: text,\n voice: input.voice,\n response_format: responseFormat,\n };\n if (input.instructions !== undefined) payload.instructions = input.instructions;\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, 'audio/speech'),\n JSON.stringify(body),\n 'application/json'\n );\n const data = new Uint8Array(await response.arrayBuffer());\n const seconds = outputSeconds(format, data.length);\n const meters: AudioCounters = { characters: text.length };\n if (seconds !== undefined) 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 } catch (error) {\n const voice = asVoiceError(error);\n stream.push({ type: 'error', reason: voice.reason, error: voice.error });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
2
|
+
import { priceAudio } from "../usage.js";
|
|
3
|
+
import {
|
|
4
|
+
applyPayload,
|
|
5
|
+
asVoiceError,
|
|
6
|
+
audioFile,
|
|
7
|
+
audioPayload,
|
|
8
|
+
encodeMultipart,
|
|
9
|
+
endpoint,
|
|
10
|
+
isOpenRouter,
|
|
11
|
+
isRecord,
|
|
12
|
+
multipartType,
|
|
13
|
+
numberField,
|
|
14
|
+
readAudio,
|
|
15
|
+
readJson,
|
|
16
|
+
requestSignal,
|
|
17
|
+
stringField,
|
|
18
|
+
voiceFetch
|
|
19
|
+
} from "./wire.js";
|
|
20
|
+
function durationSeconds(record) {
|
|
21
|
+
const usage = record.usage;
|
|
22
|
+
if (isRecord(usage)) {
|
|
23
|
+
const seconds = numberField(usage, "seconds");
|
|
24
|
+
if (seconds !== void 0) return seconds;
|
|
25
|
+
}
|
|
26
|
+
const duration = numberField(record, "duration");
|
|
27
|
+
return duration === void 0 ? 0 : duration;
|
|
28
|
+
}
|
|
29
|
+
function fieldText(segment) {
|
|
30
|
+
const text = stringField(segment, "text");
|
|
31
|
+
return text === void 0 ? "" : text;
|
|
32
|
+
}
|
|
33
|
+
function fieldNumber(segment, key) {
|
|
34
|
+
const value = numberField(segment, key);
|
|
35
|
+
return value === void 0 ? 0 : value;
|
|
36
|
+
}
|
|
37
|
+
function segmentFrom(segment) {
|
|
38
|
+
const speaker = stringField(segment, "speaker");
|
|
39
|
+
const mapped = {
|
|
40
|
+
text: fieldText(segment),
|
|
41
|
+
start: fieldNumber(segment, "start"),
|
|
42
|
+
end: fieldNumber(segment, "end")
|
|
43
|
+
};
|
|
44
|
+
if (speaker !== void 0) mapped.speaker = speaker;
|
|
45
|
+
return mapped;
|
|
46
|
+
}
|
|
47
|
+
function transcriptFrom(body, model, seconds) {
|
|
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 segments = Array.isArray(body.segments) ? body.segments.filter(isRecord).map(segmentFrom) : void 0;
|
|
53
|
+
return {
|
|
54
|
+
text,
|
|
55
|
+
...language !== void 0 ? { language } : {},
|
|
56
|
+
...segments !== void 0 ? { segments } : {},
|
|
57
|
+
usage: priceAudio(model.audioCost, { inputSeconds: seconds })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function fileFrom(loaded) {
|
|
61
|
+
const named = audioFile(loaded.mimeType);
|
|
62
|
+
return { data: loaded.data, mimeType: loaded.mimeType, ...named };
|
|
63
|
+
}
|
|
64
|
+
async function transcribeOpenAI(model, input, options) {
|
|
65
|
+
try {
|
|
66
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
67
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
68
|
+
const loaded = await readAudio(input, fetchImpl, requestSignal(options));
|
|
69
|
+
const file = fileFrom(loaded);
|
|
70
|
+
const payload = audioPayload(file, isOpenRouter(model), model.id);
|
|
71
|
+
const body = await applyPayload(payload, model, options);
|
|
72
|
+
if (!isRecord(body)) throw new Error("Transcription payload was not JSON");
|
|
73
|
+
const url = endpoint(model.baseUrl, "audio/transcriptions");
|
|
74
|
+
const response = await postTranscription(model, options, url, body);
|
|
75
|
+
const json = await readJson(response);
|
|
76
|
+
const seconds = isRecord(json) ? durationSeconds(json) : 0;
|
|
77
|
+
return transcriptFrom(json, model, seconds);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const voice = asVoiceError(error);
|
|
80
|
+
throw new VoiceRequestError(voice.reason, voice.error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function postTranscription(model, options, url, body) {
|
|
84
|
+
const inline = body.input_audio;
|
|
85
|
+
if (isRecord(inline)) {
|
|
86
|
+
return voiceFetch(model, options, url, JSON.stringify(body), "application/json");
|
|
87
|
+
}
|
|
88
|
+
const file = body.file;
|
|
89
|
+
if (!isRecord(file) || !(file.data instanceof Uint8Array) || typeof file.mimeType !== "string") {
|
|
90
|
+
throw new Error("Transcription payload was not JSON");
|
|
91
|
+
}
|
|
92
|
+
const named = audioFile(file.mimeType);
|
|
93
|
+
const fields = {};
|
|
94
|
+
const modelId = stringField(body, "model");
|
|
95
|
+
fields.model = modelId === void 0 ? model.id : modelId;
|
|
96
|
+
const encoded = encodeMultipart(
|
|
97
|
+
{
|
|
98
|
+
data: file.data,
|
|
99
|
+
mimeType: file.mimeType,
|
|
100
|
+
filename: typeof file.filename === "string" ? file.filename : named.filename,
|
|
101
|
+
format: named.format
|
|
102
|
+
},
|
|
103
|
+
fields
|
|
104
|
+
);
|
|
105
|
+
const copy = new ArrayBuffer(encoded.byteLength);
|
|
106
|
+
new Uint8Array(copy).set(encoded);
|
|
107
|
+
return voiceFetch(model, options, url, copy, multipartType());
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
transcribeOpenAI
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=transcribe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/openai/transcribe.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport type { AudioInput, Transcript, TranscriptSegment } from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n type AudioFile,\n applyPayload,\n asVoiceError,\n audioFile,\n audioPayload,\n encodeMultipart,\n endpoint,\n isOpenRouter,\n isRecord,\n multipartType,\n numberField,\n readAudio,\n readJson,\n requestSignal,\n stringField,\n voiceFetch,\n} from './wire.ts';\n\nfunction durationSeconds(record: Record<string, unknown>): number {\n const usage = record.usage;\n if (isRecord(usage)) {\n const seconds = numberField(usage, 'seconds');\n if (seconds !== undefined) return seconds;\n }\n const duration = numberField(record, 'duration');\n return duration === undefined ? 0 : duration;\n}\n\nfunction fieldText(segment: Record<string, unknown>): string {\n const text = stringField(segment, 'text');\n return text === undefined ? '' : text;\n}\n\nfunction fieldNumber(segment: Record<string, unknown>, key: string): number {\n const value = numberField(segment, key);\n return value === undefined ? 0 : value;\n}\n\nfunction segmentFrom(segment: Record<string, unknown>): TranscriptSegment {\n const speaker = stringField(segment, 'speaker');\n const mapped: TranscriptSegment = {\n text: fieldText(segment),\n start: fieldNumber(segment, 'start'),\n end: fieldNumber(segment, 'end'),\n };\n if (speaker !== undefined) mapped.speaker = speaker;\n return mapped;\n}\n\nfunction transcriptFrom(\n body: unknown,\n model: TranscriptionModel<string>,\n seconds: number\n): 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 segments = Array.isArray(body.segments)\n ? body.segments.filter(isRecord).map(segmentFrom)\n : undefined;\n return {\n text,\n ...(language !== undefined ? { language } : {}),\n ...(segments !== undefined ? { segments } : {}),\n usage: priceAudio(model.audioCost, { inputSeconds: seconds }),\n };\n}\n\nfunction fileFrom(loaded: { data: Uint8Array; mimeType: string }): AudioFile {\n const named = audioFile(loaded.mimeType);\n return { data: loaded.data, mimeType: loaded.mimeType, ...named };\n}\n\nexport async function transcribeOpenAI(\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 file = fileFrom(loaded);\n const payload = audioPayload(file, isOpenRouter(model), model.id);\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body)) throw new Error('Transcription payload was not JSON');\n const url = endpoint(model.baseUrl, 'audio/transcriptions');\n const response = await postTranscription(model, options, url, body);\n const json = await readJson(response);\n const seconds = isRecord(json) ? durationSeconds(json) : 0;\n return transcriptFrom(json, model, seconds);\n } catch (error) {\n const voice = asVoiceError(error);\n throw new VoiceRequestError(voice.reason, voice.error);\n }\n}\n\nasync function postTranscription(\n model: TranscriptionModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: Record<string, unknown>\n): Promise<Response> {\n const inline = body.input_audio;\n if (isRecord(inline)) {\n return voiceFetch(model, options, url, JSON.stringify(body), 'application/json');\n }\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 JSON');\n }\n const named = audioFile(file.mimeType);\n const fields: Record<string, string> = {};\n const modelId = stringField(body, 'model');\n fields.model = modelId === undefined ? model.id : modelId;\n const encoded = encodeMultipart(\n {\n data: file.data,\n mimeType: file.mimeType,\n filename: typeof file.filename === 'string' ? file.filename : named.filename,\n format: named.format,\n },\n fields\n );\n const copy = new ArrayBuffer(encoded.byteLength);\n new Uint8Array(copy).set(encoded);\n return voiceFetch(model, options, url, copy, multipartType());\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|