@ai-ecoverse/pi-audio 1.13.0 → 1.15.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 +3 -0
- package/dist/captions.js +150 -0
- package/dist/captions.js.map +7 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +2 -2
- package/dist/mistral/catalog.js +109 -0
- package/dist/mistral/catalog.js.map +7 -0
- package/dist/mistral/index.js +5 -0
- package/dist/mistral/index.js.map +7 -0
- package/dist/mistral/listen.js +260 -0
- package/dist/mistral/listen.js.map +7 -0
- package/dist/mistral/speak.js +183 -0
- package/dist/mistral/speak.js.map +7 -0
- package/dist/mistral/transcribe.js +141 -0
- package/dist/mistral/transcribe.js.map +7 -0
- package/dist/mistral/wire.js +143 -0
- package/dist/mistral/wire.js.map +7 -0
- package/dist/turns.js +51 -13
- package/dist/turns.js.map +2 -2
- package/dist/types/captions.d.ts +31 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/mistral/catalog.d.ts +1 -0
- package/dist/types/mistral/index.d.ts +1 -0
- package/dist/types/mistral/listen.d.ts +4 -0
- package/dist/types/mistral/speak.d.ts +4 -0
- package/dist/types/mistral/transcribe.d.ts +4 -0
- package/dist/types/mistral/wire.d.ts +22 -0
- package/dist/types/turns.d.ts +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mistral/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 bearerHeaders,\n connectSocket,\n messageText,\n realtimeEncoding,\n sameFormat,\n socketUrl,\n} from './wire.ts';\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 sawDone: boolean;\n failed: boolean;\n completed: boolean;\n partial: string;\n language: string | undefined;\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 segmentFrom(word: Record<string, unknown>): TranscriptSegment | undefined {\n const text = textOr(word);\n if (text === '') return undefined;\n const speaker = stringField(word, 'speaker_id');\n const segment: TranscriptSegment = {\n 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 listenError(event: Record<string, unknown>): string {\n const direct = stringField(event, 'error');\n if (direct !== undefined) return direct;\n const error = event.error;\n if (!isRecord(error)) return 'Mistral transcription failed';\n const message = error.message;\n if (typeof message === 'string') return message;\n if (isRecord(message)) {\n const detail = stringField(message, 'detail');\n if (detail !== undefined) return detail;\n }\n return 'Mistral transcription failed';\n}\n\nfunction spokenText(partial: string, doneText: string): string {\n if (partial !== '') return partial;\n return doneText;\n}\n\nfunction sttUrl(model: TranscriptionModel<string>, options: ListenOptions | undefined): string {\n const url = socketUrl(model.baseUrl, 'audio/transcriptions/realtime');\n const params = new URLSearchParams();\n params.set('model', model.id);\n if (options?.language) params.set('language', options.language);\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 realtimeEncoding(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 addSegment(word: Record<string, unknown>, state: ListenState) {\n const segment = segmentFrom(word);\n if (segment) state.segments.push(segment);\n}\n\nfunction addSegments(value: unknown, state: ListenState) {\n if (!Array.isArray(value)) return;\n for (const word of value) {\n if (!isRecord(word)) continue;\n addSegment(word, state);\n }\n}\n\nfunction emitFinal(spoken: string, state: ListenState, push: (event: TranscriptionEvent) => void) {\n if (spoken === '') return;\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, 'type');\n if (type === 'session.created' || type === 'session.updated') {\n state.ready = true;\n return;\n }\n if (type === 'transcription.text.delta') {\n state.partial += textOr(event);\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: state.partial });\n return;\n }\n if (type === 'transcription.segment') {\n addSegment(event, state);\n return;\n }\n if (type === 'transcription.done') {\n const language = stringField(event, 'language');\n if (language !== undefined) state.language = language;\n addSegments(event.segments, state);\n const spoken = spokenText(state.partial, textOr(event));\n state.partial = '';\n state.sawDone = true;\n emitFinal(spoken, state, push);\n return;\n }\n if (type === 'error') {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: listenError(event) });\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.language !== undefined ? { language: state.language } : {}),\n ...(state.segments.length > 0 ? { segments: state.segments } : {}),\n usage: priceAudio(model.audioCost, { inputSeconds: heard }),\n },\n });\n}\n\nexport function listenMistral(\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 sawDone: false,\n failed: false,\n completed: false,\n partial: '',\n language: 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 === 'transcription.done') doneWait.resolve();\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, options), bearerHeaders(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: 'Mistral transcription socket closed' });\n }\n wake('close');\n });\n await awaitOpen(socket, 'Mistral 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 socket.send(\n JSON.stringify({\n type: 'session.update',\n session: {\n audio_format: { encoding: realtimeEncoding(format), sample_rate: format.sampleRate },\n },\n })\n );\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 socket.send(JSON.stringify({ type: 'input_audio.flush' }));\n socket.send(JSON.stringify({ type: 'input_audio.end' }));\n state.clientDone = true;\n if (state.sawDone) doneWait.resolve();\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(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(JSON.stringify({ type: 'input_audio.append', audio: encodeAudioBase64(chunk.data) }));\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('Mistral transcription event was not JSON');\n applyListen(parsed, state, push);\n wake(stringField(parsed, '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,183 @@
|
|
|
1
|
+
import { chunkSeconds } from "../audio.js";
|
|
2
|
+
import { decodeAudioBase64 } from "../convert.js";
|
|
3
|
+
import {
|
|
4
|
+
applyPayload,
|
|
5
|
+
asVoiceError,
|
|
6
|
+
endpoint,
|
|
7
|
+
isRecord,
|
|
8
|
+
requestSignal,
|
|
9
|
+
stringField
|
|
10
|
+
} from "../openai/wire.js";
|
|
11
|
+
import { SpeechEventStream } from "../speech.js";
|
|
12
|
+
import { priceAudio } from "../usage.js";
|
|
13
|
+
import { bearerHeaders, sameFormat, speechFormatName } from "./wire.js";
|
|
14
|
+
function languageCode(language) {
|
|
15
|
+
if (language === void 0) return void 0;
|
|
16
|
+
if (language.trim() === "") return void 0;
|
|
17
|
+
return language;
|
|
18
|
+
}
|
|
19
|
+
function resolveFormat(model, requested) {
|
|
20
|
+
const fallback = model.outputFormats[0];
|
|
21
|
+
if (!fallback) throw new Error("No output format");
|
|
22
|
+
if (!requested) return fallback;
|
|
23
|
+
const match = model.outputFormats.find((format) => sameFormat(format, requested));
|
|
24
|
+
if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
|
|
25
|
+
return match;
|
|
26
|
+
}
|
|
27
|
+
function speechBody(model, text, voice, language, format) {
|
|
28
|
+
const body = {
|
|
29
|
+
model: model.id,
|
|
30
|
+
input: text,
|
|
31
|
+
voice_id: voice,
|
|
32
|
+
response_format: speechFormatName(format),
|
|
33
|
+
stream: true
|
|
34
|
+
};
|
|
35
|
+
const code = languageCode(language);
|
|
36
|
+
if (code !== void 0) body.language = code;
|
|
37
|
+
return body;
|
|
38
|
+
}
|
|
39
|
+
async function readText(text, signal) {
|
|
40
|
+
if (typeof text === "string") return text;
|
|
41
|
+
let combined = "";
|
|
42
|
+
for await (const part of text) {
|
|
43
|
+
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
44
|
+
combined += part;
|
|
45
|
+
}
|
|
46
|
+
return combined;
|
|
47
|
+
}
|
|
48
|
+
function pieceName(piece) {
|
|
49
|
+
if (piece.name !== void 0 && piece.name !== "") return piece.name;
|
|
50
|
+
if (!piece.body) return "";
|
|
51
|
+
const typed = stringField(piece.body, "type");
|
|
52
|
+
if (typed !== void 0) return typed;
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
function audioOf(piece) {
|
|
56
|
+
const body = piece.body;
|
|
57
|
+
if (!body) return void 0;
|
|
58
|
+
const direct = stringField(body, "audio_data");
|
|
59
|
+
if (direct !== void 0) return direct;
|
|
60
|
+
const nested = body.data;
|
|
61
|
+
if (!isRecord(nested)) return void 0;
|
|
62
|
+
return stringField(nested, "audio_data");
|
|
63
|
+
}
|
|
64
|
+
function speechError(body) {
|
|
65
|
+
if (!body) return "Mistral speech failed";
|
|
66
|
+
const message = stringField(body, "message");
|
|
67
|
+
if (message !== void 0) return message;
|
|
68
|
+
const error = body.error;
|
|
69
|
+
if (typeof error === "string") return error;
|
|
70
|
+
if (isRecord(error)) {
|
|
71
|
+
const nested = stringField(error, "message");
|
|
72
|
+
if (nested !== void 0) return nested;
|
|
73
|
+
}
|
|
74
|
+
return "Mistral speech failed";
|
|
75
|
+
}
|
|
76
|
+
function piecesFrom(raw) {
|
|
77
|
+
const trimmed = raw.trim();
|
|
78
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
79
|
+
const parsed = JSON.parse(trimmed);
|
|
80
|
+
if (isRecord(parsed) && !Array.isArray(parsed)) {
|
|
81
|
+
return [{ name: stringField(parsed, "type"), body: parsed }];
|
|
82
|
+
}
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
const pieces = [];
|
|
86
|
+
for (const block of raw.split("\n\n")) {
|
|
87
|
+
let name;
|
|
88
|
+
const data = [];
|
|
89
|
+
for (const line of block.split("\n")) {
|
|
90
|
+
if (line.startsWith("event:")) name = line.slice(6).trim();
|
|
91
|
+
if (line.startsWith("data:")) data.push(line.slice(5).trim());
|
|
92
|
+
}
|
|
93
|
+
if (data.length === 0 && name === void 0) continue;
|
|
94
|
+
const joined = data.join("\n");
|
|
95
|
+
if (joined === "") {
|
|
96
|
+
pieces.push({ name, body: void 0 });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const parsed = JSON.parse(joined);
|
|
100
|
+
pieces.push({ name, body: isRecord(parsed) ? parsed : void 0 });
|
|
101
|
+
}
|
|
102
|
+
return pieces;
|
|
103
|
+
}
|
|
104
|
+
async function mistralFetch(model, options, url, body) {
|
|
105
|
+
const headers = bearerHeaders(model, options);
|
|
106
|
+
headers["Content-Type"] = "application/json";
|
|
107
|
+
headers.Accept = "text/event-stream";
|
|
108
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
109
|
+
const response = await fetchImpl(url, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers,
|
|
112
|
+
body,
|
|
113
|
+
signal: requestSignal(options)
|
|
114
|
+
});
|
|
115
|
+
await options?.onResponse?.({ status: response.status, headers: {} }, model);
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
throw new Error(text === "" ? `HTTP ${response.status}` : text);
|
|
119
|
+
}
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
function speakMistral(model, input, options) {
|
|
123
|
+
const stream = new SpeechEventStream();
|
|
124
|
+
void pumpSpeech(model, input, options, stream);
|
|
125
|
+
return stream;
|
|
126
|
+
}
|
|
127
|
+
async function pumpSpeech(model, input, options, stream) {
|
|
128
|
+
try {
|
|
129
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
130
|
+
const format = resolveFormat(model, input.format);
|
|
131
|
+
const text = await readText(input.text, options?.signal);
|
|
132
|
+
await postSpeech(model, text, input.voice, input.language, format, options, stream);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const voice = asVoiceError(error);
|
|
135
|
+
stream.push({ type: "error", reason: voice.reason, error: voice.error });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function postSpeech(model, text, voice, language, format, options, stream) {
|
|
139
|
+
const payload = speechBody(model, text, voice, language, format);
|
|
140
|
+
const body = await applyPayload(payload, model, options);
|
|
141
|
+
if (!isRecord(body)) throw new Error("Speech payload was not JSON");
|
|
142
|
+
const response = await mistralFetch(
|
|
143
|
+
model,
|
|
144
|
+
options,
|
|
145
|
+
endpoint(model.baseUrl, "audio/speech"),
|
|
146
|
+
JSON.stringify(body)
|
|
147
|
+
);
|
|
148
|
+
const raw = await response.text();
|
|
149
|
+
const pieces = piecesFrom(raw);
|
|
150
|
+
let started = false;
|
|
151
|
+
let seconds = 0;
|
|
152
|
+
let heard = false;
|
|
153
|
+
for (const piece of pieces) {
|
|
154
|
+
const name = pieceName(piece);
|
|
155
|
+
if (name === "error" || name === "speech.error") {
|
|
156
|
+
throw new Error(speechError(piece.body));
|
|
157
|
+
}
|
|
158
|
+
if (name === "speech.audio.done") continue;
|
|
159
|
+
const audio = audioOf(piece);
|
|
160
|
+
if (audio === void 0 || audio === "") continue;
|
|
161
|
+
const data = decodeAudioBase64(audio);
|
|
162
|
+
heard = true;
|
|
163
|
+
const chunkSecondsValue = chunkSeconds({ data, format, timestamp: 0 });
|
|
164
|
+
seconds += chunkSecondsValue;
|
|
165
|
+
if (!started) {
|
|
166
|
+
started = true;
|
|
167
|
+
stream.push({ type: "start", format });
|
|
168
|
+
}
|
|
169
|
+
stream.push({
|
|
170
|
+
type: "audio",
|
|
171
|
+
chunk: { data, format, timestamp: 0 },
|
|
172
|
+
span: { itemId: model.id, start: 0, end: text.length }
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (!heard) throw new Error("Speech response had no audio");
|
|
176
|
+
const meters = { characters: text.length };
|
|
177
|
+
if (seconds > 0) meters.outputSeconds = seconds;
|
|
178
|
+
stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
speakMistral
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=speak.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mistral/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 type { SpeechModel } from '../models.ts';\nimport {\n applyPayload,\n asVoiceError,\n endpoint,\n isRecord,\n requestSignal,\n stringField,\n} from '../openai/wire.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport { bearerHeaders, sameFormat, speechFormatName } from './wire.ts';\n\ninterface SpeechPiece {\n name: string | undefined;\n body: Record<string, unknown> | undefined;\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 voice: string,\n language: string | undefined,\n format: AudioFormat\n): Record<string, unknown> {\n const body: Record<string, unknown> = {\n model: model.id,\n input: text,\n voice_id: voice,\n response_format: speechFormatName(format),\n stream: true,\n };\n const code = languageCode(language);\n if (code !== undefined) body.language = code;\n return body;\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\nfunction pieceName(piece: SpeechPiece): string {\n if (piece.name !== undefined && piece.name !== '') return piece.name;\n if (!piece.body) return '';\n const typed = stringField(piece.body, 'type');\n if (typed !== undefined) return typed;\n return '';\n}\n\nfunction audioOf(piece: SpeechPiece): string | undefined {\n const body = piece.body;\n if (!body) return undefined;\n const direct = stringField(body, 'audio_data');\n if (direct !== undefined) return direct;\n const nested = body.data;\n if (!isRecord(nested)) return undefined;\n return stringField(nested, 'audio_data');\n}\n\nfunction speechError(body: Record<string, unknown> | undefined): string {\n if (!body) return 'Mistral speech failed';\n const message = stringField(body, 'message');\n if (message !== undefined) return message;\n const error = body.error;\n if (typeof error === 'string') return error;\n if (isRecord(error)) {\n const nested = stringField(error, 'message');\n if (nested !== undefined) return nested;\n }\n return 'Mistral speech failed';\n}\n\nfunction piecesFrom(raw: string): SpeechPiece[] {\n const trimmed = raw.trim();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n const parsed = JSON.parse(trimmed) as unknown;\n if (isRecord(parsed) && !Array.isArray(parsed)) {\n return [{ name: stringField(parsed, 'type'), body: parsed }];\n }\n return [];\n }\n const pieces: SpeechPiece[] = [];\n for (const block of raw.split('\\n\\n')) {\n let name: string | undefined;\n const data: string[] = [];\n for (const line of block.split('\\n')) {\n if (line.startsWith('event:')) name = line.slice(6).trim();\n if (line.startsWith('data:')) data.push(line.slice(5).trim());\n }\n if (data.length === 0 && name === undefined) continue;\n const joined = data.join('\\n');\n if (joined === '') {\n pieces.push({ name, body: undefined });\n continue;\n }\n const parsed = JSON.parse(joined) as unknown;\n pieces.push({ name, body: isRecord(parsed) ? parsed : undefined });\n }\n return pieces;\n}\n\nasync function mistralFetch(\n model: SpeechModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: string\n): Promise<Response> {\n const headers = bearerHeaders(model, options);\n headers['Content-Type'] = 'application/json';\n headers.Accept = 'text/event-stream';\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 speakMistral(\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 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, voice, language, format);\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body)) throw new Error('Speech payload was not JSON');\n const response = await mistralFetch(\n model,\n options,\n endpoint(model.baseUrl, 'audio/speech'),\n JSON.stringify(body)\n );\n const raw = await response.text();\n const pieces = piecesFrom(raw);\n let started = false;\n let seconds = 0;\n let heard = false;\n for (const piece of pieces) {\n const name = pieceName(piece);\n if (name === 'error' || name === 'speech.error') {\n throw new Error(speechError(piece.body));\n }\n if (name === 'speech.audio.done') continue;\n const audio = audioOf(piece);\n if (audio === undefined || audio === '') continue;\n const data = decodeAudioBase64(audio);\n heard = true;\n const chunkSecondsValue = chunkSeconds({ data, format, timestamp: 0 });\n seconds += chunkSecondsValue;\n if (!started) {\n started = true;\n stream.push({ type: 'start', format });\n }\n stream.push({\n type: 'audio',\n chunk: { data, format, timestamp: 0 },\n span: { itemId: model.id, start: 0, end: text.length },\n });\n }\n if (!heard) throw new Error('Speech response had no audio');\n const meters: AudioCounters = { characters: text.length };\n if (seconds > 0) meters.outputSeconds = seconds;\n stream.push({ type: 'done', usage: priceAudio(model.audioCost, meters) });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
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, bearerHeaders, encodeForm, formType } 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 durationOf(body) {
|
|
26
|
+
const direct = numberField(body, "duration");
|
|
27
|
+
if (direct !== void 0) return direct;
|
|
28
|
+
const alt = numberField(body, "audio_duration_secs");
|
|
29
|
+
if (alt !== void 0) return alt;
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
function segmentFrom(word) {
|
|
33
|
+
const text = textOf(word, "text");
|
|
34
|
+
if (text === "") return void 0;
|
|
35
|
+
const speaker = stringField(word, "speaker_id");
|
|
36
|
+
const segment = {
|
|
37
|
+
text,
|
|
38
|
+
start: secondsOf(word, "start"),
|
|
39
|
+
end: secondsOf(word, "end")
|
|
40
|
+
};
|
|
41
|
+
if (speaker !== void 0) segment.speaker = speaker;
|
|
42
|
+
return segment;
|
|
43
|
+
}
|
|
44
|
+
function segmentsFrom(value) {
|
|
45
|
+
if (!Array.isArray(value)) return [];
|
|
46
|
+
const segments = [];
|
|
47
|
+
for (const word of value) {
|
|
48
|
+
if (!isRecord(word)) continue;
|
|
49
|
+
const segment = segmentFrom(word);
|
|
50
|
+
if (segment) segments.push(segment);
|
|
51
|
+
}
|
|
52
|
+
return segments;
|
|
53
|
+
}
|
|
54
|
+
function transcriptFrom(body, model) {
|
|
55
|
+
if (!isRecord(body)) throw new Error("Transcription response had no text");
|
|
56
|
+
const direct = stringField(body, "text");
|
|
57
|
+
const words = segmentsFrom(body.segments);
|
|
58
|
+
const timed = words.length > 0 ? words : segmentsFrom(body.words);
|
|
59
|
+
const joined = timed.map((segment) => segment.text).join(" ");
|
|
60
|
+
const text = direct === void 0 ? joined : direct;
|
|
61
|
+
if (direct === void 0 && timed.length === 0) {
|
|
62
|
+
throw new Error("Transcription response had no text");
|
|
63
|
+
}
|
|
64
|
+
const language = stringField(body, "language");
|
|
65
|
+
return {
|
|
66
|
+
text,
|
|
67
|
+
...language !== void 0 ? { language } : {},
|
|
68
|
+
...timed.length > 0 ? { segments: timed } : {},
|
|
69
|
+
usage: priceAudio(model.audioCost, { inputSeconds: durationOf(body) })
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function fileOf(body) {
|
|
73
|
+
const file = body.file;
|
|
74
|
+
if (!isRecord(file) || !(file.data instanceof Uint8Array) || typeof file.mimeType !== "string") {
|
|
75
|
+
throw new Error("Transcription payload was not a file");
|
|
76
|
+
}
|
|
77
|
+
const named = typeof file.filename === "string" ? file.filename : audioFilename(file.mimeType);
|
|
78
|
+
return { data: file.data, mimeType: file.mimeType, filename: named };
|
|
79
|
+
}
|
|
80
|
+
function fieldStrings(body) {
|
|
81
|
+
const fields = {};
|
|
82
|
+
for (const [key, value] of Object.entries(body)) {
|
|
83
|
+
if (key === "file") continue;
|
|
84
|
+
if (typeof value !== "string") continue;
|
|
85
|
+
fields[key] = value;
|
|
86
|
+
}
|
|
87
|
+
return fields;
|
|
88
|
+
}
|
|
89
|
+
async function mistralFetch(model, options, url, body, contentType) {
|
|
90
|
+
const headers = bearerHeaders(model, options);
|
|
91
|
+
headers["Content-Type"] = contentType;
|
|
92
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
93
|
+
const response = await fetchImpl(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers,
|
|
96
|
+
body,
|
|
97
|
+
signal: requestSignal(options)
|
|
98
|
+
});
|
|
99
|
+
await options?.onResponse?.({ status: response.status, headers: {} }, model);
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
const text = await response.text();
|
|
102
|
+
throw new Error(text === "" ? `HTTP ${response.status}` : text);
|
|
103
|
+
}
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
async function transcribeMistral(model, input, options) {
|
|
107
|
+
try {
|
|
108
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
109
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
110
|
+
const loaded = await readAudio(input, fetchImpl, requestSignal(options));
|
|
111
|
+
const payload = {
|
|
112
|
+
model: model.id,
|
|
113
|
+
timestamp_granularities: "word",
|
|
114
|
+
file: {
|
|
115
|
+
data: loaded.data,
|
|
116
|
+
mimeType: loaded.mimeType,
|
|
117
|
+
filename: audioFilename(loaded.mimeType)
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (model.diarization) payload.diarize = "true";
|
|
121
|
+
const body = await applyPayload(payload, model, options);
|
|
122
|
+
if (!isRecord(body)) throw new Error("Transcription payload was not a file");
|
|
123
|
+
const file = fileOf(body);
|
|
124
|
+
const encoded = encodeForm(file, fieldStrings(body));
|
|
125
|
+
const response = await mistralFetch(
|
|
126
|
+
model,
|
|
127
|
+
options,
|
|
128
|
+
endpoint(model.baseUrl, "audio/transcriptions"),
|
|
129
|
+
encoded,
|
|
130
|
+
formType()
|
|
131
|
+
);
|
|
132
|
+
return transcriptFrom(await readJson(response), model);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const voice = asVoiceError(error);
|
|
135
|
+
throw new VoiceRequestError(voice.reason, voice.error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
transcribeMistral
|
|
140
|
+
};
|
|
141
|
+
//# sourceMappingURL=transcribe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mistral/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, bearerHeaders, encodeForm, formType, type MistralFile } 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 durationOf(body: Record<string, unknown>): number {\n const direct = numberField(body, 'duration');\n if (direct !== undefined) return direct;\n const alt = numberField(body, 'audio_duration_secs');\n if (alt !== undefined) return alt;\n return 0;\n}\n\nfunction segmentFrom(word: Record<string, unknown>): TranscriptSegment | undefined {\n const text = textOf(word, 'text');\n if (text === '') return undefined;\n const speaker = stringField(word, 'speaker_id');\n const segment: TranscriptSegment = {\n 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 segmentsFrom(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 = segmentFrom(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 words = segmentsFrom(body.segments);\n const timed = words.length > 0 ? words : segmentsFrom(body.words);\n const joined = timed.map((segment) => segment.text).join(' ');\n const text = direct === undefined ? joined : direct;\n if (direct === undefined && timed.length === 0) {\n throw new Error('Transcription response had no text');\n }\n const language = stringField(body, 'language');\n return {\n text,\n ...(language !== undefined ? { language } : {}),\n ...(timed.length > 0 ? { segments: timed } : {}),\n usage: priceAudio(model.audioCost, { inputSeconds: durationOf(body) }),\n };\n}\n\nfunction fileOf(body: Record<string, unknown>): MistralFile {\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 mistralFetch(\n model: TranscriptionModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: BodyInit,\n contentType: string\n): Promise<Response> {\n const headers = bearerHeaders(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 transcribeMistral(\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 payload: Record<string, unknown> = {\n model: model.id,\n timestamp_granularities: 'word',\n file: {\n data: loaded.data,\n mimeType: loaded.mimeType,\n filename: audioFilename(loaded.mimeType),\n },\n };\n if (model.diarization) payload.diarize = 'true';\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 mistralFetch(\n model,\n options,\n endpoint(model.baseUrl, 'audio/transcriptions'),\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,143 @@
|
|
|
1
|
+
import { requestHeaders } from "../openai/wire.js";
|
|
2
|
+
const boundary = "pi-audio-mistral-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/webm": "audio.webm",
|
|
15
|
+
"audio/mp4": "audio.mp4",
|
|
16
|
+
"audio/m4a": "audio.m4a",
|
|
17
|
+
"audio/aac": "audio.aac"
|
|
18
|
+
};
|
|
19
|
+
const encodings = {
|
|
20
|
+
pcm16: "pcm_s16le",
|
|
21
|
+
pcmf32: "pcm_f32le",
|
|
22
|
+
mulaw: "pcm_mulaw",
|
|
23
|
+
alaw: "pcm_alaw"
|
|
24
|
+
};
|
|
25
|
+
const speechNames = {
|
|
26
|
+
pcmf32: "pcm",
|
|
27
|
+
mp3: "mp3",
|
|
28
|
+
wav: "wav",
|
|
29
|
+
flac: "flac",
|
|
30
|
+
opus: "opus"
|
|
31
|
+
};
|
|
32
|
+
function mediaType(mimeType) {
|
|
33
|
+
const type = mimeType.split(";")[0]?.trim().toLowerCase();
|
|
34
|
+
if (!type) throw new Error(`Unsupported audio type ${mimeType}`);
|
|
35
|
+
return type;
|
|
36
|
+
}
|
|
37
|
+
function audioFilename(mimeType) {
|
|
38
|
+
const name = filenames[mediaType(mimeType)];
|
|
39
|
+
if (!name) throw new Error(`Unsupported audio type ${mimeType}`);
|
|
40
|
+
return name;
|
|
41
|
+
}
|
|
42
|
+
function encodeForm(file, fields) {
|
|
43
|
+
const chunks = [];
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
const write = (value) => {
|
|
46
|
+
chunks.push(encoder.encode(value));
|
|
47
|
+
};
|
|
48
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
49
|
+
write(`--${boundary}\r
|
|
50
|
+
Content-Disposition: form-data; name="${name}"\r
|
|
51
|
+
\r
|
|
52
|
+
${value}\r
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
const header = `--${boundary}\r
|
|
56
|
+
Content-Disposition: form-data; name="file"; filename="${file.filename}"\r
|
|
57
|
+
Content-Type: ${file.mimeType}\r
|
|
58
|
+
\r
|
|
59
|
+
`;
|
|
60
|
+
write(header);
|
|
61
|
+
chunks.push(file.data);
|
|
62
|
+
write(`\r
|
|
63
|
+
--${boundary}--\r
|
|
64
|
+
`);
|
|
65
|
+
const body = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0));
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (const chunk of chunks) {
|
|
68
|
+
body.set(chunk, offset);
|
|
69
|
+
offset += chunk.length;
|
|
70
|
+
}
|
|
71
|
+
return body.buffer;
|
|
72
|
+
}
|
|
73
|
+
function formType() {
|
|
74
|
+
return `multipart/form-data; boundary=${boundary}`;
|
|
75
|
+
}
|
|
76
|
+
function bearerHeaders(model, options) {
|
|
77
|
+
return requestHeaders(model, options);
|
|
78
|
+
}
|
|
79
|
+
function socketUrl(baseUrl, path) {
|
|
80
|
+
const url = new URL(baseUrl);
|
|
81
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
82
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path}`;
|
|
83
|
+
url.search = "";
|
|
84
|
+
return url;
|
|
85
|
+
}
|
|
86
|
+
function realtimeEncoding(format) {
|
|
87
|
+
const name = encodings[format.encoding];
|
|
88
|
+
if (!name) throw new Error(`Unsupported audio format ${format.encoding}`);
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
91
|
+
function speechFormatName(format) {
|
|
92
|
+
const name = speechNames[format.encoding];
|
|
93
|
+
if (!name) throw new Error(`Unsupported speech format ${format.encoding}`);
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
96
|
+
function connectSocket(url, headers) {
|
|
97
|
+
const Socket = WebSocket;
|
|
98
|
+
return new Socket(url, { headers });
|
|
99
|
+
}
|
|
100
|
+
function awaitOpen(socket, label, fail) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
let opened = false;
|
|
103
|
+
socket.addEventListener("open", () => {
|
|
104
|
+
opened = true;
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
socket.addEventListener("error", () => {
|
|
108
|
+
fail(`${label} socket failed`);
|
|
109
|
+
if (!opened) resolve();
|
|
110
|
+
});
|
|
111
|
+
socket.addEventListener("close", () => {
|
|
112
|
+
if (!opened) {
|
|
113
|
+
fail(`${label} socket closed`);
|
|
114
|
+
resolve();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function messageText(data) {
|
|
120
|
+
if (typeof data === "string") return data;
|
|
121
|
+
if (data instanceof ArrayBuffer) return new TextDecoder().decode(data);
|
|
122
|
+
if (data instanceof Uint8Array) return new TextDecoder().decode(data);
|
|
123
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) return data.text();
|
|
124
|
+
throw new Error("Unsupported socket message");
|
|
125
|
+
}
|
|
126
|
+
function sameFormat(left, right) {
|
|
127
|
+
return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
audioFilename,
|
|
131
|
+
awaitOpen,
|
|
132
|
+
bearerHeaders,
|
|
133
|
+
connectSocket,
|
|
134
|
+
encodeForm,
|
|
135
|
+
formType,
|
|
136
|
+
mediaType,
|
|
137
|
+
messageText,
|
|
138
|
+
realtimeEncoding,
|
|
139
|
+
sameFormat,
|
|
140
|
+
socketUrl,
|
|
141
|
+
speechFormatName
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/mistral/wire.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport { requestHeaders } from '../openai/wire.ts';\n\nconst boundary = 'pi-audio-mistral-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/webm': 'audio.webm',\n 'audio/mp4': 'audio.mp4',\n 'audio/m4a': 'audio.m4a',\n 'audio/aac': 'audio.aac',\n};\n\nconst encodings: Record<string, string> = {\n pcm16: 'pcm_s16le',\n pcmf32: 'pcm_f32le',\n mulaw: 'pcm_mulaw',\n alaw: 'pcm_alaw',\n};\n\nconst speechNames: Record<string, string> = {\n pcmf32: 'pcm',\n mp3: 'mp3',\n wav: 'wav',\n flac: 'flac',\n opus: 'opus',\n};\n\nexport interface MistralFile {\n data: Uint8Array;\n mimeType: string;\n filename: 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\nexport function encodeForm(file: MistralFile, 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\nexport function bearerHeaders(\n model: { provider: string; headers?: Record<string, string> },\n options: ProviderRequestOptions | undefined\n): Record<string, string> {\n return requestHeaders(model, options);\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 realtimeEncoding(format: AudioFormat): string {\n const name = encodings[format.encoding];\n if (!name) throw new Error(`Unsupported audio format ${format.encoding}`);\n return name;\n}\n\nexport function speechFormatName(format: AudioFormat): string {\n const name = speechNames[format.encoding];\n if (!name) throw new Error(`Unsupported speech 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"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|