@ai-ecoverse/pi-audio 1.20.1 → 1.22.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.
Files changed (39) hide show
  1. package/README.md +5 -1
  2. package/dist/cloudflare/catalog.js +165 -0
  3. package/dist/cloudflare/catalog.js.map +7 -0
  4. package/dist/cloudflare/index.js +5 -0
  5. package/dist/cloudflare/index.js.map +7 -0
  6. package/dist/cloudflare/listen.js +133 -0
  7. package/dist/cloudflare/listen.js.map +7 -0
  8. package/dist/cloudflare/speak.js +224 -0
  9. package/dist/cloudflare/speak.js.map +7 -0
  10. package/dist/cloudflare/transcribe.js +155 -0
  11. package/dist/cloudflare/transcribe.js.map +7 -0
  12. package/dist/cloudflare/wire.js +166 -0
  13. package/dist/cloudflare/wire.js.map +7 -0
  14. package/dist/fish-agents/events.js +94 -0
  15. package/dist/fish-agents/events.js.map +7 -0
  16. package/dist/fish-agents/index.js +57 -0
  17. package/dist/fish-agents/index.js.map +7 -0
  18. package/dist/fish-agents/provision.js +178 -0
  19. package/dist/fish-agents/provision.js.map +7 -0
  20. package/dist/fish-agents/room.js +80 -0
  21. package/dist/fish-agents/room.js.map +7 -0
  22. package/dist/fish-agents/session.js +304 -0
  23. package/dist/fish-agents/session.js.map +7 -0
  24. package/dist/realtime.js.map +2 -2
  25. package/dist/tokens.js +11 -1
  26. package/dist/tokens.js.map +2 -2
  27. package/dist/types/cloudflare/catalog.d.ts +1 -0
  28. package/dist/types/cloudflare/index.d.ts +1 -0
  29. package/dist/types/cloudflare/listen.d.ts +4 -0
  30. package/dist/types/cloudflare/speak.d.ts +4 -0
  31. package/dist/types/cloudflare/transcribe.d.ts +4 -0
  32. package/dist/types/cloudflare/wire.d.ts +27 -0
  33. package/dist/types/fish-agents/events.d.ts +21 -0
  34. package/dist/types/fish-agents/index.d.ts +11 -0
  35. package/dist/types/fish-agents/provision.d.ts +11 -0
  36. package/dist/types/fish-agents/room.d.ts +46 -0
  37. package/dist/types/fish-agents/session.d.ts +19 -0
  38. package/dist/types/realtime.d.ts +1 -0
  39. package/package.json +9 -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/cloudflare` registers Workers AI speech adapters when `registerCloudflare()` is called. One Cloudflare token calls `POST /client/v4/accounts/{account_id}/ai/run/{model}`. Put the account in `baseUrl` or the `cf-account-id` header. Streaming models open `wss://gateway.ai.cloudflare.com/v1/{account}/{gateway}/workers-ai?model=…`. The gateway id is the `cf-aig-gateway-id` header, or `default`. `@cf/deepgram/nova-3` streams (`turnDetection: vad`, catalog price $0.0092 per minute). `@cf/deepgram/flux` streams with semantic end of turn ($0.0077 per minute) and has no batch transcribe. `@cf/openai/whisper-large-v3-turbo` is batch only ($0.000513 per minute). `@cf/deepgram/aura-1` ($0.015 per thousand characters), `aura-2-en`, and `aura-2-es` ($0.03) speak MP3 over HTTP and linear16 over the socket (`textStreaming: true`). `@cf/myshell-ai/melotts` is batch MP3 priced at $0.000205 per audio minute. `@cf/pipecat-ai/smart-turn-v2` is not a transcript model, so it is not registered.
