@ai-ecoverse/pi-audio 1.19.0 → 1.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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/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
+
26
28
  `@ai-ecoverse/pi-audio/fish` registers Fish Audio speech adapters when `registerFish()` is called. `transcribe-1-pro` posts multipart field `audio` to `/asr` with header `model: transcribe-1-pro` ($0.36 per hour). `ignore_timestamps` is false, so word `segments` and `speaker_turns` come back. There is no realtime ASR. `s2.1-pro` posts JSON to `/tts` for a string and streams MessagePack `text` events on `wss://…/tts/live` for an async iterable (`textStreaming: true`). `s2.1-pro-free` is the same path at $0. Paid speech is $15 per million UTF-8 bytes, stored as $0.015 per thousand characters. Voices are caller `reference_id`s (`cloning: true`).
27
29
 
28
30
  `@ai-ecoverse/pi-audio/mistral` registers Mistral speech adapters when `registerMistral()` is called. `voxtral-mini-latest` posts multipart audio to `/audio/transcriptions` ($0.003 per minute, diarization on). `voxtral-mini-transcribe-realtime-2602` streams base64 PCM on `wss://…/audio/transcriptions/realtime` after `session.created` ($0.006 per minute). Realtime has no server endpointing, so `turnDetection` is `none` and the registry VAD wrapper applies. `voxtral-mini-tts-2603` posts JSON to `/audio/speech` and reads SSE `speech.audio.delta` float32 PCM at 24 kHz ($0.016 per thousand characters). Hosted voice ids such as `en_paul_neutral` differ from the open-weight presets. `textStreaming` is false.
