@moxxy/plugin-tts-gemini 0.0.0-stage → 0.42.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/LICENSE +21 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +59 -3
- package/src/gemini-tts.test.ts +172 -0
- package/src/index.ts +316 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moxxy (moxxy.ai)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Plugin, type SynthesizeOptions, type SynthesisResult, type Synthesizer, type SynthesizerCreateContext } from '@moxxy/sdk';
|
|
2
|
+
export declare const GEMINI_TTS_SYNTHESIZER_NAME = "gemini-tts";
|
|
3
|
+
export declare const GEMINI_TTS_MODEL = "gemini-3.8-flash-lite-tts";
|
|
4
|
+
export declare const GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
|
|
5
|
+
export declare const GEMINI_TTS_API_KEY_NAME = "GEMINI_API_KEY";
|
|
6
|
+
declare const DEFAULT_VOICE = "Fola";
|
|
7
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
8
|
+
export interface GeminiTtsOptions {
|
|
9
|
+
readonly apiKey?: string;
|
|
10
|
+
readonly getSecret?: (name: string) => Promise<string | null>;
|
|
11
|
+
readonly voice?: string;
|
|
12
|
+
readonly baseURL?: string;
|
|
13
|
+
readonly fetchImpl?: FetchLike;
|
|
14
|
+
}
|
|
15
|
+
export interface GeminiVoice {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly displayName: string;
|
|
18
|
+
readonly languageCode?: string;
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class GeminiTtsSynthesizer implements Synthesizer {
|
|
22
|
+
readonly name = "gemini-tts";
|
|
23
|
+
private readonly explicitKey;
|
|
24
|
+
private readonly getSecret;
|
|
25
|
+
private readonly defaultVoice;
|
|
26
|
+
private readonly baseURL;
|
|
27
|
+
private readonly fetchImpl;
|
|
28
|
+
constructor(options?: GeminiTtsOptions);
|
|
29
|
+
synthesize(text: string, opts?: SynthesizeOptions): Promise<SynthesisResult>;
|
|
30
|
+
private resolveKey;
|
|
31
|
+
}
|
|
32
|
+
export declare function listGeminiVoices(apiKey: string, options?: {
|
|
33
|
+
readonly baseURL?: string;
|
|
34
|
+
readonly fetchImpl?: FetchLike;
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
}): Promise<ReadonlyArray<GeminiVoice>>;
|
|
37
|
+
export declare function listGeminiVoicesWithSecret(getSecret: (name: string) => Promise<string | null>, options?: {
|
|
38
|
+
readonly fetchImpl?: FetchLike;
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
}): Promise<ReadonlyArray<GeminiVoice>>;
|
|
41
|
+
export declare function buildGeminiTtsPlugin(options?: {
|
|
42
|
+
readonly defaults?: GeminiTtsOptions;
|
|
43
|
+
}): Plugin;
|
|
44
|
+
export { DEFAULT_VOICE };
|
|
45
|
+
export { type SynthesizerCreateContext };
|
|
46
|
+
declare const _default: Plugin;
|
|
47
|
+
export default _default;
|
|
48
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,MAAM,EACX,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC9B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,2BAA2B,eAAe,CAAC;AACxD,eAAO,MAAM,gBAAgB,8BAA8B,CAAC;AAC5D,eAAO,MAAM,mBAAmB,qDAAqD,CAAC;AACtF,eAAO,MAAM,uBAAuB,mBAAmB,CAAC;AAExD,QAAA,MAAM,aAAa,SAAS,CAAC;AAI7B,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,qBAAa,oBAAqB,YAAW,WAAW;IACtD,QAAQ,CAAC,IAAI,gBAA+B;IAE5C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyD;IACnF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAE1B,OAAO,GAAE,gBAAqB;IAQpC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC;YAyExE,UAAU;CAYzB;AAED,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACzG,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAwDrC;AAED,wBAAsB,0BAA0B,CAC9C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,EACnD,OAAO,GAAE;IAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAC9E,OAAO,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAWrC;AA2CD,wBAAgB,oBAAoB,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAAO,GAAG,MAAM,CAiBnG;AAkCD,OAAO,EAAE,aAAa,EAAE,CAAC;AACzB,OAAO,EAAE,KAAK,wBAAwB,EAAE,CAAC;;AAEzC,wBAAsC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { classifyHttpStatus, classifyNetworkError, MoxxyError, definePlugin, defineSynthesizer, } from '@moxxy/sdk';
|
|
2
|
+
export const GEMINI_TTS_SYNTHESIZER_NAME = 'gemini-tts';
|
|
3
|
+
export const GEMINI_TTS_MODEL = 'gemini-3.8-flash-lite-tts';
|
|
4
|
+
export const GEMINI_API_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
5
|
+
export const GEMINI_TTS_API_KEY_NAME = 'GEMINI_API_KEY';
|
|
6
|
+
const DEFAULT_VOICE = 'Fola';
|
|
7
|
+
const VOICE_PAGE_SIZE = 1000;
|
|
8
|
+
const MAX_VOICE_PAGES = 20;
|
|
9
|
+
export class GeminiTtsSynthesizer {
|
|
10
|
+
name = GEMINI_TTS_SYNTHESIZER_NAME;
|
|
11
|
+
explicitKey;
|
|
12
|
+
getSecret;
|
|
13
|
+
defaultVoice;
|
|
14
|
+
baseURL;
|
|
15
|
+
fetchImpl;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.explicitKey = options.apiKey;
|
|
18
|
+
this.getSecret = options.getSecret;
|
|
19
|
+
this.defaultVoice = options.voice ?? DEFAULT_VOICE;
|
|
20
|
+
this.baseURL = trimTrailingSlashes(options.baseURL ?? GEMINI_API_BASE_URL);
|
|
21
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
22
|
+
}
|
|
23
|
+
async synthesize(text, opts = {}) {
|
|
24
|
+
opts.signal?.throwIfAborted();
|
|
25
|
+
const apiKey = await this.resolveKey();
|
|
26
|
+
opts.signal?.throwIfAborted();
|
|
27
|
+
const url = `${this.baseURL}/interactions`;
|
|
28
|
+
let response;
|
|
29
|
+
try {
|
|
30
|
+
response = await this.fetchImpl(url, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'content-type': 'application/json',
|
|
34
|
+
'x-goog-api-key': apiKey,
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
model: GEMINI_TTS_MODEL,
|
|
38
|
+
input: [{
|
|
39
|
+
type: 'user_input',
|
|
40
|
+
content: [{
|
|
41
|
+
type: 'text',
|
|
42
|
+
text,
|
|
43
|
+
annotations: [{ type: 'speech_metadata', style: speechStyle(opts.rate) }],
|
|
44
|
+
}],
|
|
45
|
+
}],
|
|
46
|
+
response_format: { type: 'audio' },
|
|
47
|
+
generation_config: {
|
|
48
|
+
speech_config: [{ voice: opts.voice ?? this.defaultVoice }],
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (opts.signal?.aborted)
|
|
56
|
+
throw opts.signal.reason ?? error;
|
|
57
|
+
const network = classifyNetworkError(error, { provider: 'gemini-tts', url });
|
|
58
|
+
if (network)
|
|
59
|
+
throw network;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const body = await response.text().catch(() => '');
|
|
64
|
+
const classified = classifyHttpStatus(response.status, {
|
|
65
|
+
provider: 'gemini-tts',
|
|
66
|
+
url,
|
|
67
|
+
body,
|
|
68
|
+
});
|
|
69
|
+
if (classified)
|
|
70
|
+
throw classified;
|
|
71
|
+
throw new MoxxyError({
|
|
72
|
+
code: 'PROVIDER_BAD_REQUEST',
|
|
73
|
+
message: `Gemini text-to-speech returned HTTP ${response.status}.`,
|
|
74
|
+
context: { provider: 'gemini-tts', url, status: response.status },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const payload = await response.json();
|
|
78
|
+
const audioPart = payload.steps
|
|
79
|
+
?.flatMap((step) => step.content ?? [])
|
|
80
|
+
.find((part) => part.type === 'audio' && part.data);
|
|
81
|
+
if (!audioPart?.data) {
|
|
82
|
+
throw new MoxxyError({
|
|
83
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
84
|
+
message: 'Gemini text-to-speech returned no audio data.',
|
|
85
|
+
context: { provider: 'gemini-tts', url },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const rawAudio = Buffer.from(audioPart.data, 'base64');
|
|
89
|
+
const encodedAudio = wrapLinearPcmAsWav(rawAudio, audioPart.mime_type || 'audio/wav');
|
|
90
|
+
return {
|
|
91
|
+
audio: new Uint8Array(encodedAudio.audio),
|
|
92
|
+
mimeType: encodedAudio.mimeType,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async resolveKey() {
|
|
96
|
+
const key = this.explicitKey ?? (await this.getSecret?.(GEMINI_TTS_API_KEY_NAME)) ?? null;
|
|
97
|
+
if (!key) {
|
|
98
|
+
throw new MoxxyError({
|
|
99
|
+
code: 'AUTH_NO_CREDENTIALS',
|
|
100
|
+
message: 'No Gemini API key is configured for cloud text-to-speech.',
|
|
101
|
+
hint: 'Add a Gemini API key in Settings → Voice. It is stored in the local vault.',
|
|
102
|
+
context: { provider: 'gemini-tts' },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return key;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function listGeminiVoices(apiKey, options = {}) {
|
|
109
|
+
const baseURL = trimTrailingSlashes(options.baseURL ?? GEMINI_API_BASE_URL);
|
|
110
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
111
|
+
const voices = [];
|
|
112
|
+
let pageToken;
|
|
113
|
+
for (let page = 0; page < MAX_VOICE_PAGES; page += 1) {
|
|
114
|
+
options.signal?.throwIfAborted();
|
|
115
|
+
const query = new URLSearchParams({ page_size: String(VOICE_PAGE_SIZE) });
|
|
116
|
+
if (pageToken)
|
|
117
|
+
query.set('page_token', pageToken);
|
|
118
|
+
const url = `${baseURL}/voices?${query.toString()}`;
|
|
119
|
+
let response;
|
|
120
|
+
try {
|
|
121
|
+
response = await fetchImpl(url, {
|
|
122
|
+
headers: { 'x-goog-api-key': apiKey },
|
|
123
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (options.signal?.aborted)
|
|
128
|
+
throw options.signal.reason ?? error;
|
|
129
|
+
const network = classifyNetworkError(error, { provider: 'gemini-tts', url });
|
|
130
|
+
if (network)
|
|
131
|
+
throw network;
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const classified = classifyHttpStatus(response.status, {
|
|
136
|
+
provider: 'gemini-tts',
|
|
137
|
+
url,
|
|
138
|
+
body: await response.text().catch(() => ''),
|
|
139
|
+
});
|
|
140
|
+
if (classified)
|
|
141
|
+
throw classified;
|
|
142
|
+
throw new MoxxyError({
|
|
143
|
+
code: 'PROVIDER_BAD_REQUEST',
|
|
144
|
+
message: `Gemini voice library returned HTTP ${response.status}.`,
|
|
145
|
+
context: { provider: 'gemini-tts', url, status: response.status },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const payload = await response.json();
|
|
149
|
+
for (const voice of payload.voices ?? []) {
|
|
150
|
+
if (!voice.id)
|
|
151
|
+
continue;
|
|
152
|
+
voices.push({
|
|
153
|
+
id: voice.id,
|
|
154
|
+
displayName: voice.display_name || voice.id,
|
|
155
|
+
...(voice.language_code ? { languageCode: voice.language_code } : {}),
|
|
156
|
+
...(voice.description ? { description: voice.description } : {}),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
pageToken = payload.next_page_token;
|
|
160
|
+
if (!pageToken)
|
|
161
|
+
return voices;
|
|
162
|
+
}
|
|
163
|
+
throw new MoxxyError({
|
|
164
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
165
|
+
message: `Gemini voice library exceeded the ${MAX_VOICE_PAGES}-page safety limit.`,
|
|
166
|
+
context: { provider: 'gemini-tts' },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export async function listGeminiVoicesWithSecret(getSecret, options = {}) {
|
|
170
|
+
const apiKey = await getSecret(GEMINI_TTS_API_KEY_NAME);
|
|
171
|
+
if (!apiKey) {
|
|
172
|
+
throw new MoxxyError({
|
|
173
|
+
code: 'AUTH_NO_CREDENTIALS',
|
|
174
|
+
message: 'No Gemini API key is configured for cloud text-to-speech.',
|
|
175
|
+
hint: 'Add a Gemini API key in Settings → Voice. It is stored in the local vault.',
|
|
176
|
+
context: { provider: 'gemini-tts' },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return listGeminiVoices(apiKey, options);
|
|
180
|
+
}
|
|
181
|
+
/** Gemini returns raw PCM (`audio/L16;codec=pcm;rate=24000`) rather than a
|
|
182
|
+
* container. The browser audio element consumes WAV, so wrap supported PCM
|
|
183
|
+
* before crossing the IPC boundary. Already-containerized formats pass through. */
|
|
184
|
+
function wrapLinearPcmAsWav(audio, mimeType) {
|
|
185
|
+
if (/^audio\/(?:wav|x-wav)(?:;|$)/i.test(mimeType))
|
|
186
|
+
return { audio, mimeType };
|
|
187
|
+
const [type, ...params] = mimeType.split(';').map((part) => part.trim());
|
|
188
|
+
const format = type?.split('/')[1] ?? '';
|
|
189
|
+
const linear = /^L(8|16|24|32)$/i.exec(format);
|
|
190
|
+
if (!/^audio\/(?:L(?:8|16|24|32)|pcm)$/i.test(type ?? ''))
|
|
191
|
+
return { audio, mimeType };
|
|
192
|
+
const bitsPerSample = linear ? Number(linear[1]) : 16;
|
|
193
|
+
const rateParam = params.find((param) => param.toLowerCase().startsWith('rate='));
|
|
194
|
+
const parsedRate = rateParam ? Number(rateParam.slice(rateParam.indexOf('=') + 1)) : 24_000;
|
|
195
|
+
const sampleRate = Number.isInteger(parsedRate) && parsedRate > 0 ? parsedRate : 24_000;
|
|
196
|
+
const channels = 1;
|
|
197
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
198
|
+
const header = Buffer.alloc(44);
|
|
199
|
+
header.write('RIFF', 0);
|
|
200
|
+
header.writeUInt32LE(36 + audio.length, 4);
|
|
201
|
+
header.write('WAVE', 8);
|
|
202
|
+
header.write('fmt ', 12);
|
|
203
|
+
header.writeUInt32LE(16, 16);
|
|
204
|
+
header.writeUInt16LE(1, 20);
|
|
205
|
+
header.writeUInt16LE(channels, 22);
|
|
206
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
207
|
+
header.writeUInt32LE(sampleRate * blockAlign, 28);
|
|
208
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
209
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
210
|
+
header.write('data', 36);
|
|
211
|
+
header.writeUInt32LE(audio.length, 40);
|
|
212
|
+
return { audio: Buffer.concat([header, audio]), mimeType: 'audio/wav' };
|
|
213
|
+
}
|
|
214
|
+
function speechStyle(rate) {
|
|
215
|
+
if (rate !== undefined && rate >= 1.1)
|
|
216
|
+
return 'Natural, conversational delivery at a slightly brisk pace.';
|
|
217
|
+
if (rate !== undefined && rate <= 0.9)
|
|
218
|
+
return 'Natural, conversational delivery at a slightly slower pace.';
|
|
219
|
+
return 'Natural, conversational delivery.';
|
|
220
|
+
}
|
|
221
|
+
export function buildGeminiTtsPlugin(options = {}) {
|
|
222
|
+
const defaults = options.defaults ?? {};
|
|
223
|
+
return definePlugin({
|
|
224
|
+
name: '@moxxy/plugin-tts-gemini',
|
|
225
|
+
version: '0.0.0',
|
|
226
|
+
synthesizers: [
|
|
227
|
+
defineSynthesizer({
|
|
228
|
+
name: GEMINI_TTS_SYNTHESIZER_NAME,
|
|
229
|
+
displayName: 'Cloud (Gemini Flash-Lite)',
|
|
230
|
+
create: (ctx) => new GeminiTtsSynthesizer({
|
|
231
|
+
...defaults,
|
|
232
|
+
...optionsFromConfig(ctx.config),
|
|
233
|
+
...(ctx.getSecret ? { getSecret: ctx.getSecret } : {}),
|
|
234
|
+
}),
|
|
235
|
+
}),
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function optionsFromConfig(config) {
|
|
240
|
+
return typeof config.voice === 'string' && config.voice.length > 0
|
|
241
|
+
? { voice: config.voice }
|
|
242
|
+
: {};
|
|
243
|
+
}
|
|
244
|
+
function trimTrailingSlashes(value) {
|
|
245
|
+
let end = value.length;
|
|
246
|
+
while (end > 0 && value[end - 1] === '/')
|
|
247
|
+
end -= 1;
|
|
248
|
+
return value.slice(0, end);
|
|
249
|
+
}
|
|
250
|
+
export { DEFAULT_VOICE };
|
|
251
|
+
export {};
|
|
252
|
+
export default buildGeminiTtsPlugin();
|
|
253
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moxxy/plugin-tts-gemini",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.42.0",
|
|
4
|
+
"description": "Google Gemini Flash-Lite text-to-speech extension for moxxy.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"moxxy",
|
|
7
|
+
"agent",
|
|
8
|
+
"tts",
|
|
9
|
+
"text-to-speech",
|
|
10
|
+
"gemini",
|
|
11
|
+
"google"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://moxxy.ai",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/moxxy-ai/moxxy/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/moxxy-ai/moxxy.git",
|
|
20
|
+
"directory": "packages/plugin-tts-gemini"
|
|
21
|
+
},
|
|
22
|
+
"author": "Moxxy contributors",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"import": "./dist/index.js"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src"
|
|
39
|
+
],
|
|
40
|
+
"moxxy": {
|
|
41
|
+
"plugin": {
|
|
42
|
+
"entry": "./dist/index.js",
|
|
43
|
+
"kind": "synthesizer"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@moxxy/sdk": "0.42.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.20.5",
|
|
51
|
+
"typescript": "^6.0.3",
|
|
52
|
+
"vitest": "^5.0.3",
|
|
53
|
+
"@moxxy/vitest-preset": "0.0.0",
|
|
54
|
+
"@moxxy/tsconfig": "0.0.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.json",
|
|
58
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"clean": "rm -rf dist .turbo"
|
|
61
|
+
}
|
|
6
62
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
GeminiTtsSynthesizer,
|
|
4
|
+
buildGeminiTtsPlugin,
|
|
5
|
+
listGeminiVoices,
|
|
6
|
+
GEMINI_TTS_MODEL,
|
|
7
|
+
GEMINI_TTS_SYNTHESIZER_NAME,
|
|
8
|
+
type FetchLike,
|
|
9
|
+
} from './index.js';
|
|
10
|
+
|
|
11
|
+
interface CapturedCall {
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly init: RequestInit;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeFetch(body: unknown, status = 200): { fetchImpl: FetchLike; calls: CapturedCall[] } {
|
|
17
|
+
const calls: CapturedCall[] = [];
|
|
18
|
+
const fetchImpl: FetchLike = async (url, init) => {
|
|
19
|
+
calls.push({ url, init });
|
|
20
|
+
return new Response(JSON.stringify(body), {
|
|
21
|
+
status,
|
|
22
|
+
headers: { 'content-type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
return { fetchImpl, calls };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const audioResponse = {
|
|
29
|
+
steps: [{
|
|
30
|
+
type: 'model_output',
|
|
31
|
+
content: [{
|
|
32
|
+
type: 'audio',
|
|
33
|
+
mime_type: 'audio/wav',
|
|
34
|
+
data: Buffer.from('RIFFaudio').toString('base64'),
|
|
35
|
+
}],
|
|
36
|
+
}],
|
|
37
|
+
usage: {
|
|
38
|
+
total_input_tokens: 9,
|
|
39
|
+
total_output_tokens: 75,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function rawPcmResponse(seconds: number): unknown {
|
|
44
|
+
const pcm16 = Buffer.alloc(24_000 * 2 * seconds);
|
|
45
|
+
return {
|
|
46
|
+
steps: [{
|
|
47
|
+
type: 'model_output',
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'audio',
|
|
50
|
+
mime_type: 'audio/L16;codec=pcm;rate=24000',
|
|
51
|
+
data: pcm16.toString('base64'),
|
|
52
|
+
}],
|
|
53
|
+
}],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('GeminiTtsSynthesizer', () => {
|
|
58
|
+
it('requests one short text chunk through Interactions and returns the WAV', async () => {
|
|
59
|
+
const { fetchImpl, calls } = makeFetch(audioResponse);
|
|
60
|
+
const synth = new GeminiTtsSynthesizer({
|
|
61
|
+
apiKey: 'test-key',
|
|
62
|
+
fetchImpl,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await synth.synthesize('Pierwsze zdanie.', { voice: 'Fola', language: 'pl' });
|
|
66
|
+
|
|
67
|
+
expect(result.mimeType).toBe('audio/wav');
|
|
68
|
+
expect(Buffer.from(result.audio).toString()).toBe('RIFFaudio');
|
|
69
|
+
expect(result).not.toHaveProperty('usage');
|
|
70
|
+
expect(calls).toHaveLength(1);
|
|
71
|
+
expect(calls[0]?.url).toBe('https://generativelanguage.googleapis.com/v1beta/interactions');
|
|
72
|
+
expect((calls[0]?.init.headers as Record<string, string>)['x-goog-api-key']).toBe('test-key');
|
|
73
|
+
expect(JSON.parse(calls[0]?.init.body as string)).toMatchObject({
|
|
74
|
+
model: GEMINI_TTS_MODEL,
|
|
75
|
+
input: [{ type: 'user_input', content: [{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: 'Pierwsze zdanie.',
|
|
78
|
+
annotations: [{ type: 'speech_metadata', style: 'Natural, conversational delivery.' }],
|
|
79
|
+
}] }],
|
|
80
|
+
response_format: { type: 'audio' },
|
|
81
|
+
generation_config: { speech_config: [{ voice: 'Fola' }] },
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('wraps raw PCM from Google as WAV and returns audio only', async () => {
|
|
86
|
+
const { fetchImpl } = makeFetch(rawPcmResponse(1));
|
|
87
|
+
const synth = new GeminiTtsSynthesizer({ apiKey: 'test-key', fetchImpl });
|
|
88
|
+
|
|
89
|
+
const result = await synth.synthesize('A short sentence.');
|
|
90
|
+
|
|
91
|
+
expect(result.mimeType).toBe('audio/wav');
|
|
92
|
+
expect(Buffer.from(result.audio).toString('ascii', 0, 4)).toBe('RIFF');
|
|
93
|
+
expect(result).not.toHaveProperty('usage');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('resolves the API key lazily from the vault and honors cancellation', async () => {
|
|
97
|
+
const { fetchImpl, calls } = makeFetch(audioResponse);
|
|
98
|
+
const synth = new GeminiTtsSynthesizer({
|
|
99
|
+
getSecret: async (name) => name === 'GEMINI_API_KEY' ? 'vault-key' : null,
|
|
100
|
+
fetchImpl,
|
|
101
|
+
});
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
|
|
104
|
+
await synth.synthesize('Cześć.', { signal: controller.signal });
|
|
105
|
+
expect((calls[0]?.init.headers as Record<string, string>)['x-goog-api-key']).toBe('vault-key');
|
|
106
|
+
expect(calls[0]?.init.signal).toBe(controller.signal);
|
|
107
|
+
|
|
108
|
+
controller.abort(new Error('interrupted'));
|
|
109
|
+
await expect(synth.synthesize('Przerwane.', { signal: controller.signal })).rejects.toThrow('interrupted');
|
|
110
|
+
expect(calls).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('uses a rotated vault key on the next sentence without rebuilding the synthesizer', async () => {
|
|
114
|
+
let key = 'first-key';
|
|
115
|
+
const { fetchImpl, calls } = makeFetch(audioResponse);
|
|
116
|
+
const synth = new GeminiTtsSynthesizer({
|
|
117
|
+
getSecret: async () => key,
|
|
118
|
+
fetchImpl,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await synth.synthesize('Pierwsze zdanie.');
|
|
122
|
+
key = 'rotated-key';
|
|
123
|
+
await synth.synthesize('Następne zdanie.');
|
|
124
|
+
|
|
125
|
+
expect(calls.map(({ init }) => (init.headers as Record<string, string>)['x-goog-api-key']))
|
|
126
|
+
.toEqual(['first-key', 'rotated-key']);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('classifies missing credentials and provider errors', async () => {
|
|
130
|
+
const noKey = new GeminiTtsSynthesizer({ getSecret: async () => null, fetchImpl: makeFetch(audioResponse).fetchImpl });
|
|
131
|
+
await expect(noKey.synthesize('Hej.')).rejects.toMatchObject({ code: 'AUTH_NO_CREDENTIALS' });
|
|
132
|
+
|
|
133
|
+
const limited = new GeminiTtsSynthesizer({ apiKey: 'bad', fetchImpl: makeFetch({ error: { message: 'quota' } }, 429).fetchImpl });
|
|
134
|
+
await expect(limited.synthesize('Hej.')).rejects.toMatchObject({ code: 'PROVIDER_RATE_LIMITED' });
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('listGeminiVoices', () => {
|
|
139
|
+
it('loads all pages and returns ids with display metadata', async () => {
|
|
140
|
+
const calls: string[] = [];
|
|
141
|
+
const fetchImpl: FetchLike = async (url) => {
|
|
142
|
+
calls.push(url);
|
|
143
|
+
if (url.includes('page_token')) {
|
|
144
|
+
return Response.json({ voices: [{ id: 'voice_2', display_name: 'Warm Fola', language_code: 'pl-PL' }] });
|
|
145
|
+
}
|
|
146
|
+
return Response.json({
|
|
147
|
+
voices: [{ id: 'Fola', display_name: 'Fola', language_code: 'en-US' }],
|
|
148
|
+
next_page_token: 'next-page',
|
|
149
|
+
});
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const voices = await listGeminiVoices('test-key', { fetchImpl });
|
|
153
|
+
|
|
154
|
+
expect(voices).toEqual([
|
|
155
|
+
{ id: 'Fola', displayName: 'Fola', languageCode: 'en-US' },
|
|
156
|
+
{ id: 'voice_2', displayName: 'Warm Fola', languageCode: 'pl-PL' },
|
|
157
|
+
]);
|
|
158
|
+
expect(calls).toHaveLength(2);
|
|
159
|
+
expect(calls[1]).toContain('page_token=next-page');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('buildGeminiTtsPlugin', () => {
|
|
164
|
+
it('registers a single Gemini synthesizer with the selected default voice', () => {
|
|
165
|
+
const plugin = buildGeminiTtsPlugin();
|
|
166
|
+
expect(plugin.synthesizers).toHaveLength(1);
|
|
167
|
+
expect(plugin.synthesizers?.[0]?.name).toBe(GEMINI_TTS_SYNTHESIZER_NAME);
|
|
168
|
+
|
|
169
|
+
const synth = plugin.synthesizers?.[0]?.create({ config: { voice: 'Fola' } });
|
|
170
|
+
expect(synth.name).toBe(GEMINI_TTS_SYNTHESIZER_NAME);
|
|
171
|
+
});
|
|
172
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import {
|
|
2
|
+
classifyHttpStatus,
|
|
3
|
+
classifyNetworkError,
|
|
4
|
+
MoxxyError,
|
|
5
|
+
definePlugin,
|
|
6
|
+
defineSynthesizer,
|
|
7
|
+
type Plugin,
|
|
8
|
+
type SynthesizeOptions,
|
|
9
|
+
type SynthesisResult,
|
|
10
|
+
type Synthesizer,
|
|
11
|
+
type SynthesizerCreateContext,
|
|
12
|
+
} from '@moxxy/sdk';
|
|
13
|
+
|
|
14
|
+
export const GEMINI_TTS_SYNTHESIZER_NAME = 'gemini-tts';
|
|
15
|
+
export const GEMINI_TTS_MODEL = 'gemini-3.8-flash-lite-tts';
|
|
16
|
+
export const GEMINI_API_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
17
|
+
export const GEMINI_TTS_API_KEY_NAME = 'GEMINI_API_KEY';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_VOICE = 'Fola';
|
|
20
|
+
const VOICE_PAGE_SIZE = 1000;
|
|
21
|
+
const MAX_VOICE_PAGES = 20;
|
|
22
|
+
|
|
23
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
24
|
+
|
|
25
|
+
export interface GeminiTtsOptions {
|
|
26
|
+
readonly apiKey?: string;
|
|
27
|
+
readonly getSecret?: (name: string) => Promise<string | null>;
|
|
28
|
+
readonly voice?: string;
|
|
29
|
+
readonly baseURL?: string;
|
|
30
|
+
readonly fetchImpl?: FetchLike;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GeminiVoice {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly displayName: string;
|
|
36
|
+
readonly languageCode?: string;
|
|
37
|
+
readonly description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class GeminiTtsSynthesizer implements Synthesizer {
|
|
41
|
+
readonly name = GEMINI_TTS_SYNTHESIZER_NAME;
|
|
42
|
+
|
|
43
|
+
private readonly explicitKey: string | undefined;
|
|
44
|
+
private readonly getSecret: ((name: string) => Promise<string | null>) | undefined;
|
|
45
|
+
private readonly defaultVoice: string;
|
|
46
|
+
private readonly baseURL: string;
|
|
47
|
+
private readonly fetchImpl: FetchLike;
|
|
48
|
+
|
|
49
|
+
constructor(options: GeminiTtsOptions = {}) {
|
|
50
|
+
this.explicitKey = options.apiKey;
|
|
51
|
+
this.getSecret = options.getSecret;
|
|
52
|
+
this.defaultVoice = options.voice ?? DEFAULT_VOICE;
|
|
53
|
+
this.baseURL = trimTrailingSlashes(options.baseURL ?? GEMINI_API_BASE_URL);
|
|
54
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async synthesize(text: string, opts: SynthesizeOptions = {}): Promise<SynthesisResult> {
|
|
58
|
+
opts.signal?.throwIfAborted();
|
|
59
|
+
const apiKey = await this.resolveKey();
|
|
60
|
+
opts.signal?.throwIfAborted();
|
|
61
|
+
|
|
62
|
+
const url = `${this.baseURL}/interactions`;
|
|
63
|
+
let response: Response;
|
|
64
|
+
try {
|
|
65
|
+
response = await this.fetchImpl(url, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: {
|
|
68
|
+
'content-type': 'application/json',
|
|
69
|
+
'x-goog-api-key': apiKey,
|
|
70
|
+
},
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
model: GEMINI_TTS_MODEL,
|
|
73
|
+
input: [{
|
|
74
|
+
type: 'user_input',
|
|
75
|
+
content: [{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text,
|
|
78
|
+
annotations: [{ type: 'speech_metadata', style: speechStyle(opts.rate) }],
|
|
79
|
+
}],
|
|
80
|
+
}],
|
|
81
|
+
response_format: { type: 'audio' },
|
|
82
|
+
generation_config: {
|
|
83
|
+
speech_config: [{ voice: opts.voice ?? this.defaultVoice }],
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (opts.signal?.aborted) throw opts.signal.reason ?? error;
|
|
90
|
+
const network = classifyNetworkError(error, { provider: 'gemini-tts', url });
|
|
91
|
+
if (network) throw network;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const body = await response.text().catch(() => '');
|
|
97
|
+
const classified = classifyHttpStatus(response.status, {
|
|
98
|
+
provider: 'gemini-tts',
|
|
99
|
+
url,
|
|
100
|
+
body,
|
|
101
|
+
});
|
|
102
|
+
if (classified) throw classified;
|
|
103
|
+
throw new MoxxyError({
|
|
104
|
+
code: 'PROVIDER_BAD_REQUEST',
|
|
105
|
+
message: `Gemini text-to-speech returned HTTP ${response.status}.`,
|
|
106
|
+
context: { provider: 'gemini-tts', url, status: response.status },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const payload = await response.json() as InteractionsResponse;
|
|
111
|
+
const audioPart = payload.steps
|
|
112
|
+
?.flatMap((step) => step.content ?? [])
|
|
113
|
+
.find((part) => part.type === 'audio' && part.data);
|
|
114
|
+
if (!audioPart?.data) {
|
|
115
|
+
throw new MoxxyError({
|
|
116
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
117
|
+
message: 'Gemini text-to-speech returned no audio data.',
|
|
118
|
+
context: { provider: 'gemini-tts', url },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rawAudio = Buffer.from(audioPart.data, 'base64');
|
|
123
|
+
const encodedAudio = wrapLinearPcmAsWav(rawAudio, audioPart.mime_type || 'audio/wav');
|
|
124
|
+
return {
|
|
125
|
+
audio: new Uint8Array(encodedAudio.audio),
|
|
126
|
+
mimeType: encodedAudio.mimeType,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async resolveKey(): Promise<string> {
|
|
131
|
+
const key = this.explicitKey ?? (await this.getSecret?.(GEMINI_TTS_API_KEY_NAME)) ?? null;
|
|
132
|
+
if (!key) {
|
|
133
|
+
throw new MoxxyError({
|
|
134
|
+
code: 'AUTH_NO_CREDENTIALS',
|
|
135
|
+
message: 'No Gemini API key is configured for cloud text-to-speech.',
|
|
136
|
+
hint: 'Add a Gemini API key in Settings → Voice. It is stored in the local vault.',
|
|
137
|
+
context: { provider: 'gemini-tts' },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return key;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function listGeminiVoices(
|
|
145
|
+
apiKey: string,
|
|
146
|
+
options: { readonly baseURL?: string; readonly fetchImpl?: FetchLike; readonly signal?: AbortSignal } = {},
|
|
147
|
+
): Promise<ReadonlyArray<GeminiVoice>> {
|
|
148
|
+
const baseURL = trimTrailingSlashes(options.baseURL ?? GEMINI_API_BASE_URL);
|
|
149
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
150
|
+
const voices: GeminiVoice[] = [];
|
|
151
|
+
let pageToken: string | undefined;
|
|
152
|
+
|
|
153
|
+
for (let page = 0; page < MAX_VOICE_PAGES; page += 1) {
|
|
154
|
+
options.signal?.throwIfAborted();
|
|
155
|
+
const query = new URLSearchParams({ page_size: String(VOICE_PAGE_SIZE) });
|
|
156
|
+
if (pageToken) query.set('page_token', pageToken);
|
|
157
|
+
const url = `${baseURL}/voices?${query.toString()}`;
|
|
158
|
+
let response: Response;
|
|
159
|
+
try {
|
|
160
|
+
response = await fetchImpl(url, {
|
|
161
|
+
headers: { 'x-goog-api-key': apiKey },
|
|
162
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (options.signal?.aborted) throw options.signal.reason ?? error;
|
|
166
|
+
const network = classifyNetworkError(error, { provider: 'gemini-tts', url });
|
|
167
|
+
if (network) throw network;
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
const classified = classifyHttpStatus(response.status, {
|
|
172
|
+
provider: 'gemini-tts',
|
|
173
|
+
url,
|
|
174
|
+
body: await response.text().catch(() => ''),
|
|
175
|
+
});
|
|
176
|
+
if (classified) throw classified;
|
|
177
|
+
throw new MoxxyError({
|
|
178
|
+
code: 'PROVIDER_BAD_REQUEST',
|
|
179
|
+
message: `Gemini voice library returned HTTP ${response.status}.`,
|
|
180
|
+
context: { provider: 'gemini-tts', url, status: response.status },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const payload = await response.json() as VoicesListResponse;
|
|
185
|
+
for (const voice of payload.voices ?? []) {
|
|
186
|
+
if (!voice.id) continue;
|
|
187
|
+
voices.push({
|
|
188
|
+
id: voice.id,
|
|
189
|
+
displayName: voice.display_name || voice.id,
|
|
190
|
+
...(voice.language_code ? { languageCode: voice.language_code } : {}),
|
|
191
|
+
...(voice.description ? { description: voice.description } : {}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
pageToken = payload.next_page_token;
|
|
195
|
+
if (!pageToken) return voices;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
throw new MoxxyError({
|
|
199
|
+
code: 'PROVIDER_UNKNOWN_RESPONSE',
|
|
200
|
+
message: `Gemini voice library exceeded the ${MAX_VOICE_PAGES}-page safety limit.`,
|
|
201
|
+
context: { provider: 'gemini-tts' },
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function listGeminiVoicesWithSecret(
|
|
206
|
+
getSecret: (name: string) => Promise<string | null>,
|
|
207
|
+
options: { readonly fetchImpl?: FetchLike; readonly signal?: AbortSignal } = {},
|
|
208
|
+
): Promise<ReadonlyArray<GeminiVoice>> {
|
|
209
|
+
const apiKey = await getSecret(GEMINI_TTS_API_KEY_NAME);
|
|
210
|
+
if (!apiKey) {
|
|
211
|
+
throw new MoxxyError({
|
|
212
|
+
code: 'AUTH_NO_CREDENTIALS',
|
|
213
|
+
message: 'No Gemini API key is configured for cloud text-to-speech.',
|
|
214
|
+
hint: 'Add a Gemini API key in Settings → Voice. It is stored in the local vault.',
|
|
215
|
+
context: { provider: 'gemini-tts' },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return listGeminiVoices(apiKey, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Gemini returns raw PCM (`audio/L16;codec=pcm;rate=24000`) rather than a
|
|
222
|
+
* container. The browser audio element consumes WAV, so wrap supported PCM
|
|
223
|
+
* before crossing the IPC boundary. Already-containerized formats pass through. */
|
|
224
|
+
function wrapLinearPcmAsWav(
|
|
225
|
+
audio: Buffer,
|
|
226
|
+
mimeType: string,
|
|
227
|
+
): { readonly audio: Buffer; readonly mimeType: string } {
|
|
228
|
+
if (/^audio\/(?:wav|x-wav)(?:;|$)/i.test(mimeType)) return { audio, mimeType };
|
|
229
|
+
const [type, ...params] = mimeType.split(';').map((part) => part.trim());
|
|
230
|
+
const format = type?.split('/')[1] ?? '';
|
|
231
|
+
const linear = /^L(8|16|24|32)$/i.exec(format);
|
|
232
|
+
if (!/^audio\/(?:L(?:8|16|24|32)|pcm)$/i.test(type ?? '')) return { audio, mimeType };
|
|
233
|
+
const bitsPerSample = linear ? Number(linear[1]) : 16;
|
|
234
|
+
const rateParam = params.find((param) => param.toLowerCase().startsWith('rate='));
|
|
235
|
+
const parsedRate = rateParam ? Number(rateParam.slice(rateParam.indexOf('=') + 1)) : 24_000;
|
|
236
|
+
const sampleRate = Number.isInteger(parsedRate) && parsedRate > 0 ? parsedRate : 24_000;
|
|
237
|
+
const channels = 1;
|
|
238
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
239
|
+
const header = Buffer.alloc(44);
|
|
240
|
+
header.write('RIFF', 0);
|
|
241
|
+
header.writeUInt32LE(36 + audio.length, 4);
|
|
242
|
+
header.write('WAVE', 8);
|
|
243
|
+
header.write('fmt ', 12);
|
|
244
|
+
header.writeUInt32LE(16, 16);
|
|
245
|
+
header.writeUInt16LE(1, 20);
|
|
246
|
+
header.writeUInt16LE(channels, 22);
|
|
247
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
248
|
+
header.writeUInt32LE(sampleRate * blockAlign, 28);
|
|
249
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
250
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
251
|
+
header.write('data', 36);
|
|
252
|
+
header.writeUInt32LE(audio.length, 40);
|
|
253
|
+
return { audio: Buffer.concat([header, audio]), mimeType: 'audio/wav' };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function speechStyle(rate: number | undefined): string {
|
|
257
|
+
if (rate !== undefined && rate >= 1.1) return 'Natural, conversational delivery at a slightly brisk pace.';
|
|
258
|
+
if (rate !== undefined && rate <= 0.9) return 'Natural, conversational delivery at a slightly slower pace.';
|
|
259
|
+
return 'Natural, conversational delivery.';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function buildGeminiTtsPlugin(options: { readonly defaults?: GeminiTtsOptions } = {}): Plugin {
|
|
263
|
+
const defaults = options.defaults ?? {};
|
|
264
|
+
return definePlugin({
|
|
265
|
+
name: '@moxxy/plugin-tts-gemini',
|
|
266
|
+
version: '0.0.0',
|
|
267
|
+
synthesizers: [
|
|
268
|
+
defineSynthesizer({
|
|
269
|
+
name: GEMINI_TTS_SYNTHESIZER_NAME,
|
|
270
|
+
displayName: 'Cloud (Gemini Flash-Lite)',
|
|
271
|
+
create: (ctx) => new GeminiTtsSynthesizer({
|
|
272
|
+
...defaults,
|
|
273
|
+
...optionsFromConfig(ctx.config),
|
|
274
|
+
...(ctx.getSecret ? { getSecret: ctx.getSecret } : {}),
|
|
275
|
+
}),
|
|
276
|
+
}),
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function optionsFromConfig(config: Record<string, unknown>): Pick<GeminiTtsOptions, 'voice'> {
|
|
282
|
+
return typeof config.voice === 'string' && config.voice.length > 0
|
|
283
|
+
? { voice: config.voice }
|
|
284
|
+
: {};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function trimTrailingSlashes(value: string): string {
|
|
288
|
+
let end = value.length;
|
|
289
|
+
while (end > 0 && value[end - 1] === '/') end -= 1;
|
|
290
|
+
return value.slice(0, end);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
interface InteractionsResponse {
|
|
294
|
+
readonly steps?: ReadonlyArray<{
|
|
295
|
+
readonly content?: ReadonlyArray<{
|
|
296
|
+
readonly type?: string;
|
|
297
|
+
readonly data?: string;
|
|
298
|
+
readonly mime_type?: string;
|
|
299
|
+
}>;
|
|
300
|
+
}>;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface VoicesListResponse {
|
|
304
|
+
readonly voices?: ReadonlyArray<{
|
|
305
|
+
readonly id?: string;
|
|
306
|
+
readonly display_name?: string;
|
|
307
|
+
readonly language_code?: string;
|
|
308
|
+
readonly description?: string;
|
|
309
|
+
}>;
|
|
310
|
+
readonly next_page_token?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export { DEFAULT_VOICE };
|
|
314
|
+
export { type SynthesizerCreateContext };
|
|
315
|
+
|
|
316
|
+
export default buildGeminiTtsPlugin();
|
package/README.md
DELETED