@ai-ecoverse/pi-audio 1.18.1 → 1.19.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 +2 -0
- package/dist/fish/catalog.js +78 -0
- package/dist/fish/catalog.js.map +7 -0
- package/dist/fish/index.js +5 -0
- package/dist/fish/index.js.map +7 -0
- package/dist/fish/speak.js +269 -0
- package/dist/fish/speak.js.map +7 -0
- package/dist/fish/transcribe.js +158 -0
- package/dist/fish/transcribe.js.map +7 -0
- package/dist/fish/wire.js +386 -0
- package/dist/fish/wire.js.map +7 -0
- package/dist/types/fish/catalog.d.ts +1 -0
- package/dist/types/fish/index.d.ts +1 -0
- package/dist/types/fish/speak.d.ts +4 -0
- package/dist/types/fish/transcribe.d.ts +4 -0
- package/dist/types/fish/wire.d.ts +27 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
23
23
|
|
|
24
24
|
`@ai-ecoverse/pi-audio/durable` appends a `voice.truncate` entry for a `VoiceCut`. The entry has no model messages. Its `edits` replace the target assistant message with the text heard so far and keep tool calls. The stored entry stays intact, and pi-durable derives the shorter message for the next model request. `@earendil-works/pi-durable` is an optional peer (`^1.1.0`); the root package does not import it. `VoiceCut.entryId` is the decimal string of pi-durable's numeric entry id.
|
|
25
25
|
|
|
26
|
+
`@ai-ecoverse/pi-audio/fish` registers Fish Audio speech adapters when `registerFish()` is called. `transcribe-1-pro` posts multipart field `audio` to `/asr` with header `model: transcribe-1-pro` ($0.36 per hour). `ignore_timestamps` is false, so word `segments` and `speaker_turns` come back. There is no realtime ASR. `s2.1-pro` posts JSON to `/tts` for a string and streams MessagePack `text` events on `wss://…/tts/live` for an async iterable (`textStreaming: true`). `s2.1-pro-free` is the same path at $0. Paid speech is $15 per million UTF-8 bytes, stored as $0.015 per thousand characters. Voices are caller `reference_id`s (`cloning: true`).
|
|
27
|
+
|
|
26
28
|
`@ai-ecoverse/pi-audio/mistral` registers Mistral speech adapters when `registerMistral()` is called. `voxtral-mini-latest` posts multipart audio to `/audio/transcriptions` ($0.003 per minute, diarization on). `voxtral-mini-transcribe-realtime-2602` streams base64 PCM on `wss://…/audio/transcriptions/realtime` after `session.created` ($0.006 per minute). Realtime has no server endpointing, so `turnDetection` is `none` and the registry VAD wrapper applies. `voxtral-mini-tts-2603` posts JSON to `/audio/speech` and reads SSE `speech.audio.delta` float32 PCM at 24 kHz ($0.016 per thousand characters). Hosted voice ids such as `en_paul_neutral` differ from the open-weight presets. `textStreaming` is false.
|
|
27
29
|
|
|
28
30
|
`@ai-ecoverse/pi-audio/elevenlabs` registers ElevenLabs speech adapters when `registerElevenLabs()` is called. `scribe_v2` posts multipart audio to `/speech-to-text`. `scribe_v2_realtime` streams base64 audio on `wss://…/speech-to-text/realtime` and commits turns with `turn_prediction` (`turnDetection: 'semantic'`). `eleven_flash_v2_5` and `eleven_multilingual_v2` post JSON to `/text-to-speech/{voice}/with-timestamps` for a string and stream text on `stream-input` for an async iterable, with character alignment. `eleven_v3` and `eleven_v4` stay on the HTTP path (`textStreaming: false`). Scribe prices are $0.22 per hour for batch and $0.39 per hour for realtime.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { registerVoiceModel } from "../models.js";
|
|
2
|
+
import { registerSpeechApiProvider, registerTranscriptionApiProvider } from "../registry.js";
|
|
3
|
+
import { speakFish } from "./speak.js";
|
|
4
|
+
import { transcribeFish } from "./transcribe.js";
|
|
5
|
+
const tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
6
|
+
const fish = "https://api.fish.audio/v1";
|
|
7
|
+
const container = (encoding, sampleRate) => ({
|
|
8
|
+
encoding,
|
|
9
|
+
sampleRate,
|
|
10
|
+
channels: 1
|
|
11
|
+
});
|
|
12
|
+
const outputFormats = [
|
|
13
|
+
container("pcm16", 44100),
|
|
14
|
+
container("wav", 44100),
|
|
15
|
+
container("mp3", 44100),
|
|
16
|
+
container("opus", 48e3)
|
|
17
|
+
];
|
|
18
|
+
function speechModel(id, perThousandChars) {
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
name: id,
|
|
22
|
+
api: "fish-tts",
|
|
23
|
+
provider: "fish",
|
|
24
|
+
baseUrl: fish,
|
|
25
|
+
type: "speech",
|
|
26
|
+
textStreaming: true,
|
|
27
|
+
outputFormats,
|
|
28
|
+
cloning: true,
|
|
29
|
+
cost: tokenCost,
|
|
30
|
+
audioCost: { perThousandChars }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function fishCatalog() {
|
|
34
|
+
const batch = {
|
|
35
|
+
id: "transcribe-1-pro",
|
|
36
|
+
name: "transcribe-1-pro",
|
|
37
|
+
api: "fish-asr",
|
|
38
|
+
provider: "fish",
|
|
39
|
+
baseUrl: fish,
|
|
40
|
+
type: "transcription",
|
|
41
|
+
streaming: false,
|
|
42
|
+
turnDetection: "none",
|
|
43
|
+
inputFormats: [
|
|
44
|
+
container("wav", 16e3),
|
|
45
|
+
container("mp3", 16e3),
|
|
46
|
+
container("flac", 16e3),
|
|
47
|
+
container("opus", 16e3)
|
|
48
|
+
],
|
|
49
|
+
diarization: true,
|
|
50
|
+
cost: tokenCost,
|
|
51
|
+
audioCost: { perInputMinute: 0.36 / 60 }
|
|
52
|
+
};
|
|
53
|
+
return [batch, speechModel("s2.1-pro", 0.015), speechModel("s2.1-pro-free", 0)];
|
|
54
|
+
}
|
|
55
|
+
function registerFish() {
|
|
56
|
+
const models = fishCatalog();
|
|
57
|
+
const unregisterModels = models.map((model) => registerVoiceModel(model));
|
|
58
|
+
const unregisterTranscription = registerTranscriptionApiProvider({
|
|
59
|
+
api: "fish-asr",
|
|
60
|
+
transcribe: transcribeFish
|
|
61
|
+
});
|
|
62
|
+
const unregisterSpeech = registerSpeechApiProvider({
|
|
63
|
+
api: "fish-tts",
|
|
64
|
+
speak: speakFish
|
|
65
|
+
});
|
|
66
|
+
let released = false;
|
|
67
|
+
return () => {
|
|
68
|
+
if (released) return;
|
|
69
|
+
released = true;
|
|
70
|
+
for (const unregister of unregisterModels) unregister();
|
|
71
|
+
unregisterTranscription();
|
|
72
|
+
unregisterSpeech();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
registerFish
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish/catalog.ts"],
|
|
4
|
+
"sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport { registerVoiceModel, type SpeechModel, type TranscriptionModel } from '../models.ts';\nimport { registerSpeechApiProvider, registerTranscriptionApiProvider } from '../registry.ts';\nimport { speakFish } from './speak.ts';\nimport { transcribeFish } from './transcribe.ts';\n\nconst tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\nconst fish = 'https://api.fish.audio/v1';\n\nconst container = (\n encoding: 'wav' | 'mp3' | 'flac' | 'opus' | 'pcm16',\n sampleRate: number\n): AudioFormat => ({\n encoding,\n sampleRate,\n channels: 1,\n});\n\nconst outputFormats = [\n container('pcm16', 44100),\n container('wav', 44100),\n container('mp3', 44100),\n container('opus', 48000),\n];\n\nfunction speechModel(id: string, perThousandChars: number): SpeechModel<string> {\n return {\n id,\n name: id,\n api: 'fish-tts',\n provider: 'fish',\n baseUrl: fish,\n type: 'speech',\n textStreaming: true,\n outputFormats,\n cloning: true,\n cost: tokenCost,\n audioCost: { perThousandChars },\n };\n}\n\nfunction fishCatalog(): Array<TranscriptionModel<string> | SpeechModel<string>> {\n const batch: TranscriptionModel<string> = {\n id: 'transcribe-1-pro',\n name: 'transcribe-1-pro',\n api: 'fish-asr',\n provider: 'fish',\n baseUrl: fish,\n type: 'transcription',\n streaming: false,\n turnDetection: 'none',\n inputFormats: [\n container('wav', 16000),\n container('mp3', 16000),\n container('flac', 16000),\n container('opus', 16000),\n ],\n diarization: true,\n cost: tokenCost,\n audioCost: { perInputMinute: 0.36 / 60 },\n };\n return [batch, speechModel('s2.1-pro', 0.015), speechModel('s2.1-pro-free', 0)];\n}\n\nexport function registerFish(): () => void {\n const models = fishCatalog();\n const unregisterModels = models.map((model) => registerVoiceModel(model));\n const unregisterTranscription = registerTranscriptionApiProvider({\n api: 'fish-asr',\n transcribe: transcribeFish,\n });\n const unregisterSpeech = registerSpeechApiProvider({\n api: 'fish-tts',\n speak: speakFish,\n });\n let released = false;\n return () => {\n if (released) return;\n released = true;\n for (const unregister of unregisterModels) unregister();\n unregisterTranscription();\n unregisterSpeech();\n };\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,0BAAqE;AAC9E,SAAS,2BAA2B,wCAAwC;AAC5E,SAAS,iBAAiB;AAC1B,SAAS,sBAAsB;AAE/B,MAAM,YAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AACrE,MAAM,OAAO;AAEb,MAAM,YAAY,CAChB,UACA,gBACiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA,UAAU;AACZ;AAEA,MAAM,gBAAgB;AAAA,EACpB,UAAU,SAAS,KAAK;AAAA,EACxB,UAAU,OAAO,KAAK;AAAA,EACtB,UAAU,OAAO,KAAK;AAAA,EACtB,UAAU,QAAQ,IAAK;AACzB;AAEA,SAAS,YAAY,IAAY,kBAA+C;AAC9E,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,eAAe;AAAA,IACf;AAAA,IACA,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW,EAAE,iBAAiB;AAAA,EAChC;AACF;AAEA,SAAS,cAAuE;AAC9E,QAAM,QAAoC;AAAA,IACxC,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,IACX,eAAe;AAAA,IACf,cAAc;AAAA,MACZ,UAAU,OAAO,IAAK;AAAA,MACtB,UAAU,OAAO,IAAK;AAAA,MACtB,UAAU,QAAQ,IAAK;AAAA,MACvB,UAAU,QAAQ,IAAK;AAAA,IACzB;AAAA,IACA,aAAa;AAAA,IACb,MAAM;AAAA,IACN,WAAW,EAAE,gBAAgB,OAAO,GAAG;AAAA,EACzC;AACA,SAAO,CAAC,OAAO,YAAY,YAAY,KAAK,GAAG,YAAY,iBAAiB,CAAC,CAAC;AAChF;AAEO,SAAS,eAA2B;AACzC,QAAM,SAAS,YAAY;AAC3B,QAAM,mBAAmB,OAAO,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC;AACxE,QAAM,0BAA0B,iCAAiC;AAAA,IAC/D,KAAK;AAAA,IACL,YAAY;AAAA,EACd,CAAC;AACD,QAAM,mBAAmB,0BAA0B;AAAA,IACjD,KAAK;AAAA,IACL,OAAO;AAAA,EACT,CAAC;AACD,MAAI,WAAW;AACf,SAAO,MAAM;AACX,QAAI,SAAU;AACd,eAAW;AACX,eAAW,cAAc,iBAAkB,YAAW;AACtD,4BAAwB;AACxB,qBAAiB;AAAA,EACnB;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { chunkSeconds } from "../audio.js";
|
|
2
|
+
import { VoiceRequestError } from "../errors.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 {
|
|
14
|
+
awaitOpen,
|
|
15
|
+
connectSocket,
|
|
16
|
+
fishHeaders,
|
|
17
|
+
messageBytes,
|
|
18
|
+
pack,
|
|
19
|
+
sameFormat,
|
|
20
|
+
socketUrl,
|
|
21
|
+
speechFormatName,
|
|
22
|
+
unpack
|
|
23
|
+
} from "./wire.js";
|
|
24
|
+
function languageCode(language) {
|
|
25
|
+
if (language === void 0) return void 0;
|
|
26
|
+
if (language.trim() === "") return void 0;
|
|
27
|
+
return language;
|
|
28
|
+
}
|
|
29
|
+
function resolveFormat(model, requested) {
|
|
30
|
+
const fallback = model.outputFormats[0];
|
|
31
|
+
if (!fallback) throw new Error("No output format");
|
|
32
|
+
if (!requested) return fallback;
|
|
33
|
+
const match = model.outputFormats.find((format) => sameFormat(format, requested));
|
|
34
|
+
if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
|
|
35
|
+
return match;
|
|
36
|
+
}
|
|
37
|
+
function speechBody(text, voice, language, format) {
|
|
38
|
+
const body = {
|
|
39
|
+
text,
|
|
40
|
+
reference_id: voice,
|
|
41
|
+
format: speechFormatName(format),
|
|
42
|
+
sample_rate: format.sampleRate,
|
|
43
|
+
latency: "normal"
|
|
44
|
+
};
|
|
45
|
+
const code = languageCode(language);
|
|
46
|
+
if (code !== void 0) body.language = code;
|
|
47
|
+
return body;
|
|
48
|
+
}
|
|
49
|
+
function startFrame(voice, language, format) {
|
|
50
|
+
const request = {
|
|
51
|
+
text: "",
|
|
52
|
+
format: speechFormatName(format),
|
|
53
|
+
reference_id: voice,
|
|
54
|
+
sample_rate: format.sampleRate,
|
|
55
|
+
latency: "balanced"
|
|
56
|
+
};
|
|
57
|
+
const code = languageCode(language);
|
|
58
|
+
if (code !== void 0) request.language = code;
|
|
59
|
+
return { event: "start", request };
|
|
60
|
+
}
|
|
61
|
+
async function readText(text, signal) {
|
|
62
|
+
if (typeof text === "string") return text;
|
|
63
|
+
let combined = "";
|
|
64
|
+
for await (const part of text) {
|
|
65
|
+
if (signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
66
|
+
combined += part;
|
|
67
|
+
}
|
|
68
|
+
return combined;
|
|
69
|
+
}
|
|
70
|
+
function errorText(text, status) {
|
|
71
|
+
if (text === "") return `HTTP ${status}`;
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(text);
|
|
75
|
+
} catch {
|
|
76
|
+
return text;
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) return text;
|
|
79
|
+
const message = stringField(parsed, "message");
|
|
80
|
+
if (message === void 0) return text;
|
|
81
|
+
return message;
|
|
82
|
+
}
|
|
83
|
+
async function fishFetch(model, options, url, body) {
|
|
84
|
+
const headers = fishHeaders(model, options);
|
|
85
|
+
headers["Content-Type"] = "application/json";
|
|
86
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
87
|
+
const response = await fetchImpl(url, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers,
|
|
90
|
+
body,
|
|
91
|
+
signal: requestSignal(options)
|
|
92
|
+
});
|
|
93
|
+
await options?.onResponse?.({ status: response.status, headers: {} }, model);
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const text = await response.text();
|
|
96
|
+
throw new Error(errorText(text, response.status));
|
|
97
|
+
}
|
|
98
|
+
return response;
|
|
99
|
+
}
|
|
100
|
+
function sendFrame(socket, value) {
|
|
101
|
+
const packed = pack(value);
|
|
102
|
+
const copy = new ArrayBuffer(packed.byteLength);
|
|
103
|
+
new Uint8Array(copy).set(packed);
|
|
104
|
+
socket.send(copy);
|
|
105
|
+
}
|
|
106
|
+
function speakFish(model, input, options) {
|
|
107
|
+
const stream = new SpeechEventStream();
|
|
108
|
+
void pumpSpeech(model, input, options, stream);
|
|
109
|
+
return stream;
|
|
110
|
+
}
|
|
111
|
+
async function pumpSpeech(model, input, options, stream) {
|
|
112
|
+
try {
|
|
113
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
114
|
+
const format = resolveFormat(model, input.format);
|
|
115
|
+
if (model.textStreaming && typeof input.text !== "string") {
|
|
116
|
+
await streamSpeech(model, input.text, input.voice, input.language, format, options, stream);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const text = await readText(input.text, options?.signal);
|
|
120
|
+
await postSpeech(model, text, input.voice, input.language, format, options, stream);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const voice = asVoiceError(error);
|
|
123
|
+
stream.push({ type: "error", reason: voice.reason, error: voice.error });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function postSpeech(model, text, voice, language, format, options, stream) {
|
|
127
|
+
const payload = speechBody(text, voice, language, format);
|
|
128
|
+
const body = await applyPayload(payload, model, options);
|
|
129
|
+
if (!isRecord(body) || Array.isArray(body)) throw new Error("Speech payload was not JSON");
|
|
130
|
+
const response = await fishFetch(
|
|
131
|
+
model,
|
|
132
|
+
options,
|
|
133
|
+
endpoint(model.baseUrl, "tts"),
|
|
134
|
+
JSON.stringify(body)
|
|
135
|
+
);
|
|
136
|
+
const data = new Uint8Array(await response.arrayBuffer());
|
|
137
|
+
if (data.byteLength === 0) throw new Error("Speech response had no audio");
|
|
138
|
+
const seconds = chunkSeconds({ data, format, timestamp: 0 });
|
|
139
|
+
const meters = { characters: text.length };
|
|
140
|
+
if (seconds > 0) meters.outputSeconds = seconds;
|
|
141
|
+
stream.push({ type: "start", format });
|
|
142
|
+
stream.push({
|
|
143
|
+
type: "audio",
|
|
144
|
+
chunk: { data, format, timestamp: 0 },
|
|
145
|
+
span: { itemId: model.id, start: 0, end: text.length }
|
|
146
|
+
});
|
|
147
|
+
stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
|
|
148
|
+
}
|
|
149
|
+
async function streamSpeech(model, text, voice, language, format, options, stream) {
|
|
150
|
+
const state = {
|
|
151
|
+
opened: false,
|
|
152
|
+
started: false,
|
|
153
|
+
done: false,
|
|
154
|
+
failed: false,
|
|
155
|
+
heard: false,
|
|
156
|
+
outputSeconds: 0,
|
|
157
|
+
characters: 0
|
|
158
|
+
};
|
|
159
|
+
let settled = false;
|
|
160
|
+
const push = (event) => {
|
|
161
|
+
if (settled) return;
|
|
162
|
+
if (event.type === "done" || event.type === "error") settled = true;
|
|
163
|
+
stream.push(event);
|
|
164
|
+
};
|
|
165
|
+
const doneWait = Promise.withResolvers();
|
|
166
|
+
const wake = (kind) => {
|
|
167
|
+
if (state.failed) doneWait.resolve();
|
|
168
|
+
if (kind === "finish" || kind === "close") doneWait.resolve();
|
|
169
|
+
};
|
|
170
|
+
const url = socketUrl(model.baseUrl, "tts/live");
|
|
171
|
+
const socket = connectSocket(url.toString(), fishHeaders(model, options));
|
|
172
|
+
let incoming = Promise.resolve();
|
|
173
|
+
socket.addEventListener("message", (event) => {
|
|
174
|
+
incoming = incoming.then(() => onMessage(event.data, model.id, format, state, push, wake));
|
|
175
|
+
});
|
|
176
|
+
socket.addEventListener("close", () => {
|
|
177
|
+
if (state.opened && !state.done && !state.failed) {
|
|
178
|
+
state.failed = true;
|
|
179
|
+
push({ type: "error", reason: "error", error: "Fish speech socket closed" });
|
|
180
|
+
}
|
|
181
|
+
wake("close");
|
|
182
|
+
});
|
|
183
|
+
try {
|
|
184
|
+
await awaitOpen(socket, "Fish speech", (message) => {
|
|
185
|
+
state.failed = true;
|
|
186
|
+
push({ type: "error", reason: "error", error: message });
|
|
187
|
+
wake("close");
|
|
188
|
+
});
|
|
189
|
+
if (state.failed) return;
|
|
190
|
+
state.opened = true;
|
|
191
|
+
sendFrame(socket, startFrame(voice, language, format));
|
|
192
|
+
state.characters = await writeText(socket, text, options?.signal);
|
|
193
|
+
if (state.failed) return;
|
|
194
|
+
sendFrame(socket, { event: "flush" });
|
|
195
|
+
sendFrame(socket, { event: "stop" });
|
|
196
|
+
if (!state.done) await doneWait.promise;
|
|
197
|
+
if (state.failed) return;
|
|
198
|
+
if (!state.heard) throw new Error("Speech response had no audio");
|
|
199
|
+
const meters = { characters: state.characters };
|
|
200
|
+
if (state.outputSeconds > 0) meters.outputSeconds = state.outputSeconds;
|
|
201
|
+
push({ type: "done", usage: priceAudio(model.audioCost, meters) });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const voice2 = asVoiceError(error);
|
|
204
|
+
state.failed = true;
|
|
205
|
+
push({ type: "error", reason: voice2.reason, error: voice2.error });
|
|
206
|
+
} finally {
|
|
207
|
+
socket.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async function writeText(socket, text, signal) {
|
|
211
|
+
let combined = "";
|
|
212
|
+
for await (const part of text) {
|
|
213
|
+
if (signal?.aborted) throw new VoiceRequestError("aborted", "aborted");
|
|
214
|
+
combined += part;
|
|
215
|
+
if (part === "") continue;
|
|
216
|
+
sendFrame(socket, { event: "text", text: part });
|
|
217
|
+
}
|
|
218
|
+
return combined.length;
|
|
219
|
+
}
|
|
220
|
+
async function onMessage(data, itemId, format, state, push, wake) {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = unpack(await messageBytes(data));
|
|
223
|
+
if (!isRecord(parsed) || Array.isArray(parsed) || parsed instanceof Uint8Array) {
|
|
224
|
+
throw new Error("Fish speech event was not a map");
|
|
225
|
+
}
|
|
226
|
+
const kind = stringField(parsed, "event");
|
|
227
|
+
applySpeech(parsed, kind, itemId, format, state, push);
|
|
228
|
+
wake(kind);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const voice = asVoiceError(error);
|
|
231
|
+
state.failed = true;
|
|
232
|
+
push({ type: "error", reason: voice.reason, error: voice.error });
|
|
233
|
+
wake("close");
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function applySpeech(event, kind, itemId, format, state, push) {
|
|
237
|
+
if (kind === "error") {
|
|
238
|
+
const message = stringField(event, "message");
|
|
239
|
+
state.failed = true;
|
|
240
|
+
push({
|
|
241
|
+
type: "error",
|
|
242
|
+
reason: "error",
|
|
243
|
+
error: message === void 0 ? "Fish speech failed" : message
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (kind === "finish") {
|
|
248
|
+
state.done = true;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (kind !== "audio") return;
|
|
252
|
+
const audio = event.audio;
|
|
253
|
+
if (!(audio instanceof Uint8Array) || audio.byteLength === 0) return;
|
|
254
|
+
if (!state.started) {
|
|
255
|
+
state.started = true;
|
|
256
|
+
push({ type: "start", format });
|
|
257
|
+
}
|
|
258
|
+
state.heard = true;
|
|
259
|
+
state.outputSeconds += chunkSeconds({ data: audio, format, timestamp: 0 });
|
|
260
|
+
push({
|
|
261
|
+
type: "audio",
|
|
262
|
+
chunk: { data: audio, format, timestamp: 0 },
|
|
263
|
+
span: { itemId, start: 0, end: state.characters }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
speakFish
|
|
268
|
+
};
|
|
269
|
+
//# sourceMappingURL=speak.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish/speak.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport { VoiceRequestError } from '../errors.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 { type SpeechEvent, SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport {\n awaitOpen,\n connectSocket,\n fishHeaders,\n messageBytes,\n type PackValue,\n pack,\n sameFormat,\n socketUrl,\n speechFormatName,\n unpack,\n} from './wire.ts';\n\ninterface SpeechState {\n opened: boolean;\n started: boolean;\n done: boolean;\n failed: boolean;\n heard: boolean;\n outputSeconds: number;\n characters: number;\n}\n\nfunction languageCode(language: string | undefined): string | undefined {\n if (language === undefined) return undefined;\n if (language.trim() === '') return undefined;\n return language;\n}\n\nfunction resolveFormat(\n model: SpeechModel<string>,\n requested: AudioFormat | undefined\n): AudioFormat {\n const fallback = model.outputFormats[0];\n if (!fallback) throw new Error('No output format');\n if (!requested) return fallback;\n const match = model.outputFormats.find((format) => sameFormat(format, requested));\n if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);\n return match;\n}\n\nfunction speechBody(\n text: string,\n voice: string,\n language: string | undefined,\n format: AudioFormat\n): Record<string, unknown> {\n const body: Record<string, unknown> = {\n text,\n reference_id: voice,\n format: speechFormatName(format),\n sample_rate: format.sampleRate,\n latency: 'normal',\n };\n const code = languageCode(language);\n if (code !== undefined) body.language = code;\n return body;\n}\n\nfunction startFrame(voice: string, language: string | undefined, format: AudioFormat): PackValue {\n const request: { [key: string]: PackValue } = {\n text: '',\n format: speechFormatName(format),\n reference_id: voice,\n sample_rate: format.sampleRate,\n latency: 'balanced',\n };\n const code = languageCode(language);\n if (code !== undefined) request.language = code;\n return { event: 'start', request };\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 errorText(text: string, status: number): string {\n if (text === '') return `HTTP ${status}`;\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return text;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) return text;\n const message = stringField(parsed, 'message');\n if (message === undefined) return text;\n return message;\n}\n\nasync function fishFetch(\n model: SpeechModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: string\n): Promise<Response> {\n const headers = fishHeaders(model, options);\n headers['Content-Type'] = 'application/json';\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n signal: requestSignal(options),\n });\n await options?.onResponse?.({ status: response.status, headers: {} }, model as never);\n if (!response.ok) {\n const text = await response.text();\n throw new Error(errorText(text, response.status));\n }\n return response;\n}\n\nfunction sendFrame(socket: WebSocket, value: PackValue): void {\n const packed = pack(value);\n const copy = new ArrayBuffer(packed.byteLength);\n new Uint8Array(copy).set(packed);\n socket.send(copy);\n}\n\nexport function speakFish(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n void pumpSpeech(model, input, options, stream);\n return stream;\n}\n\nasync function pumpSpeech(\n model: SpeechModel<string>,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = resolveFormat(model, input.format);\n if (model.textStreaming && typeof input.text !== 'string') {\n await streamSpeech(model, input.text, input.voice, input.language, format, options, stream);\n return;\n }\n const text = await readText(input.text, options?.signal);\n await postSpeech(model, text, input.voice, input.language, format, options, stream);\n } catch (error) {\n const voice = asVoiceError(error);\n stream.push({ type: 'error', reason: voice.reason, error: voice.error });\n }\n}\n\nasync function postSpeech(\n model: SpeechModel<string>,\n text: string,\n voice: string,\n language: string | undefined,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const payload = speechBody(text, voice, language, format);\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body) || Array.isArray(body)) throw new Error('Speech payload was not JSON');\n const response = await fishFetch(\n model,\n options,\n endpoint(model.baseUrl, 'tts'),\n JSON.stringify(body)\n );\n const data = new Uint8Array(await response.arrayBuffer());\n if (data.byteLength === 0) throw new Error('Speech response had no audio');\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 | undefined,\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 heard: 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 = (kind: string | undefined) => {\n if (state.failed) doneWait.resolve();\n if (kind === 'finish' || kind === 'close') doneWait.resolve();\n };\n const url = socketUrl(model.baseUrl, 'tts/live');\n const socket = connectSocket(url.toString(), fishHeaders(model, options));\n let incoming = Promise.resolve();\n socket.addEventListener('message', (event) => {\n incoming = incoming.then(() => onMessage(event.data, model.id, format, state, push, wake));\n });\n socket.addEventListener('close', () => {\n if (state.opened && !state.done && !state.failed) {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: 'Fish speech socket closed' });\n }\n wake('close');\n });\n try {\n await awaitOpen(socket, 'Fish speech', (message) => {\n state.failed = true;\n push({ type: 'error', reason: 'error', error: message });\n wake('close');\n });\n if (state.failed) return;\n state.opened = true;\n sendFrame(socket, startFrame(voice, language, format));\n state.characters = await writeText(socket, text, options?.signal);\n if (state.failed) return;\n sendFrame(socket, { event: 'flush' });\n sendFrame(socket, { event: 'stop' });\n if (!state.done) await doneWait.promise;\n if (state.failed) return;\n if (!state.heard) throw new Error('Speech response had no audio');\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 === '') continue;\n sendFrame(socket, { event: 'text', text: 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: (kind: string | undefined) => void\n) {\n try {\n const parsed = unpack(await messageBytes(data));\n if (!isRecord(parsed) || Array.isArray(parsed) || parsed instanceof Uint8Array) {\n throw new Error('Fish speech event was not a map');\n }\n const kind = stringField(parsed, 'event');\n applySpeech(parsed, kind, itemId, format, state, push);\n wake(kind);\n } catch (error) {\n const voice = asVoiceError(error);\n state.failed = true;\n push({ type: 'error', reason: voice.reason, error: voice.error });\n wake('close');\n }\n}\n\nfunction applySpeech(\n event: Record<string, unknown>,\n kind: string | undefined,\n itemId: string,\n format: AudioFormat,\n state: SpeechState,\n push: (event: SpeechEvent) => void\n) {\n if (kind === 'error') {\n const message = stringField(event, 'message');\n state.failed = true;\n push({\n type: 'error',\n reason: 'error',\n error: message === undefined ? 'Fish speech failed' : message,\n });\n return;\n }\n if (kind === 'finish') {\n state.done = true;\n return;\n }\n if (kind !== 'audio') return;\n const audio = event.audio;\n if (!(audio instanceof Uint8Array) || audio.byteLength === 0) return;\n if (!state.started) {\n state.started = true;\n push({ type: 'start', format });\n }\n state.heard = true;\n state.outputSeconds += chunkSeconds({ data: audio, format, timestamp: 0 });\n push({\n type: 'audio',\n chunk: { data: audio, format, timestamp: 0 },\n span: { itemId, start: 0, end: state.characters },\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["voice"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { VoiceRequestError } from "../errors.js";
|
|
2
|
+
import {
|
|
3
|
+
applyPayload,
|
|
4
|
+
asVoiceError,
|
|
5
|
+
endpoint,
|
|
6
|
+
isRecord,
|
|
7
|
+
numberField,
|
|
8
|
+
readAudio,
|
|
9
|
+
readJson,
|
|
10
|
+
requestSignal,
|
|
11
|
+
stringField
|
|
12
|
+
} from "../openai/wire.js";
|
|
13
|
+
import { priceAudio } from "../usage.js";
|
|
14
|
+
import { audioFilename, encodeForm, fishHeaders, 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 duration = numberField(body, "duration");
|
|
27
|
+
if (duration === void 0) return 0;
|
|
28
|
+
return duration;
|
|
29
|
+
}
|
|
30
|
+
function languageOf(body) {
|
|
31
|
+
const code = stringField(body, "language_code");
|
|
32
|
+
if (code !== void 0) return code;
|
|
33
|
+
return stringField(body, "language");
|
|
34
|
+
}
|
|
35
|
+
function segmentFrom(word, withSpeaker) {
|
|
36
|
+
const text = textOf(word, "text");
|
|
37
|
+
if (text === "") return void 0;
|
|
38
|
+
const segment = {
|
|
39
|
+
text,
|
|
40
|
+
start: secondsOf(word, "start"),
|
|
41
|
+
end: secondsOf(word, "end")
|
|
42
|
+
};
|
|
43
|
+
if (!withSpeaker) return segment;
|
|
44
|
+
const speaker = stringField(word, "speaker");
|
|
45
|
+
if (speaker !== void 0) segment.speaker = speaker;
|
|
46
|
+
return segment;
|
|
47
|
+
}
|
|
48
|
+
function segmentsFrom(value, withSpeaker) {
|
|
49
|
+
if (!Array.isArray(value)) return [];
|
|
50
|
+
const segments = [];
|
|
51
|
+
for (const word of value) {
|
|
52
|
+
if (!isRecord(word) || Array.isArray(word)) continue;
|
|
53
|
+
const segment = segmentFrom(word, withSpeaker);
|
|
54
|
+
if (segment) segments.push(segment);
|
|
55
|
+
}
|
|
56
|
+
return segments;
|
|
57
|
+
}
|
|
58
|
+
function timedFrom(body) {
|
|
59
|
+
const turns = segmentsFrom(body.speaker_turns, true);
|
|
60
|
+
if (turns.length > 0) return turns;
|
|
61
|
+
return segmentsFrom(body.segments, false);
|
|
62
|
+
}
|
|
63
|
+
function transcriptFrom(body, model) {
|
|
64
|
+
if (!isRecord(body) || Array.isArray(body)) throw new Error("Transcription response had no text");
|
|
65
|
+
const direct = stringField(body, "text");
|
|
66
|
+
if (direct === void 0) throw new Error("Transcription response had no text");
|
|
67
|
+
const timed = timedFrom(body);
|
|
68
|
+
const language = languageOf(body);
|
|
69
|
+
return {
|
|
70
|
+
text: direct,
|
|
71
|
+
...language !== void 0 ? { language } : {},
|
|
72
|
+
...timed.length > 0 ? { segments: timed } : {},
|
|
73
|
+
usage: priceAudio(model.audioCost, { inputSeconds: durationOf(body) })
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function fileOf(body) {
|
|
77
|
+
const file = body.audio;
|
|
78
|
+
if (!isRecord(file) || Array.isArray(file) || !(file.data instanceof Uint8Array) || typeof file.mimeType !== "string") {
|
|
79
|
+
throw new Error("Transcription payload was not a file");
|
|
80
|
+
}
|
|
81
|
+
const named = typeof file.filename === "string" ? file.filename : audioFilename(file.mimeType);
|
|
82
|
+
return { data: file.data, mimeType: file.mimeType, filename: named };
|
|
83
|
+
}
|
|
84
|
+
function fieldStrings(body) {
|
|
85
|
+
const fields = {};
|
|
86
|
+
for (const [key, value] of Object.entries(body)) {
|
|
87
|
+
if (key === "audio") continue;
|
|
88
|
+
if (typeof value !== "string") continue;
|
|
89
|
+
fields[key] = value;
|
|
90
|
+
}
|
|
91
|
+
return fields;
|
|
92
|
+
}
|
|
93
|
+
function errorText(text, status) {
|
|
94
|
+
if (text === "") return `HTTP ${status}`;
|
|
95
|
+
let parsed;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(text);
|
|
98
|
+
} catch {
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) return text;
|
|
102
|
+
const message = stringField(parsed, "message");
|
|
103
|
+
if (message === void 0) return text;
|
|
104
|
+
return message;
|
|
105
|
+
}
|
|
106
|
+
async function fishFetch(model, options, url, body, contentType) {
|
|
107
|
+
const headers = fishHeaders(model, options);
|
|
108
|
+
headers["Content-Type"] = contentType;
|
|
109
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
110
|
+
const response = await fetchImpl(url, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers,
|
|
113
|
+
body,
|
|
114
|
+
signal: requestSignal(options)
|
|
115
|
+
});
|
|
116
|
+
await options?.onResponse?.({ status: response.status, headers: {} }, model);
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
const text = await response.text();
|
|
119
|
+
throw new Error(errorText(text, response.status));
|
|
120
|
+
}
|
|
121
|
+
return response;
|
|
122
|
+
}
|
|
123
|
+
async function transcribeFish(model, input, options) {
|
|
124
|
+
try {
|
|
125
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
126
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
127
|
+
const loaded = await readAudio(input, fetchImpl, requestSignal(options));
|
|
128
|
+
const payload = {
|
|
129
|
+
ignore_timestamps: "false",
|
|
130
|
+
diarize: model.diarization ? "true" : "false",
|
|
131
|
+
audio: {
|
|
132
|
+
data: loaded.data,
|
|
133
|
+
mimeType: loaded.mimeType,
|
|
134
|
+
filename: audioFilename(loaded.mimeType)
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const body = await applyPayload(payload, model, options);
|
|
138
|
+
if (!isRecord(body) || Array.isArray(body))
|
|
139
|
+
throw new Error("Transcription payload was not a file");
|
|
140
|
+
const file = fileOf(body);
|
|
141
|
+
const encoded = encodeForm(file, fieldStrings(body));
|
|
142
|
+
const response = await fishFetch(
|
|
143
|
+
model,
|
|
144
|
+
options,
|
|
145
|
+
endpoint(model.baseUrl, "asr"),
|
|
146
|
+
encoded,
|
|
147
|
+
formType()
|
|
148
|
+
);
|
|
149
|
+
return transcriptFrom(await readJson(response), model);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const voice = asVoiceError(error);
|
|
152
|
+
throw new VoiceRequestError(voice.reason, voice.error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
transcribeFish
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=transcribe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish/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, encodeForm, type FishFile, fishHeaders, formType } 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 duration = numberField(body, 'duration');\n if (duration === undefined) return 0;\n return duration;\n}\n\nfunction languageOf(body: Record<string, unknown>): string | undefined {\n const code = stringField(body, 'language_code');\n if (code !== undefined) return code;\n return stringField(body, 'language');\n}\n\nfunction segmentFrom(\n word: Record<string, unknown>,\n withSpeaker: boolean\n): TranscriptSegment | undefined {\n const text = textOf(word, 'text');\n if (text === '') return undefined;\n const segment: TranscriptSegment = {\n text,\n start: secondsOf(word, 'start'),\n end: secondsOf(word, 'end'),\n };\n if (!withSpeaker) return segment;\n const speaker = stringField(word, 'speaker');\n if (speaker !== undefined) segment.speaker = speaker;\n return segment;\n}\n\nfunction segmentsFrom(value: unknown, withSpeaker: boolean): TranscriptSegment[] {\n if (!Array.isArray(value)) return [];\n const segments: TranscriptSegment[] = [];\n for (const word of value) {\n if (!isRecord(word) || Array.isArray(word)) continue;\n const segment = segmentFrom(word, withSpeaker);\n if (segment) segments.push(segment);\n }\n return segments;\n}\n\nfunction timedFrom(body: Record<string, unknown>): TranscriptSegment[] {\n const turns = segmentsFrom(body.speaker_turns, true);\n if (turns.length > 0) return turns;\n return segmentsFrom(body.segments, false);\n}\n\nfunction transcriptFrom(body: unknown, model: TranscriptionModel<string>): Transcript {\n if (!isRecord(body) || Array.isArray(body)) throw new Error('Transcription response had no text');\n const direct = stringField(body, 'text');\n if (direct === undefined) throw new Error('Transcription response had no text');\n const timed = timedFrom(body);\n const language = languageOf(body);\n return {\n text: direct,\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>): FishFile {\n const file = body.audio;\n if (\n !isRecord(file) ||\n Array.isArray(file) ||\n !(file.data instanceof Uint8Array) ||\n typeof file.mimeType !== 'string'\n ) {\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 === 'audio') continue;\n if (typeof value !== 'string') continue;\n fields[key] = value;\n }\n return fields;\n}\n\nfunction errorText(text: string, status: number): string {\n if (text === '') return `HTTP ${status}`;\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return text;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) return text;\n const message = stringField(parsed, 'message');\n if (message === undefined) return text;\n return message;\n}\n\nasync function fishFetch(\n model: TranscriptionModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: BodyInit,\n contentType: string\n): Promise<Response> {\n const headers = fishHeaders(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(errorText(text, response.status));\n }\n return response;\n}\n\nexport async function transcribeFish(\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 ignore_timestamps: 'false',\n diarize: model.diarization ? 'true' : 'false',\n audio: {\n data: loaded.data,\n mimeType: loaded.mimeType,\n filename: audioFilename(loaded.mimeType),\n },\n };\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body) || Array.isArray(body))\n 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 fishFetch(\n model,\n options,\n endpoint(model.baseUrl, 'asr'),\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,386 @@
|
|
|
1
|
+
import { requestHeaders } from "../openai/wire.js";
|
|
2
|
+
const boundary = "pi-audio-fish-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/webm": "audio.webm",
|
|
13
|
+
"audio/mp4": "audio.m4a",
|
|
14
|
+
"audio/m4a": "audio.m4a",
|
|
15
|
+
"audio/aac": "audio.aac"
|
|
16
|
+
};
|
|
17
|
+
const speechNames = {
|
|
18
|
+
pcm16: "pcm",
|
|
19
|
+
wav: "wav",
|
|
20
|
+
mp3: "mp3",
|
|
21
|
+
opus: "opus"
|
|
22
|
+
};
|
|
23
|
+
class ByteWriter {
|
|
24
|
+
#bytes = [];
|
|
25
|
+
byte(value) {
|
|
26
|
+
this.#bytes.push(value & 255);
|
|
27
|
+
}
|
|
28
|
+
u16(value) {
|
|
29
|
+
this.byte(value >> 8);
|
|
30
|
+
this.byte(value);
|
|
31
|
+
}
|
|
32
|
+
u32(value) {
|
|
33
|
+
this.byte(value >>> 24);
|
|
34
|
+
this.byte(value >>> 16);
|
|
35
|
+
this.byte(value >>> 8);
|
|
36
|
+
this.byte(value);
|
|
37
|
+
}
|
|
38
|
+
f64(value) {
|
|
39
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
40
|
+
view.setFloat64(0, value);
|
|
41
|
+
for (let index = 0; index < 8; index += 1) this.byte(view.getUint8(index));
|
|
42
|
+
}
|
|
43
|
+
raw(bytes) {
|
|
44
|
+
for (const byte of bytes) this.byte(byte);
|
|
45
|
+
}
|
|
46
|
+
toArray() {
|
|
47
|
+
return Uint8Array.from(this.#bytes);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class ByteReader {
|
|
51
|
+
#offset = 0;
|
|
52
|
+
#data;
|
|
53
|
+
constructor(data) {
|
|
54
|
+
this.#data = data;
|
|
55
|
+
}
|
|
56
|
+
byte() {
|
|
57
|
+
const value = this.#data[this.#offset];
|
|
58
|
+
if (value === void 0) throw new Error("Fish frame ended early");
|
|
59
|
+
this.#offset += 1;
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
u16() {
|
|
63
|
+
return this.byte() << 8 | this.byte();
|
|
64
|
+
}
|
|
65
|
+
u32() {
|
|
66
|
+
return (this.byte() << 24 | this.byte() << 16 | this.byte() << 8 | this.byte()) >>> 0;
|
|
67
|
+
}
|
|
68
|
+
i16() {
|
|
69
|
+
const value = this.u16();
|
|
70
|
+
return value > 32767 ? value - 65536 : value;
|
|
71
|
+
}
|
|
72
|
+
i32() {
|
|
73
|
+
return this.u32() | 0;
|
|
74
|
+
}
|
|
75
|
+
u64() {
|
|
76
|
+
const high = this.u32();
|
|
77
|
+
const low = this.u32();
|
|
78
|
+
return high * 4294967296 + low;
|
|
79
|
+
}
|
|
80
|
+
i64() {
|
|
81
|
+
const high = this.i32();
|
|
82
|
+
const low = this.u32();
|
|
83
|
+
return high * 4294967296 + low;
|
|
84
|
+
}
|
|
85
|
+
f32() {
|
|
86
|
+
const copy = new Uint8Array(4);
|
|
87
|
+
copy.set(this.bytes(4));
|
|
88
|
+
return new DataView(copy.buffer).getFloat32(0);
|
|
89
|
+
}
|
|
90
|
+
f64() {
|
|
91
|
+
const copy = new Uint8Array(8);
|
|
92
|
+
copy.set(this.bytes(8));
|
|
93
|
+
return new DataView(copy.buffer).getFloat64(0);
|
|
94
|
+
}
|
|
95
|
+
bytes(length) {
|
|
96
|
+
const start = this.#offset;
|
|
97
|
+
if (start + length > this.#data.length) throw new Error("Fish frame ended early");
|
|
98
|
+
this.#offset = start + length;
|
|
99
|
+
return this.#data.subarray(start, this.#offset);
|
|
100
|
+
}
|
|
101
|
+
done() {
|
|
102
|
+
return this.#offset === this.#data.length;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function packString(value, out) {
|
|
106
|
+
const bytes = new TextEncoder().encode(value);
|
|
107
|
+
if (bytes.length < 32) out.byte(160 | bytes.length);
|
|
108
|
+
else if (bytes.length < 256) {
|
|
109
|
+
out.byte(217);
|
|
110
|
+
out.byte(bytes.length);
|
|
111
|
+
} else if (bytes.length < 65536) {
|
|
112
|
+
out.byte(218);
|
|
113
|
+
out.u16(bytes.length);
|
|
114
|
+
} else throw new Error("String too long");
|
|
115
|
+
out.raw(bytes);
|
|
116
|
+
}
|
|
117
|
+
function packBin(value, out) {
|
|
118
|
+
if (value.byteLength < 256) {
|
|
119
|
+
out.byte(196);
|
|
120
|
+
out.byte(value.byteLength);
|
|
121
|
+
} else if (value.byteLength < 65536) {
|
|
122
|
+
out.byte(197);
|
|
123
|
+
out.u16(value.byteLength);
|
|
124
|
+
} else throw new Error("Binary too long");
|
|
125
|
+
out.raw(value);
|
|
126
|
+
}
|
|
127
|
+
function packNumber(value, out) {
|
|
128
|
+
if (!Number.isInteger(value)) {
|
|
129
|
+
out.byte(203);
|
|
130
|
+
out.f64(value);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (value >= 0 && value <= 127) {
|
|
134
|
+
out.byte(value);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (value < 0 && value >= -32) {
|
|
138
|
+
out.byte(value & 255);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (value >= 0 && value <= 255) {
|
|
142
|
+
out.byte(204);
|
|
143
|
+
out.byte(value);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (value >= 0 && value <= 65535) {
|
|
147
|
+
out.byte(205);
|
|
148
|
+
out.u16(value);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (value >= 0 && value <= 4294967295) {
|
|
152
|
+
out.byte(206);
|
|
153
|
+
out.u32(value);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (value < 0 && value >= -128) {
|
|
157
|
+
out.byte(208);
|
|
158
|
+
out.byte(value & 255);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
throw new Error("Integer out of range");
|
|
162
|
+
}
|
|
163
|
+
function packArray(values, out) {
|
|
164
|
+
if (values.length < 16) out.byte(144 | values.length);
|
|
165
|
+
else if (values.length < 65536) {
|
|
166
|
+
out.byte(220);
|
|
167
|
+
out.u16(values.length);
|
|
168
|
+
} else throw new Error("Array too long");
|
|
169
|
+
for (const value of values) packInto(value, out);
|
|
170
|
+
}
|
|
171
|
+
function packMap(value, out) {
|
|
172
|
+
const entries = Object.entries(value);
|
|
173
|
+
if (entries.length < 16) out.byte(128 | entries.length);
|
|
174
|
+
else if (entries.length < 65536) {
|
|
175
|
+
out.byte(222);
|
|
176
|
+
out.u16(entries.length);
|
|
177
|
+
} else throw new Error("Map too long");
|
|
178
|
+
for (const [key, item] of entries) {
|
|
179
|
+
packString(key, out);
|
|
180
|
+
packInto(item, out);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function packInto(value, out) {
|
|
184
|
+
if (value === null) {
|
|
185
|
+
out.byte(192);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (typeof value === "boolean") {
|
|
189
|
+
out.byte(value ? 195 : 194);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (typeof value === "number") {
|
|
193
|
+
packNumber(value, out);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (typeof value === "string") {
|
|
197
|
+
packString(value, out);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (value instanceof Uint8Array) {
|
|
201
|
+
packBin(value, out);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(value)) {
|
|
205
|
+
packArray(value, out);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
packMap(value, out);
|
|
209
|
+
}
|
|
210
|
+
function unpackString(reader, length) {
|
|
211
|
+
return new TextDecoder().decode(reader.bytes(length));
|
|
212
|
+
}
|
|
213
|
+
function unpackArray(reader, length) {
|
|
214
|
+
const values = [];
|
|
215
|
+
for (let index = 0; index < length; index += 1) values.push(unpackOne(reader));
|
|
216
|
+
return values;
|
|
217
|
+
}
|
|
218
|
+
function unpackMap(reader, length) {
|
|
219
|
+
const value = {};
|
|
220
|
+
for (let index = 0; index < length; index += 1) {
|
|
221
|
+
const key = unpackOne(reader);
|
|
222
|
+
if (typeof key !== "string") throw new Error("Fish frame key was not a string");
|
|
223
|
+
value[key] = unpackOne(reader);
|
|
224
|
+
}
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
function unpackSized(reader, marker) {
|
|
228
|
+
if (marker === 192) return null;
|
|
229
|
+
if (marker === 194) return false;
|
|
230
|
+
if (marker === 195) return true;
|
|
231
|
+
if (marker === 196) return reader.bytes(reader.byte());
|
|
232
|
+
if (marker === 197) return reader.bytes(reader.u16());
|
|
233
|
+
if (marker === 198) return reader.bytes(reader.u32());
|
|
234
|
+
if (marker === 202) return reader.f32();
|
|
235
|
+
if (marker === 203) return reader.f64();
|
|
236
|
+
if (marker === 204) return reader.byte();
|
|
237
|
+
if (marker === 205) return reader.u16();
|
|
238
|
+
if (marker === 206) return reader.u32();
|
|
239
|
+
if (marker === 207) return reader.u64();
|
|
240
|
+
if (marker === 208) return reader.byte() << 24 >> 24;
|
|
241
|
+
if (marker === 209) return reader.i16();
|
|
242
|
+
if (marker === 210) return reader.i32();
|
|
243
|
+
if (marker === 211) return reader.i64();
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
function unpackOne(reader) {
|
|
247
|
+
const marker = reader.byte();
|
|
248
|
+
if (marker <= 127) return marker;
|
|
249
|
+
if (marker >= 224) return marker - 256;
|
|
250
|
+
const sized = unpackSized(reader, marker);
|
|
251
|
+
if (sized !== void 0) return sized;
|
|
252
|
+
if ((marker & 224) === 160) return unpackString(reader, marker & 31);
|
|
253
|
+
if (marker === 217) return unpackString(reader, reader.byte());
|
|
254
|
+
if (marker === 218) return unpackString(reader, reader.u16());
|
|
255
|
+
if (marker === 219) return unpackString(reader, reader.u32());
|
|
256
|
+
if ((marker & 240) === 144) return unpackArray(reader, marker & 15);
|
|
257
|
+
if (marker === 220) return unpackArray(reader, reader.u16());
|
|
258
|
+
if (marker === 221) return unpackArray(reader, reader.u32());
|
|
259
|
+
if ((marker & 240) === 128) return unpackMap(reader, marker & 15);
|
|
260
|
+
if (marker === 222) return unpackMap(reader, reader.u16());
|
|
261
|
+
if (marker === 223) return unpackMap(reader, reader.u32());
|
|
262
|
+
throw new Error("Unsupported Fish frame");
|
|
263
|
+
}
|
|
264
|
+
function pack(value) {
|
|
265
|
+
const out = new ByteWriter();
|
|
266
|
+
packInto(value, out);
|
|
267
|
+
return out.toArray();
|
|
268
|
+
}
|
|
269
|
+
function unpack(data) {
|
|
270
|
+
const reader = new ByteReader(data);
|
|
271
|
+
const value = unpackOne(reader);
|
|
272
|
+
if (!reader.done()) throw new Error("Fish frame had trailing bytes");
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
function mediaType(mimeType) {
|
|
276
|
+
const type = mimeType.split(";")[0]?.trim().toLowerCase();
|
|
277
|
+
if (!type) throw new Error(`Unsupported audio type ${mimeType}`);
|
|
278
|
+
return type;
|
|
279
|
+
}
|
|
280
|
+
function audioFilename(mimeType) {
|
|
281
|
+
const name = filenames[mediaType(mimeType)];
|
|
282
|
+
if (!name) throw new Error(`Unsupported audio type ${mimeType}`);
|
|
283
|
+
return name;
|
|
284
|
+
}
|
|
285
|
+
function encodeForm(file, fields) {
|
|
286
|
+
const chunks = [];
|
|
287
|
+
const encoder = new TextEncoder();
|
|
288
|
+
const write = (value) => {
|
|
289
|
+
chunks.push(encoder.encode(value));
|
|
290
|
+
};
|
|
291
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
292
|
+
write(`--${boundary}\r
|
|
293
|
+
Content-Disposition: form-data; name="${name}"\r
|
|
294
|
+
\r
|
|
295
|
+
${value}\r
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
const header = `--${boundary}\r
|
|
299
|
+
Content-Disposition: form-data; name="audio"; filename="${file.filename}"\r
|
|
300
|
+
Content-Type: ${file.mimeType}\r
|
|
301
|
+
\r
|
|
302
|
+
`;
|
|
303
|
+
write(header);
|
|
304
|
+
chunks.push(file.data);
|
|
305
|
+
write(`\r
|
|
306
|
+
--${boundary}--\r
|
|
307
|
+
`);
|
|
308
|
+
const body = new Uint8Array(chunks.reduce((sum, chunk) => sum + chunk.length, 0));
|
|
309
|
+
let offset = 0;
|
|
310
|
+
for (const chunk of chunks) {
|
|
311
|
+
body.set(chunk, offset);
|
|
312
|
+
offset += chunk.length;
|
|
313
|
+
}
|
|
314
|
+
return body.buffer;
|
|
315
|
+
}
|
|
316
|
+
function formType() {
|
|
317
|
+
return `multipart/form-data; boundary=${boundary}`;
|
|
318
|
+
}
|
|
319
|
+
function fishHeaders(model, options) {
|
|
320
|
+
const headers = requestHeaders(model, options);
|
|
321
|
+
headers.model = model.id;
|
|
322
|
+
return headers;
|
|
323
|
+
}
|
|
324
|
+
function socketUrl(baseUrl, path) {
|
|
325
|
+
const url = new URL(baseUrl);
|
|
326
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
327
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/${path}`;
|
|
328
|
+
url.search = "";
|
|
329
|
+
return url;
|
|
330
|
+
}
|
|
331
|
+
function speechFormatName(format) {
|
|
332
|
+
const name = speechNames[format.encoding];
|
|
333
|
+
if (!name) throw new Error(`Unsupported speech format ${format.encoding}`);
|
|
334
|
+
return name;
|
|
335
|
+
}
|
|
336
|
+
function connectSocket(url, headers) {
|
|
337
|
+
const Socket = WebSocket;
|
|
338
|
+
return new Socket(url, { headers });
|
|
339
|
+
}
|
|
340
|
+
function awaitOpen(socket, label, fail) {
|
|
341
|
+
return new Promise((resolve) => {
|
|
342
|
+
let opened = false;
|
|
343
|
+
socket.addEventListener("open", () => {
|
|
344
|
+
opened = true;
|
|
345
|
+
resolve();
|
|
346
|
+
});
|
|
347
|
+
socket.addEventListener("error", () => {
|
|
348
|
+
fail(`${label} socket failed`);
|
|
349
|
+
if (!opened) resolve();
|
|
350
|
+
});
|
|
351
|
+
socket.addEventListener("close", () => {
|
|
352
|
+
if (!opened) {
|
|
353
|
+
fail(`${label} socket closed`);
|
|
354
|
+
resolve();
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
async function messageBytes(data) {
|
|
360
|
+
if (typeof data === "string") return new TextEncoder().encode(data);
|
|
361
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
362
|
+
if (data instanceof Uint8Array) return data;
|
|
363
|
+
if (typeof Blob !== "undefined" && data instanceof Blob) {
|
|
364
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
365
|
+
}
|
|
366
|
+
throw new Error("Unsupported socket message");
|
|
367
|
+
}
|
|
368
|
+
function sameFormat(left, right) {
|
|
369
|
+
return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
|
|
370
|
+
}
|
|
371
|
+
export {
|
|
372
|
+
audioFilename,
|
|
373
|
+
awaitOpen,
|
|
374
|
+
connectSocket,
|
|
375
|
+
encodeForm,
|
|
376
|
+
fishHeaders,
|
|
377
|
+
formType,
|
|
378
|
+
mediaType,
|
|
379
|
+
messageBytes,
|
|
380
|
+
pack,
|
|
381
|
+
sameFormat,
|
|
382
|
+
socketUrl,
|
|
383
|
+
speechFormatName,
|
|
384
|
+
unpack
|
|
385
|
+
};
|
|
386
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/fish/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-fish-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/webm': 'audio.webm',\n 'audio/mp4': 'audio.m4a',\n 'audio/m4a': 'audio.m4a',\n 'audio/aac': 'audio.aac',\n};\n\nconst speechNames: Record<string, string> = {\n pcm16: 'pcm',\n wav: 'wav',\n mp3: 'mp3',\n opus: 'opus',\n};\n\nexport type PackValue =\n | null\n | boolean\n | number\n | string\n | Uint8Array\n | PackValue[]\n | { [key: string]: PackValue };\n\nexport interface FishFile {\n data: Uint8Array;\n mimeType: string;\n filename: string;\n}\n\nclass ByteWriter {\n readonly #bytes: number[] = [];\n\n byte(value: number): void {\n this.#bytes.push(value & 0xff);\n }\n\n u16(value: number): void {\n this.byte(value >> 8);\n this.byte(value);\n }\n\n u32(value: number): void {\n this.byte(value >>> 24);\n this.byte(value >>> 16);\n this.byte(value >>> 8);\n this.byte(value);\n }\n\n f64(value: number): void {\n const view = new DataView(new ArrayBuffer(8));\n view.setFloat64(0, value);\n for (let index = 0; index < 8; index += 1) this.byte(view.getUint8(index));\n }\n\n raw(bytes: Uint8Array): void {\n for (const byte of bytes) this.byte(byte);\n }\n\n toArray(): Uint8Array {\n return Uint8Array.from(this.#bytes);\n }\n}\n\nclass ByteReader {\n #offset = 0;\n readonly #data: Uint8Array;\n\n constructor(data: Uint8Array) {\n this.#data = data;\n }\n\n byte(): number {\n const value = this.#data[this.#offset];\n if (value === undefined) throw new Error('Fish frame ended early');\n this.#offset += 1;\n return value;\n }\n\n u16(): number {\n return (this.byte() << 8) | this.byte();\n }\n\n u32(): number {\n return ((this.byte() << 24) | (this.byte() << 16) | (this.byte() << 8) | this.byte()) >>> 0;\n }\n\n i16(): number {\n const value = this.u16();\n return value > 0x7fff ? value - 0x10000 : value;\n }\n\n i32(): number {\n return this.u32() | 0;\n }\n\n u64(): number {\n const high = this.u32();\n const low = this.u32();\n return high * 0x100000000 + low;\n }\n\n i64(): number {\n const high = this.i32();\n const low = this.u32();\n return high * 0x100000000 + low;\n }\n\n f32(): number {\n const copy = new Uint8Array(4);\n copy.set(this.bytes(4));\n return new DataView(copy.buffer).getFloat32(0);\n }\n\n f64(): number {\n const copy = new Uint8Array(8);\n copy.set(this.bytes(8));\n return new DataView(copy.buffer).getFloat64(0);\n }\n\n bytes(length: number): Uint8Array {\n const start = this.#offset;\n if (start + length > this.#data.length) throw new Error('Fish frame ended early');\n this.#offset = start + length;\n return this.#data.subarray(start, this.#offset);\n }\n\n done(): boolean {\n return this.#offset === this.#data.length;\n }\n}\n\nfunction packString(value: string, out: ByteWriter): void {\n const bytes = new TextEncoder().encode(value);\n if (bytes.length < 32) out.byte(0xa0 | bytes.length);\n else if (bytes.length < 256) {\n out.byte(0xd9);\n out.byte(bytes.length);\n } else if (bytes.length < 65536) {\n out.byte(0xda);\n out.u16(bytes.length);\n } else throw new Error('String too long');\n out.raw(bytes);\n}\n\nfunction packBin(value: Uint8Array, out: ByteWriter): void {\n if (value.byteLength < 256) {\n out.byte(0xc4);\n out.byte(value.byteLength);\n } else if (value.byteLength < 65536) {\n out.byte(0xc5);\n out.u16(value.byteLength);\n } else throw new Error('Binary too long');\n out.raw(value);\n}\n\nfunction packNumber(value: number, out: ByteWriter): void {\n if (!Number.isInteger(value)) {\n out.byte(0xcb);\n out.f64(value);\n return;\n }\n if (value >= 0 && value <= 0x7f) {\n out.byte(value);\n return;\n }\n if (value < 0 && value >= -32) {\n out.byte(value & 0xff);\n return;\n }\n if (value >= 0 && value <= 0xff) {\n out.byte(0xcc);\n out.byte(value);\n return;\n }\n if (value >= 0 && value <= 0xffff) {\n out.byte(0xcd);\n out.u16(value);\n return;\n }\n if (value >= 0 && value <= 0xffffffff) {\n out.byte(0xce);\n out.u32(value);\n return;\n }\n if (value < 0 && value >= -128) {\n out.byte(0xd0);\n out.byte(value & 0xff);\n return;\n }\n throw new Error('Integer out of range');\n}\n\nfunction packArray(values: PackValue[], out: ByteWriter): void {\n if (values.length < 16) out.byte(0x90 | values.length);\n else if (values.length < 65536) {\n out.byte(0xdc);\n out.u16(values.length);\n } else throw new Error('Array too long');\n for (const value of values) packInto(value, out);\n}\n\nfunction packMap(value: { [key: string]: PackValue }, out: ByteWriter): void {\n const entries = Object.entries(value);\n if (entries.length < 16) out.byte(0x80 | entries.length);\n else if (entries.length < 65536) {\n out.byte(0xde);\n out.u16(entries.length);\n } else throw new Error('Map too long');\n for (const [key, item] of entries) {\n packString(key, out);\n packInto(item, out);\n }\n}\n\nfunction packInto(value: PackValue, out: ByteWriter): void {\n if (value === null) {\n out.byte(0xc0);\n return;\n }\n if (typeof value === 'boolean') {\n out.byte(value ? 0xc3 : 0xc2);\n return;\n }\n if (typeof value === 'number') {\n packNumber(value, out);\n return;\n }\n if (typeof value === 'string') {\n packString(value, out);\n return;\n }\n if (value instanceof Uint8Array) {\n packBin(value, out);\n return;\n }\n if (Array.isArray(value)) {\n packArray(value, out);\n return;\n }\n packMap(value, out);\n}\n\nfunction unpackString(reader: ByteReader, length: number): string {\n return new TextDecoder().decode(reader.bytes(length));\n}\n\nfunction unpackArray(reader: ByteReader, length: number): PackValue[] {\n const values: PackValue[] = [];\n for (let index = 0; index < length; index += 1) values.push(unpackOne(reader));\n return values;\n}\n\nfunction unpackMap(reader: ByteReader, length: number): { [key: string]: PackValue } {\n const value: { [key: string]: PackValue } = {};\n for (let index = 0; index < length; index += 1) {\n const key = unpackOne(reader);\n if (typeof key !== 'string') throw new Error('Fish frame key was not a string');\n value[key] = unpackOne(reader);\n }\n return value;\n}\n\nfunction unpackSized(reader: ByteReader, marker: number): PackValue | undefined {\n if (marker === 0xc0) return null;\n if (marker === 0xc2) return false;\n if (marker === 0xc3) return true;\n if (marker === 0xc4) return reader.bytes(reader.byte());\n if (marker === 0xc5) return reader.bytes(reader.u16());\n if (marker === 0xc6) return reader.bytes(reader.u32());\n if (marker === 0xca) return reader.f32();\n if (marker === 0xcb) return reader.f64();\n if (marker === 0xcc) return reader.byte();\n if (marker === 0xcd) return reader.u16();\n if (marker === 0xce) return reader.u32();\n if (marker === 0xcf) return reader.u64();\n if (marker === 0xd0) return (reader.byte() << 24) >> 24;\n if (marker === 0xd1) return reader.i16();\n if (marker === 0xd2) return reader.i32();\n if (marker === 0xd3) return reader.i64();\n return undefined;\n}\n\nfunction unpackOne(reader: ByteReader): PackValue {\n const marker = reader.byte();\n if (marker <= 0x7f) return marker;\n if (marker >= 0xe0) return marker - 0x100;\n const sized = unpackSized(reader, marker);\n if (sized !== undefined) return sized;\n if ((marker & 0xe0) === 0xa0) return unpackString(reader, marker & 0x1f);\n if (marker === 0xd9) return unpackString(reader, reader.byte());\n if (marker === 0xda) return unpackString(reader, reader.u16());\n if (marker === 0xdb) return unpackString(reader, reader.u32());\n if ((marker & 0xf0) === 0x90) return unpackArray(reader, marker & 0x0f);\n if (marker === 0xdc) return unpackArray(reader, reader.u16());\n if (marker === 0xdd) return unpackArray(reader, reader.u32());\n if ((marker & 0xf0) === 0x80) return unpackMap(reader, marker & 0x0f);\n if (marker === 0xde) return unpackMap(reader, reader.u16());\n if (marker === 0xdf) return unpackMap(reader, reader.u32());\n throw new Error('Unsupported Fish frame');\n}\n\nexport function pack(value: PackValue): Uint8Array {\n const out = new ByteWriter();\n packInto(value, out);\n return out.toArray();\n}\n\nexport function unpack(data: Uint8Array): PackValue {\n const reader = new ByteReader(data);\n const value = unpackOne(reader);\n if (!reader.done()) throw new Error('Fish frame had trailing bytes');\n return value;\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: FishFile, 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=\"audio\"; 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 fishHeaders(\n model: { id: string; provider: string; headers?: Record<string, string> },\n options: ProviderRequestOptions | undefined\n): Record<string, string> {\n const headers = requestHeaders(model, options);\n headers.model = model.id;\n return headers;\n}\n\nexport function socketUrl(baseUrl: string, path: string): URL {\n const url = new URL(baseUrl);\n url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';\n url.pathname = `${url.pathname.replace(/\\/$/, '')}/${path}`;\n url.search = '';\n return url;\n}\n\nexport function 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 messageBytes(data: unknown): Promise<Uint8Array> {\n if (typeof data === 'string') return new TextEncoder().encode(data);\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (data instanceof Uint8Array) return data;\n if (typeof Blob !== 'undefined' && data instanceof Blob) {\n return new Uint8Array(await data.arrayBuffer());\n }\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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerFish(): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerFish } from './catalog.ts';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { SpeechModel } from '../models.ts';
|
|
3
|
+
import { SpeechEventStream, type SpeechInput } from '../speech.ts';
|
|
4
|
+
export declare function speakFish(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): SpeechEventStream;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { TranscriptionModel } from '../models.ts';
|
|
3
|
+
import type { AudioInput, Transcript } from '../transcription.ts';
|
|
4
|
+
export declare function transcribeFish(model: TranscriptionModel<string>, input: AudioInput, options?: ProviderRequestOptions): Promise<Transcript>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { AudioFormat } from '../audio.ts';
|
|
3
|
+
export type PackValue = null | boolean | number | string | Uint8Array | PackValue[] | {
|
|
4
|
+
[key: string]: PackValue;
|
|
5
|
+
};
|
|
6
|
+
export interface FishFile {
|
|
7
|
+
data: Uint8Array;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
filename: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function pack(value: PackValue): Uint8Array;
|
|
12
|
+
export declare function unpack(data: Uint8Array): PackValue;
|
|
13
|
+
export declare function mediaType(mimeType: string): string;
|
|
14
|
+
export declare function audioFilename(mimeType: string): string;
|
|
15
|
+
export declare function encodeForm(file: FishFile, fields: Record<string, string>): ArrayBuffer;
|
|
16
|
+
export declare function formType(): string;
|
|
17
|
+
export declare function fishHeaders(model: {
|
|
18
|
+
id: string;
|
|
19
|
+
provider: string;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
}, options: ProviderRequestOptions | undefined): Record<string, string>;
|
|
22
|
+
export declare function socketUrl(baseUrl: string, path: string): URL;
|
|
23
|
+
export declare function speechFormatName(format: AudioFormat): string;
|
|
24
|
+
export declare function connectSocket(url: string, headers: Record<string, string>): WebSocket;
|
|
25
|
+
export declare function awaitOpen(socket: WebSocket, label: string, fail: (message: string) => void): Promise<void>;
|
|
26
|
+
export declare function messageBytes(data: unknown): Promise<Uint8Array>;
|
|
27
|
+
export declare function sameFormat(left: AudioFormat, right: AudioFormat): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -54,6 +54,10 @@
|
|
|
54
54
|
"types": "./dist/types/mistral/index.d.ts",
|
|
55
55
|
"default": "./dist/mistral/index.js"
|
|
56
56
|
},
|
|
57
|
+
"./fish": {
|
|
58
|
+
"types": "./dist/types/fish/index.d.ts",
|
|
59
|
+
"default": "./dist/fish/index.js"
|
|
60
|
+
},
|
|
57
61
|
"./durable": {
|
|
58
62
|
"types": "./dist/types/durable/index.d.ts",
|
|
59
63
|
"default": "./dist/durable/index.js"
|