@@ -0,0 +1,61 @@
1
+ import { registerVoiceModel } from "../models.js";
2
+ import { registerSpeechApiProvider } from "../registry.js";
3
+ import { speakGoogle } from "./speak.js";
4
+ const tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
5
+ const google = "https://generativelanguage.googleapis.com/v1beta";
6
+ const format = (encoding, sampleRate) => ({
7
+ encoding,
8
+ sampleRate,
9
+ channels: 1
10
+ });
11
+ const outputFormats = [
12
+ format("wav", 24e3),
13
+ format("pcm16", 24e3),
14
+ format("mulaw", 8e3),
15
+ format("alaw", 8e3)
16
+ ];
17
+ const voices = [
18
+ { id: "Kore", name: "Kore" },
19
+ { id: "Puck", name: "Puck" },
20
+ { id: "Charon", name: "Charon" },
21
+ { id: "Fenrir", name: "Fenrir" },
22
+ { id: "Aoede", name: "Aoede" }
23
+ ];
24
+ function speechModel(id, perOutputMinute) {
25
+ return {
26
+ id,
27
+ name: id,
28
+ api: "google-tts",
29
+ provider: "google",
30
+ baseUrl: google,
31
+ type: "speech",
32
+ textStreaming: false,
33
+ outputFormats,
34
+ voices,
35
+ cloning: true,
36
+ cost: tokenCost,
37
+ audioCost: { perOutputMinute }
38
+ };
39
+ }
40
+ function registerGoogle() {
41
+ const models = [
42
+ speechModel("gemini-3.8-flash-tts", 25 * 9 * 60 / 1e6),
43
+ speechModel("gemini-3.8-flash-lite-tts", 25 * 6 * 60 / 1e6)
44
+ ];
45
+ const unregisterModels = models.map((model) => registerVoiceModel(model));
46
+ const unregisterSpeech = registerSpeechApiProvider({
47
+ api: "google-tts",
48
+ speak: speakGoogle
49
+ });
50
+ let released = false;
51
+ return () => {
52
+ if (released) return;
53
+ released = true;
54
+ for (const unregister of unregisterModels) unregister();
55
+ unregisterSpeech();
56
+ };
57
+ }
58
+ export {
59
+ registerGoogle
60
+ };
61
+ //# sourceMappingURL=catalog.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/google/catalog.ts"],
4
+ "sourcesContent": ["import type { AudioFormat } from '../audio.ts';\nimport { registerVoiceModel, type SpeechModel, type VoiceInfo } from '../models.ts';\nimport { registerSpeechApiProvider } from '../registry.ts';\nimport { speakGoogle } from './speak.ts';\n\nconst tokenCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };\nconst google = 'https://generativelanguage.googleapis.com/v1beta';\n\nconst format = (encoding: 'wav' | 'pcm16' | 'mulaw' | 'alaw', sampleRate: number): AudioFormat => ({\n encoding,\n sampleRate,\n channels: 1,\n});\n\nconst outputFormats = [\n format('wav', 24000),\n format('pcm16', 24000),\n format('mulaw', 8000),\n format('alaw', 8000),\n];\n\nconst voices: VoiceInfo[] = [\n { id: 'Kore', name: 'Kore' },\n { id: 'Puck', name: 'Puck' },\n { id: 'Charon', name: 'Charon' },\n { id: 'Fenrir', name: 'Fenrir' },\n { id: 'Aoede', name: 'Aoede' },\n];\n\nfunction speechModel(id: string, perOutputMinute: number): SpeechModel<string> {\n return {\n id,\n name: id,\n api: 'google-tts',\n provider: 'google',\n baseUrl: google,\n type: 'speech',\n textStreaming: false,\n outputFormats,\n voices,\n cloning: true,\n cost: tokenCost,\n audioCost: { perOutputMinute },\n };\n}\n\nexport function registerGoogle(): () => void {\n const models = [\n speechModel('gemini-3.8-flash-tts', (25 * 9 * 60) / 1_000_000),\n speechModel('gemini-3.8-flash-lite-tts', (25 * 6 * 60) / 1_000_000),\n ];\n const unregisterModels = models.map((model) => registerVoiceModel(model));\n const unregisterSpeech = registerSpeechApiProvider({\n api: 'google-tts',\n speak: speakGoogle,\n });\n let released = false;\n return () => {\n if (released) return;\n released = true;\n for (const unregister of unregisterModels) unregister();\n unregisterSpeech();\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,0BAA4D;AACrE,SAAS,iCAAiC;AAC1C,SAAS,mBAAmB;AAE5B,MAAM,YAAY,EAAE,OAAO,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,EAAE;AACrE,MAAM,SAAS;AAEf,MAAM,SAAS,CAAC,UAA8C,gBAAqC;AAAA,EACjG;AAAA,EACA;AAAA,EACA,UAAU;AACZ;AAEA,MAAM,gBAAgB;AAAA,EACpB,OAAO,OAAO,IAAK;AAAA,EACnB,OAAO,SAAS,IAAK;AAAA,EACrB,OAAO,SAAS,GAAI;AAAA,EACpB,OAAO,QAAQ,GAAI;AACrB;AAEA,MAAM,SAAsB;AAAA,EAC1B,EAAE,IAAI,QAAQ,MAAM,OAAO;AAAA,EAC3B,EAAE,IAAI,QAAQ,MAAM,OAAO;AAAA,EAC3B,EAAE,IAAI,UAAU,MAAM,SAAS;AAAA,EAC/B,EAAE,IAAI,UAAU,MAAM,SAAS;AAAA,EAC/B,EAAE,IAAI,SAAS,MAAM,QAAQ;AAC/B;AAEA,SAAS,YAAY,IAAY,iBAA8C;AAC7E,SAAO;AAAA,IACL;AAAA,IACA,MAAM;AAAA,IACN,KAAK;AAAA,IACL,UAAU;AAAA,IACV,SAAS;AAAA,IACT,MAAM;AAAA,IACN,eAAe;AAAA,IACf;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW,EAAE,gBAAgB;AAAA,EAC/B;AACF;AAEO,SAAS,iBAA6B;AAC3C,QAAM,SAAS;AAAA,IACb,YAAY,wBAAyB,KAAK,IAAI,KAAM,GAAS;AAAA,IAC7D,YAAY,6BAA8B,KAAK,IAAI,KAAM,GAAS;AAAA,EACpE;AACA,QAAM,mBAAmB,OAAO,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC;AACxE,QAAM,mBAAmB,0BAA0B;AAAA,IACjD,KAAK;AAAA,IACL,OAAO;AAAA,EACT,CAAC;AACD,MAAI,WAAW;AACf,SAAO,MAAM;AACX,QAAI,SAAU;AACd,eAAW;AACX,eAAW,cAAc,iBAAkB,YAAW;AACtD,qBAAiB;AAAA,EACnB;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ import { registerGoogle } from "./catalog.js";
2
+ export {
3
+ registerGoogle
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/google/index.ts"],
4
+ "sourcesContent": ["export { registerGoogle } from './catalog.ts';\n"],
5
+ "mappings": "AAAA,SAAS,sBAAsB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,160 @@
1
+ import { chunkSeconds } from "../audio.js";
2
+ import { decodeAudioBase64 } from "../convert.js";
3
+ import {
4
+ applyPayload,
5
+ asVoiceError,
6
+ endpoint,
7
+ isRecord,
8
+ readJson,
9
+ requestSignal,
10
+ stringField
11
+ } from "../openai/wire.js";
12
+ import { SpeechEventStream } from "../speech.js";
13
+ import { priceAudio } from "../usage.js";
14
+ import { googleHeaders, sameFormat, speechMime } from "./wire.js";
15
+ function resolveFormat(model, requested) {
16
+ const fallback = model.outputFormats[0];
17
+ if (!fallback) throw new Error("No output format");
18
+ if (!requested) return fallback;
19
+ const match = model.outputFormats.find((format) => sameFormat(format, requested));
20
+ if (!match) throw new Error(`Unsupported speech format ${requested.encoding}`);
21
+ return match;
22
+ }
23
+ function speechBody(text, voice, format) {
24
+ return {
25
+ contents: [{ role: "user", parts: [{ text }] }],
26
+ generationConfig: {
27
+ responseModalities: ["AUDIO"],
28
+ responseFormat: { audio: { mimeType: speechMime(format) } },
29
+ speechConfig: {
30
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } }
31
+ }
32
+ }
33
+ };
34
+ }
35
+ async function readText(text, signal) {
36
+ if (typeof text === "string") return text;
37
+ let combined = "";
38
+ for await (const part of text) {
39
+ if (signal?.aborted) throw new DOMException("aborted", "AbortError");
40
+ combined += part;
41
+ }
42
+ return combined;
43
+ }
44
+ function errorText(text, status) {
45
+ if (text === "") return `HTTP ${status}`;
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(text);
49
+ } catch {
50
+ return text;
51
+ }
52
+ if (!isRecord(parsed) || Array.isArray(parsed)) return text;
53
+ const direct = stringField(parsed, "message");
54
+ if (direct !== void 0) return direct;
55
+ const error = parsed.error;
56
+ if (typeof error === "string") return error;
57
+ if (isRecord(error) && !Array.isArray(error)) {
58
+ const nested = stringField(error, "message");
59
+ if (nested !== void 0) return nested;
60
+ }
61
+ return text;
62
+ }
63
+ function inlineData(part) {
64
+ const camel = part.inlineData;
65
+ const snake = part.inline_data;
66
+ const inline = isRecord(camel) && !Array.isArray(camel) ? camel : snake;
67
+ if (!isRecord(inline) || Array.isArray(inline)) return void 0;
68
+ return stringField(inline, "data");
69
+ }
70
+ function audioParts(body) {
71
+ const candidates = body.candidates;
72
+ if (!Array.isArray(candidates)) return [];
73
+ const first = candidates[0];
74
+ if (!isRecord(first) || Array.isArray(first)) return [];
75
+ const content = first.content;
76
+ if (!isRecord(content) || Array.isArray(content)) return [];
77
+ const parts = content.parts;
78
+ if (!Array.isArray(parts)) return [];
79
+ const chunks = [];
80
+ for (const part of parts) {
81
+ if (!isRecord(part) || Array.isArray(part)) continue;
82
+ const data = inlineData(part);
83
+ if (data === void 0 || data === "") continue;
84
+ chunks.push(data);
85
+ }
86
+ return chunks;
87
+ }
88
+ function concat(chunks) {
89
+ const length = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
90
+ const out = new Uint8Array(length);
91
+ let offset = 0;
92
+ for (const chunk of chunks) {
93
+ out.set(chunk, offset);
94
+ offset += chunk.byteLength;
95
+ }
96
+ return out;
97
+ }
98
+ async function googleFetch(model, options, url, body) {
99
+ const headers = googleHeaders(model, options);
100
+ headers["Content-Type"] = "application/json";
101
+ const fetchImpl = options?.fetch ?? globalThis.fetch;
102
+ const response = await fetchImpl(url, {
103
+ method: "POST",
104
+ headers,
105
+ body,
106
+ signal: requestSignal(options)
107
+ });
108
+ await options?.onResponse?.({ status: response.status, headers: {} }, model);
109
+ if (!response.ok) {
110
+ const text = await response.text();
111
+ throw new Error(errorText(text, response.status));
112
+ }
113
+ return response;
114
+ }
115
+ function speakGoogle(model, input, options) {
116
+ const stream = new SpeechEventStream();
117
+ void pumpSpeech(model, input, options, stream);
118
+ return stream;
119
+ }
120
+ async function pumpSpeech(model, input, options, stream) {
121
+ try {
122
+ if (options?.signal?.aborted) throw new DOMException("aborted", "AbortError");
123
+ const format = resolveFormat(model, input.format);
124
+ const text = await readText(input.text, options?.signal);
125
+ await postSpeech(model, text, input.voice, format, options, stream);
126
+ } catch (error) {
127
+ const voice = asVoiceError(error);
128
+ stream.push({ type: "error", reason: voice.reason, error: voice.error });
129
+ }
130
+ }
131
+ async function postSpeech(model, text, voice, format, options, stream) {
132
+ const payload = speechBody(text, voice, format);
133
+ const body = await applyPayload(payload, model, options);
134
+ if (!isRecord(body) || Array.isArray(body)) throw new Error("Speech payload was not JSON");
135
+ const response = await googleFetch(
136
+ model,
137
+ options,
138
+ endpoint(model.baseUrl, `models/${encodeURIComponent(model.id)}:generateContent`),
139
+ JSON.stringify(body)
140
+ );
141
+ const parsed = await readJson(response);
142
+ if (!isRecord(parsed) || Array.isArray(parsed)) throw new Error("Speech response had no audio");
143
+ const encoded = audioParts(parsed);
144
+ if (encoded.length === 0) throw new Error("Speech response had no audio");
145
+ const data = concat(encoded.map((item) => decodeAudioBase64(item)));
146
+ const seconds = chunkSeconds({ data, format, timestamp: 0 });
147
+ const meters = { characters: text.length };
148
+ if (seconds > 0) meters.outputSeconds = seconds;
149
+ stream.push({ type: "start", format });
150
+ stream.push({
151
+ type: "audio",
152
+ chunk: { data, format, timestamp: 0 },
153
+ span: { itemId: model.id, start: 0, end: text.length }
154
+ });
155
+ stream.push({ type: "done", usage: priceAudio(model.audioCost, meters) });
156
+ }
157
+ export {
158
+ speakGoogle
159
+ };
160
+ //# sourceMappingURL=speak.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/google/speak.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\nimport { chunkSeconds } from '../audio.ts';\nimport { decodeAudioBase64 } from '../convert.ts';\nimport type { SpeechModel } from '../models.ts';\nimport {\n applyPayload,\n asVoiceError,\n endpoint,\n isRecord,\n readJson,\n requestSignal,\n stringField,\n} from '../openai/wire.ts';\nimport { SpeechEventStream, type SpeechInput } from '../speech.ts';\nimport { type AudioCounters, priceAudio } from '../usage.ts';\nimport { googleHeaders, sameFormat, speechMime } 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\nfunction speechBody(text: string, voice: string, format: AudioFormat): Record<string, unknown> {\n return {\n contents: [{ role: 'user', parts: [{ text }] }],\n generationConfig: {\n responseModalities: ['AUDIO'],\n responseFormat: { audio: { mimeType: speechMime(format) } },\n speechConfig: {\n voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } },\n },\n },\n };\n}\n\nasync function readText(\n text: string | AsyncIterable<string>,\n signal: AbortSignal | undefined\n): Promise<string> {\n if (typeof text === 'string') return text;\n let combined = '';\n for await (const part of text) {\n if (signal?.aborted) throw new DOMException('aborted', 'AbortError');\n combined += part;\n }\n return combined;\n}\n\nfunction errorText(text: string, status: number): string {\n if (text === '') return `HTTP ${status}`;\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return text;\n }\n if (!isRecord(parsed) || Array.isArray(parsed)) return text;\n const direct = stringField(parsed, 'message');\n if (direct !== undefined) return direct;\n const error = parsed.error;\n if (typeof error === 'string') return error;\n if (isRecord(error) && !Array.isArray(error)) {\n const nested = stringField(error, 'message');\n if (nested !== undefined) return nested;\n }\n return text;\n}\n\nfunction inlineData(part: Record<string, unknown>): string | undefined {\n const camel = part.inlineData;\n const snake = part.inline_data;\n const inline = isRecord(camel) && !Array.isArray(camel) ? camel : snake;\n if (!isRecord(inline) || Array.isArray(inline)) return undefined;\n return stringField(inline, 'data');\n}\n\nfunction audioParts(body: Record<string, unknown>): string[] {\n const candidates = body.candidates;\n if (!Array.isArray(candidates)) return [];\n const first = candidates[0];\n if (!isRecord(first) || Array.isArray(first)) return [];\n const content = first.content;\n if (!isRecord(content) || Array.isArray(content)) return [];\n const parts = content.parts;\n if (!Array.isArray(parts)) return [];\n const chunks: string[] = [];\n for (const part of parts) {\n if (!isRecord(part) || Array.isArray(part)) continue;\n const data = inlineData(part);\n if (data === undefined || data === '') continue;\n chunks.push(data);\n }\n return chunks;\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 googleFetch(\n model: SpeechModel<string>,\n options: ProviderRequestOptions | undefined,\n url: string,\n body: string\n): Promise<Response> {\n const headers = googleHeaders(model, options);\n headers['Content-Type'] = 'application/json';\n const fetchImpl = options?.fetch ?? globalThis.fetch;\n const response = await fetchImpl(url, {\n method: 'POST',\n headers,\n body,\n signal: requestSignal(options),\n });\n await options?.onResponse?.({ status: response.status, headers: {} }, model as never);\n if (!response.ok) {\n const text = await response.text();\n throw new Error(errorText(text, response.status));\n }\n return response;\n}\n\nexport function speakGoogle(\n model: SpeechModel<string>,\n input: SpeechInput,\n options?: ProviderRequestOptions\n): SpeechEventStream {\n const stream = new SpeechEventStream();\n void pumpSpeech(model, input, options, stream);\n return stream;\n}\n\nasync function pumpSpeech(\n model: SpeechModel<string>,\n input: SpeechInput,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n try {\n if (options?.signal?.aborted) throw new DOMException('aborted', 'AbortError');\n const format = resolveFormat(model, input.format);\n const text = await readText(input.text, options?.signal);\n await postSpeech(model, text, input.voice, format, options, stream);\n } catch (error) {\n const voice = asVoiceError(error);\n stream.push({ type: 'error', reason: voice.reason, error: voice.error });\n }\n}\n\nasync function postSpeech(\n model: SpeechModel<string>,\n text: string,\n voice: string,\n format: AudioFormat,\n options: ProviderRequestOptions | undefined,\n stream: SpeechEventStream\n) {\n const payload = speechBody(text, voice, format);\n const body = await applyPayload(payload, model, options);\n if (!isRecord(body) || Array.isArray(body)) throw new Error('Speech payload was not JSON');\n const response = await googleFetch(\n model,\n options,\n endpoint(model.baseUrl, `models/${encodeURIComponent(model.id)}:generateContent`),\n JSON.stringify(body)\n );\n const parsed = await readJson(response);\n if (!isRecord(parsed) || Array.isArray(parsed)) throw new Error('Speech response had no audio');\n const encoded = audioParts(parsed);\n if (encoded.length === 0) throw new Error('Speech response had no audio');\n const data = concat(encoded.map((item) => decodeAudioBase64(item)));\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"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,43 @@
1
+ const mimes = {
2
+ wav: "AUDIO_WAV",
3
+ pcm16: "AUDIO_L16",
4
+ mulaw: "AUDIO_MULAW",
5
+ alaw: "AUDIO_ALAW"
6
+ };
7
+ function speechMime(format) {
8
+ const name = mimes[format.encoding];
9
+ if (!name) throw new Error(`Unsupported speech format ${format.encoding}`);
10
+ return name;
11
+ }
12
+ function sameFormat(left, right) {
13
+ return left.encoding === right.encoding && left.sampleRate === right.sampleRate && left.channels === right.channels;
14
+ }
15
+ function headerValue(headers, name) {
16
+ const expected = name.toLowerCase();
17
+ for (const [key, value] of Object.entries(headers)) {
18
+ if (key.toLowerCase() === expected && value.trim() !== "") return value;
19
+ }
20
+ return void 0;
21
+ }
22
+ function googleHeaders(model, options) {
23
+ const headers = { ...model.headers };
24
+ const extra = options?.headers;
25
+ if (extra) {
26
+ for (const [key, value] of Object.entries(extra)) {
27
+ if (value === null) delete headers[key];
28
+ else headers[key] = value;
29
+ }
30
+ }
31
+ if (headerValue(headers, "x-goog-api-key") === void 0) {
32
+ const apiKey = options?.apiKey;
33
+ if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
34
+ headers["x-goog-api-key"] = apiKey;
35
+ }
36
+ return headers;
37
+ }
38
+ export {
39
+ googleHeaders,
40
+ sameFormat,
41
+ speechMime
42
+ };
43
+ //# sourceMappingURL=wire.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/google/wire.ts"],
4
+ "sourcesContent": ["import type { ProviderRequestOptions } from '@earendil-works/pi-ai';\nimport type { AudioFormat } from '../audio.ts';\n\nconst mimes: Record<string, string> = {\n wav: 'AUDIO_WAV',\n pcm16: 'AUDIO_L16',\n mulaw: 'AUDIO_MULAW',\n alaw: 'AUDIO_ALAW',\n};\n\nexport function speechMime(format: AudioFormat): string {\n const name = mimes[format.encoding];\n if (!name) throw new Error(`Unsupported speech format ${format.encoding}`);\n return name;\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\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\nexport function googleHeaders(\n model: { provider: string; headers?: Record<string, string> },\n options: ProviderRequestOptions | undefined\n): Record<string, string> {\n const headers: Record<string, string> = { ...model.headers };\n const extra = options?.headers;\n if (extra) {\n for (const [key, value] of Object.entries(extra)) {\n if (value === null) delete headers[key];\n else headers[key] = value;\n }\n }\n if (headerValue(headers, 'x-goog-api-key') === undefined) {\n const apiKey = options?.apiKey;\n if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);\n headers['x-goog-api-key'] = apiKey;\n }\n return headers;\n}\n"],
5
+ "mappings": "AAGA,MAAM,QAAgC;AAAA,EACpC,KAAK;AAAA,EACL,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AACR;AAEO,SAAS,WAAW,QAA6B;AACtD,QAAM,OAAO,MAAM,OAAO,QAAQ;AAClC,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,6BAA6B,OAAO,QAAQ,EAAE;AACzE,SAAO;AACT;AAEO,SAAS,WAAW,MAAmB,OAA6B;AACzE,SACE,KAAK,aAAa,MAAM,YACxB,KAAK,eAAe,MAAM,cAC1B,KAAK,aAAa,MAAM;AAE5B;AAEA,SAAS,YAAY,SAAiC,MAAkC;AACtF,QAAM,WAAW,KAAK,YAAY;AAClC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,QAAI,IAAI,YAAY,MAAM,YAAY,MAAM,KAAK,MAAM,GAAI,QAAO;AAAA,EACpE;AACA,SAAO;AACT;AAEO,SAAS,cACd,OACA,SACwB;AACxB,QAAM,UAAkC,EAAE,GAAG,MAAM,QAAQ;AAC3D,QAAM,QAAQ,SAAS;AACvB,MAAI,OAAO;AACT,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,UAAI,UAAU,KAAM,QAAO,QAAQ,GAAG;AAAA,UACjC,SAAQ,GAAG,IAAI;AAAA,IACtB;AAAA,EACF;AACA,MAAI,YAAY,SAAS,gBAAgB,MAAM,QAAW;AACxD,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,4BAA4B,MAAM,QAAQ,EAAE;AACzE,YAAQ,gBAAgB,IAAI;AAAA,EAC9B;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ export declare function registerGoogle(): () => void;
@@ -0,0 +1 @@
1
+ export { registerGoogle } from './catalog.ts';
@@ -0,0 +1,4 @@
1
+ import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import type { SpeechModel } from '../models.ts';
3
+ import { SpeechEventStream, type SpeechInput } from '../speech.ts';
4
+ export declare function speakGoogle(model: SpeechModel<string>, input: SpeechInput, options?: ProviderRequestOptions): SpeechEventStream;
@@ -0,0 +1,8 @@
1
+ import type { ProviderRequestOptions } from '@earendil-works/pi-ai';
2
+ import type { AudioFormat } from '../audio.ts';
3
+ export declare function speechMime(format: AudioFormat): string;
4
+ export declare function sameFormat(left: AudioFormat, right: AudioFormat): boolean;
5
+ export declare function googleHeaders(model: {
6
+ provider: string;
7
+ headers?: Record<string, string>;
8
+ }, options: ProviderRequestOptions | undefined): Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/pi-audio",
3
- "version": "1.19.0",
3
+ "version": "1.20.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",
@@ -58,6 +58,10 @@
58
58
  "types": "./dist/types/fish/index.d.ts",
59
59
  "default": "./dist/fish/index.js"
60
60
  },
61
+ "./google": {
62
+ "types": "./dist/types/google/index.d.ts",
63
+ "default": "./dist/google/index.js"
64
+ },
61
65
  "./durable": {
62
66
  "types": "./dist/types/durable/index.d.ts",
63
67
  "default": "./dist/durable/index.js"