@ai-ecoverse/pi-audio 1.18.1 → 1.20.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 +4 -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/google/catalog.js +61 -0
- package/dist/google/catalog.js.map +7 -0
- package/dist/google/index.js +5 -0
- package/dist/google/index.js.map +7 -0
- package/dist/google/speak.js +160 -0
- package/dist/google/speak.js.map +7 -0
- package/dist/google/wire.js +43 -0
- package/dist/google/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/dist/types/google/catalog.d.ts +1 -0
- package/dist/types/google/index.d.ts +1 -0
- package/dist/types/google/speak.d.ts +4 -0
- package/dist/types/google/wire.d.ts +8 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -23,6 +23,10 @@ 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/google` registers Gemini speech adapters when `registerGoogle()` is called. `gemini-3.8-flash-tts` and `gemini-3.8-flash-lite-tts` post JSON to `models/{id}:generateContent` with `responseModalities: ["AUDIO"]` and a prebuilt `voiceName`. Unary audio is WAV at 24 kHz unless the format asks for `AUDIO_L16`, `AUDIO_MULAW`, or `AUDIO_ALAW`. `textStreaming` is false. Output audio is priced from 25 tokens per second: $0.0135 per minute for Flash and $0.009 per minute for Flash-Lite through December 31, 2026. Text input is $0.50 per million tokens and is not on `audioCost`. Auth is `x-goog-api-key`.
|
|
27
|
+
|
|
28
|
+
`@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`).
|
|
29
|
+
|
|
26
30
|
`@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
31
|
|
|
28
32
|
`@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
|
+
}
|