@ai-ecoverse/pi-audio 1.21.0 → 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.
- package/README.md +2 -0
- package/dist/cloudflare/catalog.js +165 -0
- package/dist/cloudflare/catalog.js.map +7 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/cloudflare/index.js.map +7 -0
- package/dist/cloudflare/listen.js +133 -0
- package/dist/cloudflare/listen.js.map +7 -0
- package/dist/cloudflare/speak.js +224 -0
- package/dist/cloudflare/speak.js.map +7 -0
- package/dist/cloudflare/transcribe.js +155 -0
- package/dist/cloudflare/transcribe.js.map +7 -0
- package/dist/cloudflare/wire.js +166 -0
- package/dist/cloudflare/wire.js.map +7 -0
- package/dist/types/cloudflare/catalog.d.ts +1 -0
- package/dist/types/cloudflare/index.d.ts +1 -0
- package/dist/types/cloudflare/listen.d.ts +4 -0
- package/dist/types/cloudflare/speak.d.ts +4 -0
- package/dist/types/cloudflare/transcribe.d.ts +4 -0
- package/dist/types/cloudflare/wire.d.ts +27 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ pi-audio does not apply that patch. Hosts such as slicc-agent record monkeypatch
|
|
|
23
23
|
|
|
24
24
|
`@ai-ecoverse/pi-audio/durable` appends a `voice.truncate` entry for a `VoiceCut`. The entry has no model messages. Its `edits` replace the target assistant message with the text heard so far and keep tool calls. The stored entry stays intact, and pi-durable derives the shorter message for the next model request. `@earendil-works/pi-durable` is an optional peer (`^1.1.0`); the root package does not import it. `VoiceCut.entryId` is the decimal string of pi-durable's numeric entry id.
|
|
25
25
|
|
|
26
|
+
`@ai-ecoverse/pi-audio/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`).
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { encodeAudioBase64 } from "../convert.js";
|
|
2
|
+
import { VoiceRequestError } from "../errors.js";
|
|
3
|
+
import {
|
|
4
|
+
applyPayload,
|
|
5
|
+
asVoiceError,
|
|
6
|
+
isRecord,
|
|
7
|
+
numberField,
|
|
8
|
+
readAudio,
|
|
9
|
+
requestSignal,
|
|
10
|
+
stringField
|
|
11
|
+
} from "../openai/wire.js";
|
|
12
|
+
import { priceAudio } from "../usage.js";
|
|
13
|
+
import { errorText, resultOf, runUrl, session } from "./wire.js";
|
|
14
|
+
function novaEncoding(mimeType) {
|
|
15
|
+
const mime = mimeType.toLowerCase();
|
|
16
|
+
if (mime.includes("mulaw") || mime === "audio/basic") return "mulaw";
|
|
17
|
+
if (mime.includes("flac")) return "flac";
|
|
18
|
+
if (mime.includes("opus")) return "opus";
|
|
19
|
+
if (mime.includes("pcm") || mime.includes("l16") || mime.includes("wav")) return "linear16";
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
function requestBody(modelId, audio, mimeType, language) {
|
|
23
|
+
if (modelId.includes("whisper")) {
|
|
24
|
+
const body2 = { audio };
|
|
25
|
+
if (language !== void 0) body2.language = language;
|
|
26
|
+
return body2;
|
|
27
|
+
}
|
|
28
|
+
const body = { audio: { body: audio, contentType: mimeType } };
|
|
29
|
+
const encoding = novaEncoding(mimeType);
|
|
30
|
+
if (encoding !== void 0) body.encoding = encoding;
|
|
31
|
+
if (language !== void 0) body.language = language;
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
function speakerOf(record) {
|
|
35
|
+
const value = record.speaker;
|
|
36
|
+
if (typeof value === "string" && value !== "") return value;
|
|
37
|
+
if (typeof value === "number") return String(value);
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
function segmentText(record) {
|
|
41
|
+
return stringField(record, "text") ?? stringField(record, "word") ?? "";
|
|
42
|
+
}
|
|
43
|
+
function segmentsFrom(value) {
|
|
44
|
+
if (!Array.isArray(value)) return [];
|
|
45
|
+
const segments = [];
|
|
46
|
+
for (const item of value) {
|
|
47
|
+
if (!isRecord(item) || Array.isArray(item)) continue;
|
|
48
|
+
const text = segmentText(item);
|
|
49
|
+
if (text === "") continue;
|
|
50
|
+
const segment = {
|
|
51
|
+
text,
|
|
52
|
+
start: numberField(item, "start") ?? 0,
|
|
53
|
+
end: numberField(item, "end") ?? 0
|
|
54
|
+
};
|
|
55
|
+
const speaker = speakerOf(item);
|
|
56
|
+
if (speaker !== void 0) segment.speaker = speaker;
|
|
57
|
+
segments.push(segment);
|
|
58
|
+
}
|
|
59
|
+
return segments;
|
|
60
|
+
}
|
|
61
|
+
function alternative(body) {
|
|
62
|
+
const results = body.results;
|
|
63
|
+
if (!isRecord(results) || Array.isArray(results)) return void 0;
|
|
64
|
+
const channels = results.channels;
|
|
65
|
+
if (!Array.isArray(channels)) return void 0;
|
|
66
|
+
const first = channels[0];
|
|
67
|
+
if (!isRecord(first) || Array.isArray(first)) return void 0;
|
|
68
|
+
const alternatives = first.alternatives;
|
|
69
|
+
if (!Array.isArray(alternatives)) return void 0;
|
|
70
|
+
const alt = alternatives[0];
|
|
71
|
+
if (!isRecord(alt) || Array.isArray(alt)) return void 0;
|
|
72
|
+
return alt;
|
|
73
|
+
}
|
|
74
|
+
function durationSeconds(body) {
|
|
75
|
+
const direct = numberField(body, "duration");
|
|
76
|
+
if (direct !== void 0) return direct;
|
|
77
|
+
const metadata = body.metadata;
|
|
78
|
+
if (!isRecord(metadata) || Array.isArray(metadata)) return 0;
|
|
79
|
+
const nested = numberField(metadata, "duration");
|
|
80
|
+
if (nested === void 0) return 0;
|
|
81
|
+
return nested;
|
|
82
|
+
}
|
|
83
|
+
function optionLanguage(options) {
|
|
84
|
+
const record = options;
|
|
85
|
+
if (record === void 0) return void 0;
|
|
86
|
+
if (typeof record.language !== "string") return void 0;
|
|
87
|
+
return record.language;
|
|
88
|
+
}
|
|
89
|
+
function transcriptFrom(body, model) {
|
|
90
|
+
const result = resultOf(body);
|
|
91
|
+
if (!isRecord(result) || Array.isArray(result))
|
|
92
|
+
throw new Error("Transcription response had no text");
|
|
93
|
+
const alt = alternative(result);
|
|
94
|
+
const nova = alt === void 0 ? void 0 : stringField(alt, "transcript");
|
|
95
|
+
const text = nova ?? stringField(result, "text");
|
|
96
|
+
if (text === void 0) throw new Error("Transcription response had no text");
|
|
97
|
+
const words = alt === void 0 ? void 0 : alt.words;
|
|
98
|
+
const segments = segmentsFrom(words ?? result.segments);
|
|
99
|
+
const seconds = durationSeconds(result);
|
|
100
|
+
const meters = {};
|
|
101
|
+
if (seconds > 0) meters.inputSeconds = seconds;
|
|
102
|
+
const transcript = { text, usage: priceAudio(model.audioCost, meters) };
|
|
103
|
+
const language = stringField(result, "language");
|
|
104
|
+
if (language !== void 0) transcript.language = language;
|
|
105
|
+
if (segments.length > 0) transcript.segments = segments;
|
|
106
|
+
return transcript;
|
|
107
|
+
}
|
|
108
|
+
async function postTranscription(model, body, options) {
|
|
109
|
+
const route = session(model, options);
|
|
110
|
+
const headers = route.headers;
|
|
111
|
+
headers["Content-Type"] = "application/json";
|
|
112
|
+
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
113
|
+
const response = await fetchImpl(runUrl(model.baseUrl, model.id, route.account), {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers,
|
|
116
|
+
body: JSON.stringify(body),
|
|
117
|
+
signal: requestSignal(options)
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const text2 = await response.text();
|
|
121
|
+
throw new Error(errorText(text2, response.status));
|
|
122
|
+
}
|
|
123
|
+
const text = await response.text();
|
|
124
|
+
if (text === "") throw new Error("Transcription response had no text");
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(text);
|
|
127
|
+
} catch {
|
|
128
|
+
return text;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function transcribeCloudflare(model, input, options) {
|
|
132
|
+
try {
|
|
133
|
+
if (model.id.includes("/flux")) throw new Error("Cloudflare flux is websocket only");
|
|
134
|
+
if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
|
|
135
|
+
const file = await readAudio(input, options?.fetch ?? globalThis.fetch, requestSignal(options));
|
|
136
|
+
const payload = requestBody(
|
|
137
|
+
model.id,
|
|
138
|
+
encodeAudioBase64(file.data),
|
|
139
|
+
file.mimeType,
|
|
140
|
+
optionLanguage(options)
|
|
141
|
+
);
|
|
142
|
+
const body = await applyPayload(payload, model, options);
|
|
143
|
+
if (!isRecord(body) || Array.isArray(body))
|
|
144
|
+
throw new Error("Transcription payload was not JSON");
|
|
145
|
+
const parsed = await postTranscription(model, body, options);
|
|
146
|
+
return transcriptFrom(parsed, model);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const voice = asVoiceError(error);
|
|
149
|
+
throw new VoiceRequestError(voice.reason, voice.error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
transcribeCloudflare
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=transcribe.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/cloudflare/transcribe.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport { encodeAudioBase64 } from '../convert.ts';\nimport { VoiceRequestError } from '../errors.ts';\nimport type { TranscriptionModel } from '../models.ts';\nimport {\n applyPayload,\n asVoiceError,\n isRecord,\n numberField,\n readAudio,\n requestSignal,\n stringField,\n} from '../openai/wire.ts';\nimport type { AudioInput, Transcript, TranscriptSegment } from '../transcription.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport { errorText, resultOf, runUrl, session } from './wire.ts';\n\nfunction novaEncoding(mimeType: string): string | undefined {\n const mime = mimeType.toLowerCase();\n if (mime.includes('mulaw') || mime === 'audio/basic') return 'mulaw';\n if (mime.includes('flac')) return 'flac';\n if (mime.includes('opus')) return 'opus';\n if (mime.includes('pcm') || mime.includes('l16') || mime.includes('wav')) return 'linear16';\n return undefined;\n}\n\nfunction requestBody(\n modelId: string,\n audio: string,\n mimeType: string,\n language: string | undefined\n): Record<string, unknown> {\n if (modelId.includes('whisper')) {\n const body: Record<string, unknown> = { audio };\n if (language !== undefined) body.language = language;\n return body;\n }\n const body: Record<string, unknown> = { audio: { body: audio, contentType: mimeType } };\n const encoding = novaEncoding(mimeType);\n if (encoding !== undefined) body.encoding = encoding;\n if (language !== undefined) body.language = language;\n return body;\n}\n\nfunction speakerOf(record: Record<string, unknown>): string | undefined {\n const value = record.speaker;\n if (typeof value === 'string' && value !== '') return value;\n if (typeof value === 'number') return String(value);\n return undefined;\n}\n\nfunction segmentText(record: Record<string, unknown>): string {\n return stringField(record, 'text') ?? stringField(record, 'word') ?? '';\n}\n\nfunction segmentsFrom(value: unknown): TranscriptSegment[] {\n if (!Array.isArray(value)) return [];\n const segments: TranscriptSegment[] = [];\n for (const item of value) {\n if (!isRecord(item) || Array.isArray(item)) continue;\n const text = segmentText(item);\n if (text === '') continue;\n const segment: TranscriptSegment = {\n text,\n start: numberField(item, 'start') ?? 0,\n end: numberField(item, 'end') ?? 0,\n };\n const speaker = speakerOf(item);\n if (speaker !== undefined) segment.speaker = speaker;\n segments.push(segment);\n }\n return segments;\n}\n\nfunction alternative(body: Record<string, unknown>): Record<string, unknown> | undefined {\n const results = body.results;\n if (!isRecord(results) || Array.isArray(results)) return undefined;\n const channels = results.channels;\n if (!Array.isArray(channels)) return undefined;\n const first = channels[0];\n if (!isRecord(first) || Array.isArray(first)) return undefined;\n const alternatives = first.alternatives;\n if (!Array.isArray(alternatives)) return undefined;\n const alt = alternatives[0];\n if (!isRecord(alt) || Array.isArray(alt)) return undefined;\n return alt;\n}\n\nfunction durationSeconds(body: Record<string, unknown>): number {\n const direct = numberField(body, 'duration');\n if (direct !== undefined) return direct;\n const metadata = body.metadata;\n if (!isRecord(metadata) || Array.isArray(metadata)) return 0;\n const nested = numberField(metadata, 'duration');\n if (nested === undefined) return 0;\n return nested;\n}\n\nfunction optionLanguage(options: ProviderRequestOptions | undefined): string | undefined {\n const record = options as { language?: unknown } | undefined;\n if (record === undefined) return undefined;\n if (typeof record.language !== 'string') return undefined;\n return record.language;\n}\n\nfunction transcriptFrom(body: unknown, model: TranscriptionModel<string>): Transcript {\n const result = resultOf(body);\n if (!isRecord(result) || Array.isArray(result))\n throw new Error('Transcription response had no text');\n const alt = alternative(result);\n const nova = alt === undefined ? undefined : stringField(alt, 'transcript');\n const text = nova ?? stringField(result, 'text');\n if (text === undefined) throw new Error('Transcription response had no text');\n const words = alt === undefined ? undefined : alt.words;\n const segments = segmentsFrom(words ?? result.segments);\n const seconds = durationSeconds(result);\n const meters: AudioCounters = {};\n if (seconds > 0) meters.inputSeconds = seconds;\n const transcript: Transcript = { text, usage: priceAudio(model.audioCost, meters) };\n const language = stringField(result, 'language');\n if (language !== undefined) transcript.language = language;\n if (segments.length > 0) transcript.segments = segments;\n return transcript;\n}\n\nasync function postTranscription(\n model: TranscriptionModel<string>,\n body: unknown,\n options: ProviderRequestOptions | undefined\n): Promise<unknown> {\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 (!response.ok) {\n const text = await response.text();\n throw new Error(errorText(text, response.status));\n }\n const text = await response.text();\n if (text === '') throw new Error('Transcription response had no text');\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nexport async function transcribeCloudflare(\n model: TranscriptionModel<string>,\n input: AudioInput,\n options?: ProviderRequestOptions\n): Promise<Transcript> {\n try {\n if (model.id.includes('/flux')) throw new Error('Cloudflare flux is websocket only');\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const file = await readAudio(input, options?.fetch ?? globalThis.fetch, requestSignal(options));\n const payload = requestBody(\n model.id,\n encodeAudioBase64(file.data),\n file.mimeType,\n optionLanguage(options)\n );\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body) || Array.isArray(body))\n throw new Error('Transcription payload was not JSON');\n const parsed = await postTranscription(model, body, options);\n return transcriptFrom(parsed, model);\n } catch (error) {\n const voice = asVoiceError(error);\n throw new VoiceRequestError(voice.reason, voice.error);\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,yBAAyB;AAClC,SAAS,yBAAyB;AAElC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAA6B,kBAAkB;AAC/C,SAAS,WAAW,UAAU,QAAQ,eAAe;AAErD,SAAS,aAAa,UAAsC;AAC1D,QAAM,OAAO,SAAS,YAAY;AAClC,MAAI,KAAK,SAAS,OAAO,KAAK,SAAS,cAAe,QAAO;AAC7D,MAAI,KAAK,SAAS,MAAM,EAAG,QAAO;AAClC,MAAI,KAAK,SAAS,MAAM,EAAG,QAAO;AAClC,MAAI,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,KAAK,KAAK,SAAS,KAAK,EAAG,QAAO;AACjF,SAAO;AACT;AAEA,SAAS,YACP,SACA,OACA,UACA,UACyB;AACzB,MAAI,QAAQ,SAAS,SAAS,GAAG;AAC/B,UAAMA,QAAgC,EAAE,MAAM;AAC9C,QAAI,aAAa,OAAW,CAAAA,MAAK,WAAW;AAC5C,WAAOA;AAAA,EACT;AACA,QAAM,OAAgC,EAAE,OAAO,EAAE,MAAM,OAAO,aAAa,SAAS,EAAE;AACtF,QAAM,WAAW,aAAa,QAAQ;AACtC,MAAI,aAAa,OAAW,MAAK,WAAW;AAC5C,MAAI,aAAa,OAAW,MAAK,WAAW;AAC5C,SAAO;AACT;AAEA,SAAS,UAAU,QAAqD;AACtE,QAAM,QAAQ,OAAO;AACrB,MAAI,OAAO,UAAU,YAAY,UAAU,GAAI,QAAO;AACtD,MAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK;AAClD,SAAO;AACT;AAEA,SAAS,YAAY,QAAyC;AAC5D,SAAO,YAAY,QAAQ,MAAM,KAAK,YAAY,QAAQ,MAAM,KAAK;AACvE;AAEA,SAAS,aAAa,OAAqC;AACzD,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO,CAAC;AACnC,QAAM,WAAgC,CAAC;AACvC,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,SAAS,IAAI,KAAK,MAAM,QAAQ,IAAI,EAAG;AAC5C,UAAM,OAAO,YAAY,IAAI;AAC7B,QAAI,SAAS,GAAI;AACjB,UAAM,UAA6B;AAAA,MACjC;AAAA,MACA,OAAO,YAAY,MAAM,OAAO,KAAK;AAAA,MACrC,KAAK,YAAY,MAAM,KAAK,KAAK;AAAA,IACnC;AACA,UAAM,UAAU,UAAU,IAAI;AAC9B,QAAI,YAAY,OAAW,SAAQ,UAAU;AAC7C,aAAS,KAAK,OAAO;AAAA,EACvB;AACA,SAAO;AACT;AAEA,SAAS,YAAY,MAAoE;AACvF,QAAM,UAAU,KAAK;AACrB,MAAI,CAAC,SAAS,OAAO,KAAK,MAAM,QAAQ,OAAO,EAAG,QAAO;AACzD,QAAM,WAAW,QAAQ;AACzB,MAAI,CAAC,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACrC,QAAM,QAAQ,SAAS,CAAC;AACxB,MAAI,CAAC,SAAS,KAAK,KAAK,MAAM,QAAQ,KAAK,EAAG,QAAO;AACrD,QAAM,eAAe,MAAM;AAC3B,MAAI,CAAC,MAAM,QAAQ,YAAY,EAAG,QAAO;AACzC,QAAM,MAAM,aAAa,CAAC;AAC1B,MAAI,CAAC,SAAS,GAAG,KAAK,MAAM,QAAQ,GAAG,EAAG,QAAO;AACjD,SAAO;AACT;AAEA,SAAS,gBAAgB,MAAuC;AAC9D,QAAM,SAAS,YAAY,MAAM,UAAU;AAC3C,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,WAAW,KAAK;AACtB,MAAI,CAAC,SAAS,QAAQ,KAAK,MAAM,QAAQ,QAAQ,EAAG,QAAO;AAC3D,QAAM,SAAS,YAAY,UAAU,UAAU;AAC/C,MAAI,WAAW,OAAW,QAAO;AACjC,SAAO;AACT;AAEA,SAAS,eAAe,SAAiE;AACvF,QAAM,SAAS;AACf,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI,OAAO,OAAO,aAAa,SAAU,QAAO;AAChD,SAAO,OAAO;AAChB;AAEA,SAAS,eAAe,MAAe,OAA+C;AACpF,QAAM,SAAS,SAAS,IAAI;AAC5B,MAAI,CAAC,SAAS,MAAM,KAAK,MAAM,QAAQ,MAAM;AAC3C,UAAM,IAAI,MAAM,oCAAoC;AACtD,QAAM,MAAM,YAAY,MAAM;AAC9B,QAAM,OAAO,QAAQ,SAAY,SAAY,YAAY,KAAK,YAAY;AAC1E,QAAM,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC/C,MAAI,SAAS,OAAW,OAAM,IAAI,MAAM,oCAAoC;AAC5E,QAAM,QAAQ,QAAQ,SAAY,SAAY,IAAI;AAClD,QAAM,WAAW,aAAa,SAAS,OAAO,QAAQ;AACtD,QAAM,UAAU,gBAAgB,MAAM;AACtC,QAAM,SAAwB,CAAC;AAC/B,MAAI,UAAU,EAAG,QAAO,eAAe;AACvC,QAAM,aAAyB,EAAE,MAAM,OAAO,WAAW,MAAM,WAAW,MAAM,EAAE;AAClF,QAAM,WAAW,YAAY,QAAQ,UAAU;AAC/C,MAAI,aAAa,OAAW,YAAW,WAAW;AAClD,MAAI,SAAS,SAAS,EAAG,YAAW,WAAW;AAC/C,SAAO;AACT;AAEA,eAAe,kBACb,OACA,MACA,SACkB;AAClB,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,UAAU,MAAM;AACtB,UAAQ,cAAc,IAAI;AAC1B,QAAM,YAAY,SAAS,SAAS,WAAW;AAC/C,QAAM,WAAW,MAAM,UAAU,OAAO,MAAM,SAAS,MAAM,IAAI,MAAM,OAAO,GAAG;AAAA,IAC/E,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,KAAK,UAAU,IAAI;AAAA,IACzB,QAAQ,cAAc,OAAO;AAAA,EAC/B,CAAC;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAMC,QAAO,MAAM,SAAS,KAAK;AACjC,UAAM,IAAI,MAAM,UAAUA,OAAM,SAAS,MAAM,CAAC;AAAA,EAClD;AACA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,MAAI,SAAS,GAAI,OAAM,IAAI,MAAM,oCAAoC;AACrE,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,qBACpB,OACA,OACA,SACqB;AACrB,MAAI;AACF,QAAI,MAAM,GAAG,SAAS,OAAO,EAAG,OAAM,IAAI,MAAM,mCAAmC;AACnF,QAAI,SAAS,QAAQ,QAAS,OAAM,IAAI,aAAa,WAAW,YAAY;AAC5E,UAAM,OAAO,MAAM,UAAU,OAAO,SAAS,SAAS,WAAW,OAAO,cAAc,OAAO,CAAC;AAC9F,UAAM,UAAU;AAAA,MACd,MAAM;AAAA,MACN,kBAAkB,KAAK,IAAI;AAAA,MAC3B,KAAK;AAAA,MACL,eAAe,OAAO;AAAA,IACxB;AACA,UAAM,OAAO,MAAM,aAAa,SAAS,OAAO,OAAO;AACvD,QAAI,CAAC,SAAS,IAAI,KAAK,MAAM,QAAQ,IAAI;AACvC,YAAM,IAAI,MAAM,oCAAoC;AACtD,UAAM,SAAS,MAAM,kBAAkB,OAAO,MAAM,OAAO;AAC3D,WAAO,eAAe,QAAQ,KAAK;AAAA,EACrC,SAAS,OAAO;AACd,UAAM,QAAQ,aAAa,KAAK;AAChC,UAAM,IAAI,kBAAkB,MAAM,QAAQ,MAAM,KAAK;AAAA,EACvD;AACF;",
|
|
6
|
+
"names": ["body", "text"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { isRecord, requestHeaders, stringField } from "../openai/wire.js";
|
|
2
|
+
const placeholder = "{account_id}";
|
|
3
|
+
const speechEncodings = {
|
|
4
|
+
pcm16: "linear16",
|
|
5
|
+
mp3: "mp3",
|
|
6
|
+
flac: "flac",
|
|
7
|
+
mulaw: "mulaw",
|
|
8
|
+
alaw: "alaw",
|
|
9
|
+
opus: "opus"
|
|
10
|
+
};
|
|
11
|
+
function headerValue(headers, name) {
|
|
12
|
+
const expected = name.toLowerCase();
|
|
13
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
14
|
+
if (key.toLowerCase() === expected && value.trim() !== "") return value;
|
|
15
|
+
}
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
function takeHeader(headers, name) {
|
|
19
|
+
const expected = name.toLowerCase();
|
|
20
|
+
let found;
|
|
21
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
22
|
+
if (key.toLowerCase() !== expected) continue;
|
|
23
|
+
if (found === void 0 && value.trim() !== "") found = value.trim();
|
|
24
|
+
delete headers[key];
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
function accountFromUrl(baseUrl) {
|
|
29
|
+
const match = /\/accounts\/([^/]+)\/ai\/run/.exec(baseUrl);
|
|
30
|
+
const found = match?.[1];
|
|
31
|
+
if (found === void 0 || found === placeholder) return void 0;
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
function gatewayId(headers) {
|
|
35
|
+
const value = headerValue(headers, "cf-aig-gateway-id");
|
|
36
|
+
if (value === void 0) return "default";
|
|
37
|
+
return value.trim();
|
|
38
|
+
}
|
|
39
|
+
function stampGatewayAuth(headers) {
|
|
40
|
+
const gateway = headerValue(headers, "cf-aig-authorization");
|
|
41
|
+
const authorization = headerValue(headers, "authorization");
|
|
42
|
+
if (gateway === void 0 && authorization !== void 0 && authorization.toLowerCase().startsWith("bearer ")) {
|
|
43
|
+
headers["cf-aig-authorization"] = authorization.slice(authorization.indexOf(" ") + 1).trim();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (gateway !== void 0 && authorization === void 0)
|
|
47
|
+
headers.Authorization = `Bearer ${gateway}`;
|
|
48
|
+
}
|
|
49
|
+
function session(model, options) {
|
|
50
|
+
const headers = requestHeaders(model, options);
|
|
51
|
+
const fromHeader = takeHeader(headers, "cf-account-id");
|
|
52
|
+
const account = fromHeader ?? accountFromUrl(model.baseUrl);
|
|
53
|
+
if (account === void 0) throw new Error("No Cloudflare account id");
|
|
54
|
+
const gateway = gatewayId(headers);
|
|
55
|
+
stampGatewayAuth(headers);
|
|
56
|
+
return { headers, account, gateway };
|
|
57
|
+
}
|
|
58
|
+
function runUrl(baseUrl, modelId, account) {
|
|
59
|
+
const base = baseUrl.replaceAll(placeholder, account).replace(/\/$/, "");
|
|
60
|
+
return `${base}/${modelId}`;
|
|
61
|
+
}
|
|
62
|
+
function workersAiSocket(account, gateway, modelId, query) {
|
|
63
|
+
const extra = query === "" ? "" : `&${query}`;
|
|
64
|
+
return `wss://gateway.ai.cloudflare.com/v1/${account}/${gateway}/workers-ai?model=${encodeURIComponent(modelId)}${extra}`;
|
|
65
|
+
}
|
|
66
|
+
function auraEncoding(format) {
|
|
67
|
+
const name = speechEncodings[format.encoding];
|
|
68
|
+
if (!name) throw new Error(`Unsupported speech format ${format.encoding}`);
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
function listenQuery(modelId, format) {
|
|
72
|
+
if (format.encoding !== "pcm16" && format.encoding !== "mulaw") {
|
|
73
|
+
throw new Error(`Unsupported audio format ${format.encoding}`);
|
|
74
|
+
}
|
|
75
|
+
if (modelId.includes("/flux") && format.encoding !== "pcm16") {
|
|
76
|
+
throw new Error(`Unsupported audio format ${format.encoding}`);
|
|
77
|
+
}
|
|
78
|
+
const encoding = format.encoding === "mulaw" ? "mulaw" : "linear16";
|
|
79
|
+
const shared = `encoding=${encoding}&sample_rate=${format.sampleRate}`;
|
|
80
|
+
if (modelId.includes("/flux")) return shared;
|
|
81
|
+
return `${shared}&interim_results=true&vad_events=true`;
|
|
82
|
+
}
|
|
83
|
+
function sameFormat(left, right) {
|
|
84
|
+
return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
|
|
85
|
+
}
|
|
86
|
+
function connectSocket(url, headers) {
|
|
87
|
+
const Socket = WebSocket;
|
|
88
|
+
return new Socket(url, { headers });
|
|
89
|
+
}
|
|
90
|
+
function awaitOpen(socket, label, fail) {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
let opened = false;
|
|
93
|
+
socket.addEventListener("open", () => {
|
|
94
|
+
opened = true;
|
|
95
|
+
resolve();
|
|
96
|
+
});
|
|
97
|
+
socket.addEventListener("error", () => {
|
|
98
|
+
fail(`${label} socket failed`);
|
|
99
|
+
if (!opened) resolve();
|
|
100
|
+
});
|
|
101
|
+
socket.addEventListener("close", () => {
|
|
102
|
+
if (!opened) {
|
|
103
|
+
fail(`${label} socket closed`);
|
|
104
|
+
resolve();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function audioFrame(data) {
|
|
110
|
+
const copy = new Uint8Array(data.byteLength);
|
|
111
|
+
copy.set(data);
|
|
112
|
+
return copy.buffer;
|
|
113
|
+
}
|
|
114
|
+
async function messageBytes(data) {
|
|
115
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
116
|
+
if (data instanceof Uint8Array) return data;
|
|
117
|
+
if (typeof Blob !== "undefined" && data instanceof Blob)
|
|
118
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
function failureMessage(body, fallback) {
|
|
122
|
+
const errors = body.errors;
|
|
123
|
+
if (!Array.isArray(errors)) return fallback;
|
|
124
|
+
const first = errors[0];
|
|
125
|
+
if (!isRecord(first) || Array.isArray(first)) return fallback;
|
|
126
|
+
const message = stringField(first, "message");
|
|
127
|
+
if (message === void 0 || message === "") return fallback;
|
|
128
|
+
return message;
|
|
129
|
+
}
|
|
130
|
+
function resultOf(body) {
|
|
131
|
+
if (!isRecord(body) || Array.isArray(body)) return body;
|
|
132
|
+
if (body.success === false) throw new Error(failureMessage(body, "Cloudflare request failed"));
|
|
133
|
+
if (!Object.hasOwn(body, "result")) return body;
|
|
134
|
+
return body.result;
|
|
135
|
+
}
|
|
136
|
+
function errorText(text, status) {
|
|
137
|
+
if (text === "") return `HTTP ${status}`;
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(text);
|
|
141
|
+
} catch {
|
|
142
|
+
return text;
|
|
143
|
+
}
|
|
144
|
+
if (!isRecord(parsed) || Array.isArray(parsed)) return text;
|
|
145
|
+
if (Array.isArray(parsed.errors)) return failureMessage(parsed, text);
|
|
146
|
+
const message = stringField(parsed, "message");
|
|
147
|
+
if (message !== void 0) return message;
|
|
148
|
+
return text;
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
audioFrame,
|
|
152
|
+
auraEncoding,
|
|
153
|
+
awaitOpen,
|
|
154
|
+
connectSocket,
|
|
155
|
+
errorText,
|
|
156
|
+
failureMessage,
|
|
157
|
+
gatewayId,
|
|
158
|
+
listenQuery,
|
|
159
|
+
messageBytes,
|
|
160
|
+
resultOf,
|
|
161
|
+
runUrl,
|
|
162
|
+
sameFormat,
|
|
163
|
+
session,
|
|
164
|
+
workersAiSocket
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=wire.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/cloudflare/wire.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport { isRecord, requestHeaders, stringField } from '../openai/wire.ts';\n\nconst placeholder = '{account_id}';\n\nconst speechEncodings: Record<string, string> = {\n pcm16: 'linear16',\n mp3: 'mp3',\n flac: 'flac',\n mulaw: 'mulaw',\n alaw: 'alaw',\n opus: 'opus',\n};\n\ninterface RouteModel {\n provider: string;\n baseUrl: string;\n headers?: Record<string, string>;\n}\n\nexport interface CloudflareRoute {\n headers: Record<string, string>;\n account: string;\n gateway: string;\n}\n\nfunction headerValue(headers: Record<string, string>, name: string): string | undefined {\n const expected = name.toLowerCase();\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === expected && value.trim() !== '') return value;\n }\n return undefined;\n}\n\nfunction takeHeader(headers: Record<string, string>, name: string): string | undefined {\n const expected = name.toLowerCase();\n let found: string | undefined;\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() !== expected) continue;\n if (found === undefined && value.trim() !== '') found = value.trim();\n delete headers[key];\n }\n return found;\n}\n\nfunction accountFromUrl(baseUrl: string): string | undefined {\n const match = /\\/accounts\\/([^/]+)\\/ai\\/run/.exec(baseUrl);\n const found = match?.[1];\n if (found === undefined || found === placeholder) return undefined;\n return found;\n}\n\nexport function gatewayId(headers: Record<string, string>): string {\n const value = headerValue(headers, 'cf-aig-gateway-id');\n if (value === undefined) return 'default';\n return value.trim();\n}\n\nfunction stampGatewayAuth(headers: Record<string, string>): void {\n const gateway = headerValue(headers, 'cf-aig-authorization');\n const authorization = headerValue(headers, 'authorization');\n if (\n gateway === undefined &&\n authorization !== undefined &&\n authorization.toLowerCase().startsWith('bearer ')\n ) {\n headers['cf-aig-authorization'] = authorization.slice(authorization.indexOf(' ') + 1).trim();\n return;\n }\n if (gateway !== undefined && authorization === undefined)\n headers.Authorization = `Bearer ${gateway}`;\n}\n\nexport function session(\n model: RouteModel,\n options: ProviderRequestOptions | undefined\n): CloudflareRoute {\n const headers = requestHeaders(model, options);\n const fromHeader = takeHeader(headers, 'cf-account-id');\n const account = fromHeader ?? accountFromUrl(model.baseUrl);\n if (account === undefined) throw new Error('No Cloudflare account id');\n const gateway = gatewayId(headers);\n stampGatewayAuth(headers);\n return { headers, account, gateway };\n}\n\nexport function runUrl(baseUrl: string, modelId: string, account: string): string {\n const base = baseUrl.replaceAll(placeholder, account).replace(/\\/$/, '');\n return `${base}/${modelId}`;\n}\n\nexport function workersAiSocket(\n account: string,\n gateway: string,\n modelId: string,\n query: string\n): string {\n const extra = query === '' ? '' : `&${query}`;\n return `wss://gateway.ai.cloudflare.com/v1/${account}/${gateway}/workers-ai?model=${encodeURIComponent(modelId)}${extra}`;\n}\n\nexport function auraEncoding(format: AudioFormat): string {\n const name = speechEncodings[format.encoding];\n if (!name) throw new Error(`Unsupported speech format ${format.encoding}`);\n return name;\n}\n\nexport function listenQuery(modelId: string, format: AudioFormat): string {\n if (format.encoding !== 'pcm16' && format.encoding !== 'mulaw') {\n throw new Error(`Unsupported audio format ${format.encoding}`);\n }\n if (modelId.includes('/flux') && format.encoding !== 'pcm16') {\n throw new Error(`Unsupported audio format ${format.encoding}`);\n }\n const encoding = format.encoding === 'mulaw' ? 'mulaw' : 'linear16';\n const shared = `encoding=${encoding}&sample_rate=${format.sampleRate}`;\n if (modelId.includes('/flux')) return shared;\n return `${shared}&interim_results=true&vad_events=true`;\n}\n\nexport function sameFormat(left: AudioFormat, right: AudioFormat): boolean {\n return (\n left.encoding === right.encoding &&\n left.sampleRate === right.sampleRate &&\n left.channels === right.channels\n );\n}\n\nexport function connectSocket(url: string, headers: Record<string, string>): WebSocket {\n const Socket = WebSocket as unknown as new (\n address: string,\n init: { headers: Record<string, string> }\n ) => WebSocket;\n return new Socket(url, { headers });\n}\n\nexport function awaitOpen(\n socket: WebSocket,\n label: string,\n fail: (message: string) => void\n): Promise<void> {\n return new Promise((resolve) => {\n let opened = false;\n socket.addEventListener('open', () => {\n opened = true;\n resolve();\n });\n socket.addEventListener('error', () => {\n fail(`${label} socket failed`);\n if (!opened) resolve();\n });\n socket.addEventListener('close', () => {\n if (!opened) {\n fail(`${label} socket closed`);\n resolve();\n }\n });\n });\n}\n\nexport function audioFrame(data: Uint8Array): ArrayBuffer {\n const copy = new Uint8Array(data.byteLength);\n copy.set(data);\n return copy.buffer;\n}\n\nexport async function messageBytes(data: unknown): Promise<Uint8Array | undefined> {\n if (data instanceof ArrayBuffer) return new Uint8Array(data);\n if (data instanceof Uint8Array) return data;\n if (typeof Blob !== 'undefined' && data instanceof Blob)\n return new Uint8Array(await data.arrayBuffer());\n return undefined;\n}\n\nexport function failureMessage(body: Record<string, unknown>, fallback: string): string {\n const errors = body.errors;\n if (!Array.isArray(errors)) return fallback;\n const first = errors[0];\n if (!isRecord(first) || Array.isArray(first)) return fallback;\n const message = stringField(first, 'message');\n if (message === undefined || message === '') return fallback;\n return message;\n}\n\nexport function resultOf(body: unknown): unknown {\n if (!isRecord(body) || Array.isArray(body)) return body;\n if (body.success === false) throw new Error(failureMessage(body, 'Cloudflare request failed'));\n if (!Object.hasOwn(body, 'result')) return body;\n return body.result;\n}\n\nexport function 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 if (Array.isArray(parsed.errors)) return failureMessage(parsed, text);\n const message = stringField(parsed, 'message');\n if (message !== undefined) return message;\n return text;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerCloudflare(): () => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerCloudflare } from './catalog.ts';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AudioSource } from '../audio.ts';
|
|
2
|
+
import type { TranscriptionModel } from '../models.ts';
|
|
3
|
+
import { type ListenOptions, TranscriptionEventStream } from '../transcription.ts';
|
|
4
|
+
export declare function listenCloudflare(model: TranscriptionModel<string>, audio: AudioSource, options?: ListenOptions): TranscriptionEventStream;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { SpeechModel } from '../models.ts';
|
|
3
|
+
import { SpeechEventStream, type SpeechInput } from '../speech.ts';
|
|
4
|
+
export declare function speakCloudflare(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): SpeechEventStream;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { TranscriptionModel } from '../models.ts';
|
|
3
|
+
import type { AudioInput, Transcript } from '../transcription.ts';
|
|
4
|
+
export declare function transcribeCloudflare(model: TranscriptionModel<string>, input: AudioInput, options?: ProviderRequestOptions): Promise<Transcript>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
|
|
2
|
+
import type { AudioFormat } from '../audio.ts';
|
|
3
|
+
interface RouteModel {
|
|
4
|
+
provider: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export interface CloudflareRoute {
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
account: string;
|
|
11
|
+
gateway: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function gatewayId(headers: Record<string, string>): string;
|
|
14
|
+
export declare function session(model: RouteModel, options: ProviderRequestOptions | undefined): CloudflareRoute;
|
|
15
|
+
export declare function runUrl(baseUrl: string, modelId: string, account: string): string;
|
|
16
|
+
export declare function workersAiSocket(account: string, gateway: string, modelId: string, query: string): string;
|
|
17
|
+
export declare function auraEncoding(format: AudioFormat): string;
|
|
18
|
+
export declare function listenQuery(modelId: string, format: AudioFormat): string;
|
|
19
|
+
export declare function sameFormat(left: AudioFormat, right: AudioFormat): boolean;
|
|
20
|
+
export declare function connectSocket(url: string, headers: Record<string, string>): WebSocket;
|
|
21
|
+
export declare function awaitOpen(socket: WebSocket, label: string, fail: (message: string) => void): Promise<void>;
|
|
22
|
+
export declare function audioFrame(data: Uint8Array): ArrayBuffer;
|
|
23
|
+
export declare function messageBytes(data: unknown): Promise<Uint8Array | undefined>;
|
|
24
|
+
export declare function failureMessage(body: Record<string, unknown>, fallback: string): string;
|
|
25
|
+
export declare function resultOf(body: unknown): unknown;
|
|
26
|
+
export declare function errorText(text: string, status: number): string;
|
|
27
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/pi-audio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Voice bindings for pi: speech-to-text, text-to-speech and realtime speech for pi-coding-agent plugins and slicc-agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"types": "./dist/types/google/index.d.ts",
|
|
63
63
|
"default": "./dist/google/index.js"
|
|
64
64
|
},
|
|
65
|
+
"./cloudflare": {
|
|
66
|
+
"types": "./dist/types/cloudflare/index.d.ts",
|
|
67
|
+
"default": "./dist/cloudflare/index.js"
|
|
68
|
+
},
|
|
65
69
|
"./durable": {
|
|
66
70
|
"types": "./dist/types/durable/index.d.ts",
|
|
67
71
|
"default": "./dist/durable/index.js"
|