27
+
26
28
  `@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
29
 
28
30
  `@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`).
@@ -52,10 +54,12 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
52
54
 
53
55
  `@ai-ecoverse/pi-audio/gemini-live` registers `gemini-3.8-live`, `gemini-live-2.5-flash-native-audio`, and `gemini-3.1-flash-live-preview`. Setup enables sliding-window compression and session resumption. `gemini-3.8-live` leaves function behavior at the model default and accepts `vocabulary` as `customVocabulary`. The 2.5 model declares `NON_BLOCKING`. The 3.1 preview declares `BLOCKING`. `respond` maps `Delivery` onto `scheduling` (`INTERRUPT`, `WHEN_IDLE`, `SILENT`) and returns `status`, `retryable`, and `message`. A `goAway` reconnects with the latest resumable handle. There is no truncate call. The adapter takes a recorded `wire` and never calls Google.
54
56
 
55
- `mintRealtimeToken(model, constraints, options)` returns `{ value, expiresAt, model }` and calls `options.fetch` (or `globalThis.fetch`). OpenAI `POST /v1/realtime/client_secrets` sends a realtime session with instructions, voice, and tools. xAI `POST /v1/realtime/client_secrets` sends only `expires_after.seconds` of 300. Gemini `POST /v1beta/auth_tokens` locks `models/<id>`, instructions, voice, and tools in `liveConnectConstraints` and returns `name`, which the live adapter sends as `access_token`. ElevenLabs `GET /v1/convai/conversation/get-signed-url` takes `agent_id` (or `GET /conversation/token` when the only transport is WebRTC) and expires in 15 minutes. Fish `POST /v1/agent/sessions` sends `agent_id` plus `overrides.system_prompt` and `overrides.voice_id`. `value` is the LiveKit participant token. `expiresAt` is milliseconds. A numeric `expires_at` below `1e12` is unix seconds.
57
+ `mintRealtimeToken(model, constraints, options)` returns `{ value, expiresAt, model, url? }` and calls `options.fetch` (or `globalThis.fetch`). OpenAI `POST /v1/realtime/client_secrets` sends a realtime session with instructions, voice, and tools. xAI `POST /v1/realtime/client_secrets` sends only `expires_after.seconds` of 300. Gemini `POST /v1beta/auth_tokens` locks `models/<id>`, instructions, voice, and tools in `liveConnectConstraints` and returns `name`, which the live adapter sends as `access_token`. ElevenLabs `GET /v1/convai/conversation/get-signed-url` takes `agent_id` (or `GET /conversation/token` when the only transport is WebRTC) and expires in 15 minutes. Fish `POST /v1/agent/sessions` sends `agent_id` plus `overrides.system_prompt` and `overrides.voice_id`. `value` is the LiveKit participant token and `url` is `livekit_url`. `expiresAt` is milliseconds. A numeric `expires_at` below `1e12` is unix seconds.
56
58
 
57
59
  `@ai-ecoverse/pi-audio/elevenlabs-agents` registers `elevenlabs-agent`, a hosted cascade (`provisioning: "agent"`, `truncation: "server"`, `liveToolUpdate: false`). `provisionAgent` creates or updates the agent and reuses it when the spec hash matches. pi tools become client tools. A string `llm` is a vendor model name. A `CustomLlm` sets `llm` to `custom-llm` and sends `custom_llm.url`, `model_id`, `api_type`, and `request_headers` (ElevenLabs `api_key` is a workspace secret reference, not a raw token). `connectRealtime` uses a `RealtimeToken` from `mintRealtimeToken` (`get-signed-url`) when one is passed. Otherwise an API key provisions the agent and mints the signed URL. Silent `inject` sends `contextual_update` with `context_id` `pi-audio`. `close()` prices `audio.inputSeconds` from the wall-clock session at $0.08 per minute.
58
60
 
61
+ `@ai-ecoverse/pi-audio/fish-agents` registers `fish-agent` (`transports: ["webrtc"]`, `delivery.silent: "approximate"`, `delivery.interrupt: "native"`). `provisionAgent` creates client tools, writes the draft, and publishes it. A `RealtimeToken` with `url` is the LiveKit room from `mintRealtimeToken`; connect does not open a second session. Without a token, connect creates the session and fails on an unknown `transport`. Silent `inject` sends `user.message` with `audio: false` and drops that reply's transcript. The microphone is published after `openEchoMic`. The catalog `audioCost` is zero because Fish has not published a per-minute session rate. The example voice id is the one in Fish's quickstart.
62
+
59
63
  `talkerTools` is `ask_agent` and `read`. pi-audio defines `askAgent` (a pi `Tool` whose parameters are `request` and optional `context`) and the `Thinker` interface. The host implements `Thinker` and supplies `read`. `bindThinker` runs direct tools with `directTimeoutMs` and truncates their text to `directMaxChars`. On a blocking `ask_agent` it answers with `ack` immediately and injects the result later. On an async call it `respond`s with the result and `options.result`. Progress is injected with `options.progress`. `tool_call_cancelled` aborts the thinker and any direct tool. A blocking failure is injected after the acknowledgement, with a `thinker_result` caption. `Thinker.ask` receives the optional `context` string. Closing the session aborts work that is still running. The binding also yields `thinker_progress` / `thinker_result` captions and `thinking` / `tool` / `idle` phases.
60
64
 
61
65
  ## Development
@@ -0,0 +1,165 @@
1
+ import {
2
+ registerVoiceModel
3
+ } from "../models.js";
4
+ import { registerSpeechApiProvider, registerTranscriptionApiProvider } from "../registry.js";
5
+ import { listenCloudflare } from "./listen.js";
6
+ import { speakCloudflare } from "./speak.js";
7
+ import { transcribeCloudflare } from "./transcribe.js";
8
+ const tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
9
+ const cloudflare = "https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run";
10
+ const format = (encoding, sampleRate) => ({
11
+ encoding,
12
+ sampleRate,
13
+ channels: 1
14
+ });
15
+ const pcm = [format("pcm16", 16e3), format("mulaw", 8e3)];
16
+ const auraOut = [
17
+ format("mp3", 24e3),
18
+ format("pcm16", 24e3),
19
+ format("mulaw", 8e3),
20
+ format("alaw", 8e3)
21
+ ];
22
+ function named(ids) {
23
+ return ids.map((id) => ({ id, name: id }));
24
+ }
25
+ const aura1Voices = named([
26
+ "angus",
27
+ "asteria",
28
+ "arcas",
29
+ "orion",
30
+ "orpheus",
31
+ "athena",
32
+ "luna",
33
+ "zeus",
34
+ "perseus",
35
+ "helios",
36
+ "hera",
37
+ "stella"
38
+ ]);
39
+ const aura2EnVoices = named([
40
+ "amalthea",
41
+ "andromeda",
42
+ "apollo",
43
+ "arcas",
44
+ "aries",
45
+ "asteria",
46
+ "athena",
47
+ "atlas",
48
+ "aurora",
49
+ "callista",
50
+ "cora",
51
+ "cordelia",
52
+ "delia",
53
+ "draco",
54
+ "electra",
55
+ "harmonia",
56
+ "helena",
57
+ "hera",
58
+ "hermes",
59
+ "hyperion",
60
+ "iris",
61
+ "janus",
62
+ "juno",
63
+ "jupiter",
64
+ "luna",
65
+ "mars",
66
+ "minerva",
67
+ "neptune",
68
+ "odysseus",
69
+ "ophelia",
70
+ "orion",
71
+ "orpheus",
72
+ "pandora",
73
+ "phoebe",
74
+ "pluto",
75
+ "saturn",
76
+ "thalia",
77
+ "theia",
78
+ "vesta",
79
+ "zeus"
80
+ ]);
81
+ const aura2EsVoices = named([
82
+ "sirio",
83
+ "nestor",
84
+ "carina",
85
+ "celeste",
86
+ "alvaro",
87
+ "diana",
88
+ "aquila",
89
+ "selena",
90
+ "estrella",
91
+ "javier"
92
+ ]);
93
+ function transcription(id, streaming, turnDetection, inputFormats, perInputMinute, diarization) {
94
+ return {
95
+ id,
96
+ name: id,
97
+ api: "cloudflare-ai-run",
98
+ provider: "cloudflare",
99
+ baseUrl: cloudflare,
100
+ type: "transcription",
101
+ streaming,
102
+ turnDetection,
103
+ inputFormats,
104
+ diarization,
105
+ cost: tokenCost,
106
+ audioCost: { perInputMinute }
107
+ };
108
+ }
109
+ function speech(id, textStreaming, outputFormats, voices, audioCost) {
110
+ return {
111
+ id,
112
+ name: id,
113
+ api: "cloudflare-ai-run",
114
+ provider: "cloudflare",
115
+ baseUrl: cloudflare,
116
+ type: "speech",
117
+ textStreaming,
118
+ outputFormats,
119
+ voices,
120
+ cost: tokenCost,
121
+ audioCost
122
+ };
123
+ }
124
+ function registerCloudflare() {
125
+ const models = [
126
+ transcription("@cf/deepgram/nova-3", true, "vad", pcm, 92e-4, true),
127
+ transcription("@cf/deepgram/flux", true, "semantic", [format("pcm16", 16e3)], 77e-4, false),
128
+ transcription(
129
+ "@cf/openai/whisper-large-v3-turbo",
130
+ false,
131
+ "none",
132
+ [format("pcm16", 16e3), format("wav", 16e3), format("mp3", 16e3)],
133
+ 513e-6,
134
+ false
135
+ ),
136
+ speech("@cf/deepgram/aura-1", true, auraOut, aura1Voices, { perThousandChars: 0.015 }),
137
+ speech("@cf/deepgram/aura-2-en", true, auraOut, aura2EnVoices, { perThousandChars: 0.03 }),
138
+ speech("@cf/deepgram/aura-2-es", true, auraOut, aura2EsVoices, { perThousandChars: 0.03 }),
139
+ speech("@cf/myshell-ai/melotts", false, [format("mp3", 24e3)], void 0, {
140
+ perOutputMinute: 205e-6
141
+ })
142
+ ];
143
+ const unregisterModels = models.map((model) => registerVoiceModel(model));
144
+ const unregisterTranscription = registerTranscriptionApiProvider({
145
+ api: "cloudflare-ai-run",
146
+ transcribe: transcribeCloudflare,
147
+ listen: listenCloudflare
148
+ });
149
+ const unregisterSpeech = registerSpeechApiProvider({
150
+ api: "cloudflare-ai-run",
151
+ speak: speakCloudflare
152
+ });
153
+ let released = false;
154
+ return () => {
155
+ if (released) return;
156
+ released = true;
157
+ for (const unregister of unregisterModels) unregister();
158
+ unregisterTranscription();
159
+ unregisterSpeech();
160
+ };
161
+ }
162
+ export {
163
+ registerCloudflare
164
+ };
165
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cloudflare/catalog.ts"],
4
+ "sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport {\n registerVoiceModel,\n type SpeechModel,\n type TranscriptionModel,\n type VoiceInfo,\n} from '../models.ts';\nimport { registerSpeechApiProvider, registerTranscriptionApiProvider } from '../registry.ts';\nimport { listenCloudflare } from './listen.ts';\nimport { speakCloudflare } from './speak.ts';\nimport { transcribeCloudflare } from './transcribe.ts';\n\nconst tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\nconst cloudflare = 'https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run';\n\nconst format = (encoding: AudioFormat['encoding'], sampleRate: number): AudioFormat => ({\n encoding,\n sampleRate,\n channels: 1,\n});\n\nconst pcm = [format('pcm16', 16000), format('mulaw', 8000)];\nconst auraOut = [\n format('mp3', 24000),\n format('pcm16', 24000),\n format('mulaw', 8000),\n format('alaw', 8000),\n];\n\nfunction named(ids: string[]): VoiceInfo[] {\n return ids.map((id) => ({ id, name: id }));\n}\n\nconst aura1Voices = named([\n 'angus',\n 'asteria',\n 'arcas',\n 'orion',\n 'orpheus',\n 'athena',\n 'luna',\n 'zeus',\n 'perseus',\n 'helios',\n 'hera',\n 'stella',\n]);\n\nconst aura2EnVoices = named([\n 'amalthea',\n 'andromeda',\n 'apollo',\n 'arcas',\n 'aries',\n 'asteria',\n 'athena',\n 'atlas',\n 'aurora',\n 'callista',\n 'cora',\n 'cordelia',\n 'delia',\n 'draco',\n 'electra',\n 'harmonia',\n 'helena',\n 'hera',\n 'hermes',\n 'hyperion',\n 'iris',\n 'janus',\n 'juno',\n 'jupiter',\n 'luna',\n 'mars',\n 'minerva',\n 'neptune',\n 'odysseus',\n 'ophelia',\n 'orion',\n 'orpheus',\n 'pandora',\n 'phoebe',\n 'pluto',\n 'saturn',\n 'thalia',\n 'theia',\n 'vesta',\n 'zeus',\n]);\n\nconst aura2EsVoices = named([\n 'sirio',\n 'nestor',\n 'carina',\n 'celeste',\n 'alvaro',\n 'diana',\n 'aquila',\n 'selena',\n 'estrella',\n 'javier',\n]);\n\nfunction transcription(\n id: string,\n streaming: boolean,\n turnDetection: 'none' | 'vad' | 'semantic',\n inputFormats: AudioFormat[],\n perInputMinute: number,\n diarization: boolean\n): TranscriptionModel<string> {\n return {\n id,\n name: id,\n api: 'cloudflare-ai-run',\n provider: 'cloudflare',\n baseUrl: cloudflare,\n type: 'transcription',\n streaming,\n turnDetection,\n inputFormats,\n diarization,\n cost: tokenCost,\n audioCost: { perInputMinute },\n };\n}\n\nfunction speech(\n id: string,\n textStreaming: boolean,\n outputFormats: AudioFormat[],\n voices: VoiceInfo[] | undefined,\n audioCost: SpeechModel<string>['audioCost']\n): SpeechModel<string> {\n return {\n id,\n name: id,\n api: 'cloudflare-ai-run',\n provider: 'cloudflare',\n baseUrl: cloudflare,\n type: 'speech',\n textStreaming,\n outputFormats,\n voices,\n cost: tokenCost,\n audioCost,\n };\n}\n\nexport function registerCloudflare(): () => void {\n const models = [\n transcription('@cf/deepgram/nova-3', true, 'vad', pcm, 0.0092, true),\n transcription('@cf/deepgram/flux', true, 'semantic', [format('pcm16', 16000)], 0.0077, false),\n transcription(\n '@cf/openai/whisper-large-v3-turbo',\n false,\n 'none',\n [format('pcm16', 16000), format('wav', 16000), format('mp3', 16000)],\n 0.000513,\n false\n ),\n speech('@cf/deepgram/aura-1', true, auraOut, aura1Voices, { perThousandChars: 0.015 }),\n speech('@cf/deepgram/aura-2-en', true, auraOut, aura2EnVoices, { perThousandChars: 0.03 }),\n speech('@cf/deepgram/aura-2-es', true, auraOut, aura2EsVoices, { perThousandChars: 0.03 }),\n speech('@cf/myshell-ai/melotts', false, [format('mp3', 24000)], undefined, {\n perOutputMinute: 0.000205,\n }),\n ];\n const unregisterModels = models.map((model) => registerVoiceModel(model));\n const unregisterTranscription = registerTranscriptionApiProvider({\n api: 'cloudflare-ai-run',\n transcribe: transcribeCloudflare,\n listen: listenCloudflare,\n });\n const unregisterSpeech = registerSpeechApiProvider({\n api: 'cloudflare-ai-run',\n speak: speakCloudflare,\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": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ import { registerCloudflare } from "./catalog.js";
2
+ export {
3
+ registerCloudflare
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cloudflare/index.ts"],
4
+ "sourcesContent": ["export { registerCloudflare } from './catalog.ts';\n"],
5
+ "mappings": "AAAA,SAAS,0BAA0B;",
6
+ "names": []
7
+ }
@@ -0,0 +1,133 @@
1
+ import { chunkSeconds } from "../audio.js";
2
+ import { asVoiceError, isRecord, numberField, stringField } from "../openai/wire.js";
3
+ import {
4
+ TranscriptionEventStream
5
+ } from "../transcription.js";
6
+ import { priceAudio } from "../usage.js";
7
+ import {
8
+ audioFrame,
9
+ awaitOpen,
10
+ connectSocket,
11
+ listenQuery,
12
+ messageBytes,
13
+ session,
14
+ workersAiSocket
15
+ } from "./wire.js";
16
+ function novaEvents(record, state) {
17
+ if (stringField(record, "type") === "SpeechStarted") {
18
+ const at = numberField(record, "timestamp") ?? 0;
19
+ state.events.push({ type: "speech_start", at });
20
+ return;
21
+ }
22
+ const transcript = novaTranscript(record);
23
+ if (transcript !== void 0 && transcript !== "") {
24
+ const event = record.is_final === true ? { type: "final", segmentId: "0", text: transcript } : { type: "partial", segmentId: "0", text: transcript };
25
+ state.events.push(event);
26
+ state.text = transcript;
27
+ }
28
+ if (record.speech_final === true)
29
+ state.events.push({ type: "turn_end", text: state.text, at: 0 });
30
+ }
31
+ function novaTranscript(record) {
32
+ const channel = record.channel;
33
+ if (!isRecord(channel) || Array.isArray(channel)) return void 0;
34
+ const alternatives = channel.alternatives;
35
+ if (!Array.isArray(alternatives)) return void 0;
36
+ const first = alternatives[0];
37
+ if (!isRecord(first) || Array.isArray(first)) return void 0;
38
+ return stringField(first, "transcript");
39
+ }
40
+ function fluxEvents(record, state) {
41
+ const event = stringField(record, "event");
42
+ const at = numberField(record, "audio_window_start") ?? 0;
43
+ const transcript = stringField(record, "transcript") ?? "";
44
+ if (event === "StartOfTurn") {
45
+ state.events.push({ type: "speech_start", at });
46
+ return;
47
+ }
48
+ if (event === "Update" || event === "EagerEndOfTurn") {
49
+ if (transcript !== "") state.events.push({ type: "partial", segmentId: "0", text: transcript });
50
+ return;
51
+ }
52
+ if (event === "EndOfTurn") {
53
+ if (transcript !== "") {
54
+ state.events.push({ type: "final", segmentId: "0", text: transcript });
55
+ state.text = transcript;
56
+ }
57
+ state.events.push({ type: "turn_end", text: state.text, at });
58
+ }
59
+ }
60
+ async function onListenMessage(data, modelId, state) {
61
+ const bytes = await messageBytes(data);
62
+ if (bytes !== void 0) return;
63
+ if (typeof data !== "string") throw new Error("Unsupported socket message");
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(data);
67
+ } catch {
68
+ return;
69
+ }
70
+ if (!isRecord(parsed) || Array.isArray(parsed)) return;
71
+ if (modelId.includes("/flux")) fluxEvents(parsed, state);
72
+ else novaEvents(parsed, state);
73
+ }
74
+ function doneTranscript(model, text, seconds) {
75
+ const meters = seconds > 0 ? { inputSeconds: seconds } : {};
76
+ return { text, usage: priceAudio(model.audioCost, meters) };
77
+ }
78
+ function listenCloudflare(model, audio, options) {
79
+ const stream = new TranscriptionEventStream();
80
+ void pumpListen(model, audio, options, stream);
81
+ return stream;
82
+ }
83
+ async function pumpListen(model, audio, options, stream) {
84
+ let socket;
85
+ try {
86
+ if (!model.streaming) throw new Error("Model does not stream");
87
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
88
+ const format = model.inputFormats[0];
89
+ if (!format) throw new Error("No input format");
90
+ const route = session(model, options);
91
+ const live = connectSocket(
92
+ workersAiSocket(route.account, route.gateway, model.id, listenQuery(model.id, format)),
93
+ route.headers
94
+ );
95
+ socket = live;
96
+ const state = { events: [], text: "" };
97
+ let chain = Promise.resolve();
98
+ let messageError;
99
+ const closed = new Promise((resolve) => {
100
+ live.addEventListener("close", () => resolve());
101
+ });
102
+ live.addEventListener("message", (event) => {
103
+ chain = chain.then(() => onListenMessage(event.data, model.id, state)).catch((error) => {
104
+ messageError = asVoiceError(error).error;
105
+ });
106
+ });
107
+ let failed;
108
+ await awaitOpen(socket, "Cloudflare", (message) => {
109
+ failed = message;
110
+ });
111
+ if (failed !== void 0) throw new Error(failed);
112
+ let seconds = 0;
113
+ for await (const chunk of audio) {
114
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
115
+ seconds += chunkSeconds(chunk);
116
+ live.send(audioFrame(chunk.data));
117
+ }
118
+ live.close();
119
+ await closed;
120
+ await chain;
121
+ if (messageError !== void 0) throw new Error(messageError);
122
+ for (const event of state.events) stream.push(event);
123
+ stream.push({ type: "done", transcript: doneTranscript(model, state.text, seconds) });
124
+ } catch (error) {
125
+ if (socket !== void 0) socket.close();
126
+ const voice = asVoiceError(error);
127
+ stream.push({ type: "error", reason: voice.reason, error: voice.error });
128
+ }
129
+ }
130
+ export {
131
+ listenCloudflare
132
+ };
133
+ //# sourceMappingURL=listen.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cloudflare/listen.ts"],
4
+ "sourcesContent": ["import type { AudioSource } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport { asVoiceError, isRecord, numberField, stringField } from '../openai/wire.ts';\nimport {\n type ListenOptions,\n type Transcript,\n type TranscriptionEvent,\n TranscriptionEventStream,\n} from '../transcription.ts';\nimport { priceAudio } from '../usage.ts';\nimport {\n audioFrame,\n awaitOpen,\n connectSocket,\n listenQuery,\n messageBytes,\n session,\n workersAiSocket,\n} from './wire.ts';\n\ninterface ListenState {\n events: TranscriptionEvent[];\n text: string;\n}\n\nfunction novaEvents(record: Record<string, unknown>, state: ListenState): void {\n if (stringField(record, 'type') === 'SpeechStarted') {\n const at = numberField(record, 'timestamp') ?? 0;\n state.events.push({ type: 'speech_start', at });\n return;\n }\n const transcript = novaTranscript(record);\n if (transcript !== undefined && transcript !== '') {\n const event: TranscriptionEvent =\n record.is_final === true\n ? { type: 'final', segmentId: '0', text: transcript }\n : { type: 'partial', segmentId: '0', text: transcript };\n state.events.push(event);\n state.text = transcript;\n }\n if (record.speech_final === true)\n state.events.push({ type: 'turn_end', text: state.text, at: 0 });\n}\n\nfunction novaTranscript(record: Record<string, unknown>): string | undefined {\n const channel = record.channel;\n if (!isRecord(channel) || Array.isArray(channel)) return undefined;\n const alternatives = channel.alternatives;\n if (!Array.isArray(alternatives)) return undefined;\n const first = alternatives[0];\n if (!isRecord(first) || Array.isArray(first)) return undefined;\n return stringField(first, 'transcript');\n}\n\nfunction fluxEvents(record: Record<string, unknown>, state: ListenState): void {\n const event = stringField(record, 'event');\n const at = numberField(record, 'audio_window_start') ?? 0;\n const transcript = stringField(record, 'transcript') ?? '';\n if (event === 'StartOfTurn') {\n state.events.push({ type: 'speech_start', at });\n return;\n }\n if (event === 'Update' || event === 'EagerEndOfTurn') {\n if (transcript !== '') state.events.push({ type: 'partial', segmentId: '0', text: transcript });\n return;\n }\n if (event === 'EndOfTurn') {\n if (transcript !== '') {\n state.events.push({ type: 'final', segmentId: '0', text: transcript });\n state.text = transcript;\n }\n state.events.push({ type: 'turn_end', text: state.text, at });\n }\n}\n\nasync function onListenMessage(data: unknown, modelId: string, state: ListenState): Promise<void> {\n const bytes = await messageBytes(data);\n if (bytes !== undefined) return;\n if (typeof data !== 'string') throw new Error('Unsupported socket message');\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n } catch {\n return;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) return;\n if (modelId.includes('/flux')) fluxEvents(parsed, state);\n else novaEvents(parsed, state);\n}\n\nfunction doneTranscript(\n model: TranscriptionModel<string>,\n text: string,\n seconds: number\n): Transcript {\n const meters = seconds > 0 ? { inputSeconds: seconds } : {};\n return { text, usage: priceAudio(model.audioCost, meters) };\n}\n\nexport function listenCloudflare(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options?: ListenOptions\n): TranscriptionEventStream {\n const stream = new TranscriptionEventStream();\n void pumpListen(model, audio, options, stream);\n return stream;\n}\n\nasync function pumpListen(\n model: TranscriptionModel<string>,\n audio: AudioSource,\n options: ListenOptions | undefined,\n stream: TranscriptionEventStream\n) {\n let socket: WebSocket | undefined;\n try {\n if (!model.streaming) throw new Error('Model does not stream');\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = model.inputFormats[0];\n if (!format) throw new Error('No input format');\n const route = session(model, options);\n const live = connectSocket(\n workersAiSocket(route.account, route.gateway, model.id, listenQuery(model.id, format)),\n route.headers\n );\n socket = live;\n const state: ListenState = { events: [], text: '' };\n let chain = Promise.resolve();\n let messageError: string | undefined;\n const closed = new Promise<void>((resolve) => {\n live.addEventListener('close', () => resolve());\n });\n live.addEventListener('message', (event) => {\n chain = chain\n .then(() => onListenMessage(event.data, model.id, state))\n .catch((error: unknown) => {\n messageError = asVoiceError(error).error;\n });\n });\n let failed: string | undefined;\n await awaitOpen(socket, 'Cloudflare', (message) => {\n failed = message;\n });\n if (failed !== undefined) throw new Error(failed);\n let seconds = 0;\n for await (const chunk of audio) {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n seconds += chunkSeconds(chunk);\n live.send(audioFrame(chunk.data));\n }\n live.close();\n await closed;\n await chain;\n if (messageError !== undefined) throw new Error(messageError);\n for (const event of state.events) stream.push(event);\n stream.push({ type: 'done', transcript: doneTranscript(model, state.text, seconds) });\n } catch (error) {\n if (socket !== undefined) socket.close();\n const voice = asVoiceError(error);\n stream.push({ type: 'error', reason: voice.reason, error: voice.error });\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,224 @@
1
+ import { chunkSeconds } from "../audio.js";
2
+ import { decodeAudioBase64 } from "../convert.js";
3
+ import {
4
+ applyPayload,
5
+ asVoiceError,
6
+ isRecord,
7
+ requestSignal,
8
+ stringField
9
+ } from "../openai/wire.js";
10
+ import { SpeechEventStream } from "../speech.js";
11
+ import { priceAudio } from "../usage.js";
12
+ import {
13
+ auraEncoding,
14
+ awaitOpen,
15
+ connectSocket,
16
+ errorText,
17
+ messageBytes,
18
+ resultOf,
19
+ runUrl,
20
+ sameFormat,
21
+ session,
22
+ workersAiSocket
23
+ } from "./wire.js";
24
+ function resolveFormat(model, requested) {
25
+ const fallback = model.outputFormats[0];
26
+ if (!fallback) throw new Error("No output format");
27
+ if (!requested) return fallback;
28
+ const match = model.outputFormats.find((format) => sameFormat(format, requested));
29
+ if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
30
+ return match;
31
+ }
32
+ async function readText(text, signal) {
33
+ if (typeof text === "string") return text;
34
+ let combined = "";
35
+ for await (const part of text) {
36
+ if (signal?.aborted) throw new DOMException("aborted", "AbortError");
37
+ combined += part;
38
+ }
39
+ return combined;
40
+ }
41
+ function liveSpeech(modelId, text, format) {
42
+ if (modelId.includes("melotts")) return false;
43
+ if (typeof text !== "string") return true;
44
+ return format.encoding !== "mp3";
45
+ }
46
+ function speechBody(modelId, text, voice, format, language) {
47
+ if (modelId.includes("melotts")) {
48
+ const body = { prompt: text, lang: "en" };
49
+ if (language !== void 0) body.lang = language;
50
+ return body;
51
+ }
52
+ return {
53
+ text,
54
+ speaker: voice,
55
+ encoding: auraEncoding(format),
56
+ container: "none",
57
+ sample_rate: format.sampleRate
58
+ };
59
+ }
60
+ function concat(chunks) {
61
+ const length = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
62
+ const out = new Uint8Array(length);
63
+ let offset = 0;
64
+ for (const chunk of chunks) {
65
+ out.set(chunk, offset);
66
+ offset += chunk.byteLength;
67
+ }
68
+ return out;
69
+ }
70
+ async function readSpeechBytes(response) {
71
+ const type = response.headers.get("content-type") ?? "";
72
+ if (type.includes("audio") || type.includes("octet-stream")) {
73
+ const data = new Uint8Array(await response.arrayBuffer());
74
+ if (data.byteLength === 0) throw new Error("Speech response had no audio");
75
+ return data;
76
+ }
77
+ const text = await response.text();
78
+ let parsed;
79
+ try {
80
+ parsed = text === "" ? void 0 : JSON.parse(text);
81
+ } catch {
82
+ throw new Error("Speech response had no audio");
83
+ }
84
+ const result = resultOf(parsed);
85
+ if (typeof result === "string" && result !== "") return decodeAudioBase64(result);
86
+ if (isRecord(result) && !Array.isArray(result)) {
87
+ const audio = stringField(result, "audio") ?? stringField(result, "data");
88
+ if (audio !== void 0 && audio !== "") return decodeAudioBase64(audio);
89
+ }
90
+ throw new Error("Speech response had no audio");
91
+ }
92
+ async function postSpeech(model, body, options) {
93
+ const route = session(model, options);
94
+ const headers = route.headers;
95
+ headers["Content-Type"] = "application/json";
96
+ const fetchImpl = options?.fetch ?? globalThis.fetch;
97
+ const response = await fetchImpl(runUrl(model.baseUrl, model.id, route.account), {
98
+ method: "POST",
99
+ headers,
100
+ body: JSON.stringify(body),
101
+ signal: requestSignal(options)
102
+ });
103
+ if (options?.onResponse !== void 0) {
104
+ await options.onResponse({ status: response.status, headers: {} }, model);
105
+ }
106
+ if (!response.ok) {
107
+ const text = await response.text();
108
+ throw new Error(errorText(text, response.status));
109
+ }
110
+ return response;
111
+ }
112
+ function emitSpeech(stream, model, text, format, data) {
113
+ const seconds = chunkSeconds({ data, format, timestamp: 0 });
114
+ const meters = { characters: text.length };
115
+ if (seconds > 0) meters.outputSeconds = seconds;
116
+ stream.push({ type: "start", format });
117
+ stream.push({
118
+ type: "audio",
119
+ chunk: { data, format, timestamp: 0 },
120
+ span: { itemId: model.id, start: 0, end: text.length }
121
+ });
122
+ stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
123
+ }
124
+ async function onSpeechMessage(data, audio, fail) {
125
+ const bytes = await messageBytes(data);
126
+ if (bytes !== void 0) {
127
+ if (bytes.byteLength > 0) audio.push(bytes);
128
+ return;
129
+ }
130
+ if (typeof data !== "string") throw new Error("Unsupported socket message");
131
+ let parsed;
132
+ try {
133
+ parsed = JSON.parse(data);
134
+ } catch {
135
+ return;
136
+ }
137
+ if (!isRecord(parsed) || Array.isArray(parsed)) return;
138
+ const kind = stringField(parsed, "type");
139
+ if (kind === "Warning") {
140
+ const description = stringField(parsed, "description");
141
+ if (description !== void 0) fail(description);
142
+ }
143
+ }
144
+ async function sendLines(socket, text, signal) {
145
+ if (typeof text === "string") {
146
+ socket.send(JSON.stringify({ type: "Speak", text }));
147
+ socket.send(JSON.stringify({ type: "Flush" }));
148
+ return text;
149
+ }
150
+ let combined = "";
151
+ for await (const part of text) {
152
+ if (signal?.aborted) throw new DOMException("aborted", "AbortError");
153
+ combined += part;
154
+ socket.send(JSON.stringify({ type: "Speak", text: part }));
155
+ socket.send(JSON.stringify({ type: "Flush" }));
156
+ }
157
+ return combined;
158
+ }
159
+ async function speakLive(model, input, format, options, stream) {
160
+ const route = session(model, options);
161
+ const socket = connectSocket(
162
+ workersAiSocket(route.account, route.gateway, model.id, ""),
163
+ route.headers
164
+ );
165
+ const audio = [];
166
+ let failed;
167
+ let chain = Promise.resolve();
168
+ const closed = new Promise((resolve) => {
169
+ socket.addEventListener("close", () => resolve());
170
+ });
171
+ socket.addEventListener("message", (event) => {
172
+ chain = chain.then(
173
+ () => onSpeechMessage(event.data, audio, (message) => {
174
+ failed = message;
175
+ })
176
+ ).catch((error) => {
177
+ failed = asVoiceError(error).error;
178
+ });
179
+ });
180
+ await awaitOpen(socket, "Cloudflare", (message) => {
181
+ failed = message;
182
+ });
183
+ if (failed !== void 0) throw new Error(failed);
184
+ const text = await sendLines(socket, input.text, options?.signal);
185
+ socket.send(JSON.stringify({ type: "Close" }));
186
+ socket.close();
187
+ await closed;
188
+ await chain;
189
+ if (failed !== void 0) throw new Error(failed);
190
+ const data = concat(audio);
191
+ if (data.byteLength === 0) throw new Error("Speech response had no audio");
192
+ emitSpeech(stream, model, text, format, data);
193
+ }
194
+ async function speakRest(model, input, format, options, stream) {
195
+ const text = await readText(input.text, options?.signal);
196
+ const payload = speechBody(model.id, text, input.voice, format, input.language);
197
+ const body = await applyPayload(payload, model, options);
198
+ if (!isRecord(body) || Array.isArray(body)) throw new Error("Speech payload was not JSON");
199
+ const response = await postSpeech(model, body, options);
200
+ emitSpeech(stream, model, text, format, await readSpeechBytes(response));
201
+ }
202
+ function speakCloudflare(model, input, options) {
203
+ const stream = new SpeechEventStream();
204
+ void pumpSpeech(model, input, options, stream);
205
+ return stream;
206
+ }
207
+ async function pumpSpeech(model, input, options, stream) {
208
+ try {
209
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
210
+ const format = resolveFormat(model, input.format);
211
+ if (liveSpeech(model.id, input.text, format)) {
212
+ await speakLive(model, input, format, options, stream);
213
+ return;
214
+ }
215
+ await speakRest(model, input, format, options, stream);
216
+ } catch (error) {
217
+ const voice = asVoiceError(error);
218
+ stream.push({ type: "error", reason: voice.reason, error: voice.error });
219
+ }
220
+ }
221
+ export {
222
+ speakCloudflare
223
+ };
224
+ //# sourceMappingURL=speak.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cloudflare/speak.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport { decodeAudioBase64 } from '../convert.ts';\nimport type { SpeechModel } from '../models.ts';\nimport {\n applyPayload,\n asVoiceError,\n isRecord,\n requestSignal,\n stringField,\n} from '../openai/wire.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport {\n auraEncoding,\n awaitOpen,\n connectSocket,\n errorText,\n messageBytes,\n resultOf,\n runUrl,\n sameFormat,\n session,\n workersAiSocket,\n} from './wire.ts';\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\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 liveSpeech(modelId: string, text: SpeechInput['text'], format: AudioFormat): boolean {\n if (modelId.includes('melotts')) return false;\n if (typeof text !== 'string') return true;\n return format.encoding !== 'mp3';\n}\n\nfunction speechBody(\n modelId: string,\n text: string,\n voice: string,\n format: AudioFormat,\n language: string | undefined\n): Record<string, unknown> {\n if (modelId.includes('melotts')) {\n const body: Record<string, unknown> = { prompt: text, lang: 'en' };\n if (language !== undefined) body.lang = language;\n return body;\n }\n return {\n text,\n speaker: voice,\n encoding: auraEncoding(format),\n container: 'none',\n sample_rate: format.sampleRate,\n };\n}\n\nfunction concat(chunks: Uint8Array[]): Uint8Array {\n const length = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);\n const out = new Uint8Array(length);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.byteLength;\n }\n return out;\n}\n\nasync function readSpeechBytes(response: Response): Promise<Uint8Array> {\n const type = response.headers.get('content-type') ?? '';\n if (type.includes('audio') || type.includes('octet-stream')) {\n const data = new Uint8Array(await response.arrayBuffer());\n if (data.byteLength === 0) throw new Error('Speech response had no audio');\n return data;\n }\n const text = await response.text();\n let parsed: unknown;\n try {\n parsed = text === '' ? undefined : JSON.parse(text);\n } catch {\n throw new Error('Speech response had no audio');\n }\n const result = resultOf(parsed);\n if (typeof result === 'string' && result !== '') return decodeAudioBase64(result);\n if (isRecord(result) && !Array.isArray(result)) {\n const audio = stringField(result, 'audio') ?? stringField(result, 'data');\n if (audio !== undefined && audio !== '') return decodeAudioBase64(audio);\n }\n throw new Error('Speech response had no audio');\n}\n\nasync function postSpeech(\n model: SpeechModel<string>,\n body: unknown,\n options: ProviderRequestOptions | undefined\n): Promise<Response> {\n const route = session(model, options);\n const headers = route.headers;\n headers['Content-Type'] = 'application/json';\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(runUrl(model.baseUrl, model.id, route.account), {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n signal: requestSignal(options),\n });\n if (options?.onResponse !== undefined) {\n await options.onResponse({ status: response.status, headers: {} }, model as never);\n }\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 emitSpeech(\n stream: SpeechEventStream,\n model: SpeechModel<string>,\n text: string,\n format: AudioFormat,\n data: Uint8Array\n) {\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 onSpeechMessage(\n data: unknown,\n audio: Uint8Array[],\n fail: (message: string) => void\n) {\n const bytes = await messageBytes(data);\n if (bytes !== undefined) {\n if (bytes.byteLength > 0) audio.push(bytes);\n return;\n }\n if (typeof data !== 'string') throw new Error('Unsupported socket message');\n let parsed: unknown;\n try {\n parsed = JSON.parse(data);\n } catch {\n return;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) return;\n const kind = stringField(parsed, 'type');\n if (kind === 'Warning') {\n const description = stringField(parsed, 'description');\n if (description !== undefined) fail(description);\n }\n}\n\nasync function sendLines(\n socket: WebSocket,\n text: SpeechInput['text'],\n signal: AbortSignal | undefined\n): Promise<string> {\n if (typeof text === 'string') {\n socket.send(JSON.stringify({ type: 'Speak', text }));\n socket.send(JSON.stringify({ type: 'Flush' }));\n return text;\n }\n let combined = '';\n for await (const part of text) {\n if (signal?.aborted) throw new DOMException('aborted', 'AbortError');\n combined += part;\n socket.send(JSON.stringify({ type: 'Speak', text: part }));\n socket.send(JSON.stringify({ type: 'Flush' }));\n }\n return combined;\n}\n\nasync function speakLive(\n model: SpeechModel<string>,\n input: SpeechInput,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const route = session(model, options);\n const socket = connectSocket(\n workersAiSocket(route.account, route.gateway, model.id, ''),\n route.headers\n );\n const audio: Uint8Array[] = [];\n let failed: string | undefined;\n let chain = Promise.resolve();\n const closed = new Promise<void>((resolve) => {\n socket.addEventListener('close', () => resolve());\n });\n socket.addEventListener('message', (event) => {\n chain = chain\n .then(() =>\n onSpeechMessage(event.data, audio, (message) => {\n failed = message;\n })\n )\n .catch((error: unknown) => {\n failed = asVoiceError(error).error;\n });\n });\n await awaitOpen(socket, 'Cloudflare', (message) => {\n failed = message;\n });\n if (failed !== undefined) throw new Error(failed);\n const text = await sendLines(socket, input.text, options?.signal);\n socket.send(JSON.stringify({ type: 'Close' }));\n socket.close();\n await closed;\n await chain;\n if (failed !== undefined) throw new Error(failed);\n const data = concat(audio);\n if (data.byteLength === 0) throw new Error('Speech response had no audio');\n emitSpeech(stream, model, text, format, data);\n}\n\nasync function speakRest(\n model: SpeechModel<string>,\n input: SpeechInput,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const text = await readText(input.text, options?.signal);\n const payload = speechBody(model.id, text, input.voice, format, input.language);\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 postSpeech(model, body, options);\n emitSpeech(stream, model, text, format, await readSpeechBytes(response));\n}\n\nexport function speakCloudflare(\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 (liveSpeech(model.id, input.text, format)) {\n await speakLive(model, input, format, options, stream);\n return;\n }\n await speakRest(model, input, 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"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }