@gobing-ai/knowledge-kit 0.0.22 → 0.0.23

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.
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@gobing-ai/gemini-tts",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "typecheck": "tsc --noEmit"
7
+ },
8
+ "dependencies": {
9
+ "@gobing-ai/ts-utils": "catalog:",
10
+ "@gobing-ai/voice-gen": "workspace:*"
11
+ },
12
+ "devDependencies": {
13
+ "@gobing-ai/kk-core": "workspace:*",
14
+ "@types/bun": "1.3.14"
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "gemini-tts",
3
+ "kind": "generator",
4
+ "entry": "./dist/index.js",
5
+ "version": "1.0.0"
6
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "robin-news": { "voice": "voice_REPLACE_ME" }
3
+ }
@@ -0,0 +1,190 @@
1
+ import { parseWav, type VoiceboxClient, type VoiceboxGenerateBody, type VoiceboxProfile } from '@gobing-ai/voice-gen';
2
+
3
+ export const GEMINI_TTS_MODELS = ['gemini-3.8-flash-tts', 'gemini-3.8-flash-lite-tts'] as const;
4
+ export const GEMINI_TTS_DEFAULT_MODEL = GEMINI_TTS_MODELS[0];
5
+ export const GEMINI_ASR_DEFAULT_MODEL = 'gemini-3.8-flash';
6
+ const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
7
+ const DEFAULT_PROFILES_PATH = new URL('../profiles.json', import.meta.url).pathname;
8
+ const TRANSCRIBE_PROMPT = 'Transcribe this audio verbatim. Output only the transcript, no commentary.';
9
+ const PLACEHOLDER_VOICE = 'voice_REPLACE_ME';
10
+
11
+ type Env = Record<string, string | undefined>;
12
+
13
+ export interface GeminiVoiceClientOptions {
14
+ env?: Env;
15
+ fetch?: typeof fetch;
16
+ sleep?: (ms: number) => Promise<void>;
17
+ }
18
+
19
+ interface InteractionContent {
20
+ type?: string;
21
+ data?: string;
22
+ text?: string;
23
+ }
24
+
25
+ interface InteractionResponse {
26
+ id?: string;
27
+ steps?: { type?: string; content?: InteractionContent[] }[];
28
+ error?: { message?: string };
29
+ }
30
+
31
+ /** First `model_output` content part of the given type (observed live shape, 2026-09-24). */
32
+ function findOutput(json: InteractionResponse, type: string): InteractionContent | undefined {
33
+ for (const step of json.steps ?? []) {
34
+ if (step.type !== 'model_output') continue;
35
+ const hit = step.content?.find((c) => c.type === type);
36
+ if (hit) return hit;
37
+ }
38
+ return undefined;
39
+ }
40
+
41
+ function positiveInt(raw: string | undefined, fallback: number): number {
42
+ const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
43
+ return Number.isFinite(n) && n > 0 ? n : fallback;
44
+ }
45
+
46
+ function excerpt(text: string): string {
47
+ return text.length > 30 ? `${text.slice(0, 30)}…` : text;
48
+ }
49
+
50
+ /**
51
+ * VoiceboxClient over the Gemini 3.8 TTS REST API, so voice-gen's `processGeneratorIO` pipeline
52
+ * (verify-retry, speed, concat, QC, MP3) runs unchanged against a hosted backend (ADR-023).
53
+ * Synthesis is unary: `generate` buffers the WAV and `waitUntilDone` completes immediately.
54
+ */
55
+ export function createGeminiVoiceClient(options: GeminiVoiceClientOptions = {}): VoiceboxClient {
56
+ const env = options.env ?? process.env;
57
+ const doFetch = options.fetch ?? fetch;
58
+ const sleep = options.sleep ?? ((ms: number) => Bun.sleep(ms));
59
+ const baseUrl = (env.GOOGLE_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
60
+ const model = env.GEMINI_TTS_MODEL || GEMINI_TTS_DEFAULT_MODEL;
61
+ const maxAttempts = positiveInt(env.GEMINI_TTS_MAX_ATTEMPTS, 4);
62
+ const timeoutMs = positiveInt(env.GEMINI_TTS_TIMEOUT_MS, 120_000);
63
+ const buffered = new Map<string, Uint8Array>();
64
+ let profiles: Record<string, { voice?: string }> | undefined;
65
+ let seq = 0;
66
+
67
+ async function post(body: unknown, label: string): Promise<InteractionResponse> {
68
+ const apiKey = env.GEMINI_API_KEY ?? '';
69
+ for (let attempt = 1; ; attempt++) {
70
+ let res: Response;
71
+ try {
72
+ res = await doFetch(`${baseUrl}/v1beta/interactions`, {
73
+ method: 'POST',
74
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
75
+ body: JSON.stringify(body),
76
+ signal: AbortSignal.timeout(timeoutMs),
77
+ });
78
+ } catch (err: unknown) {
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ if (attempt >= maxAttempts)
81
+ throw new Error(`${label}: request failed after ${attempt} attempts: ${msg}`);
82
+ await sleep(2000 * 2 ** (attempt - 1));
83
+ continue;
84
+ }
85
+ if (res.ok) return (await res.json()) as InteractionResponse;
86
+ const raw = await res.text();
87
+ let apiMessage = raw.slice(0, 300);
88
+ try {
89
+ apiMessage = (JSON.parse(raw) as InteractionResponse).error?.message ?? apiMessage;
90
+ } catch {
91
+ // non-JSON error body — keep the raw excerpt
92
+ }
93
+ const retryable = res.status === 429 || res.status >= 500;
94
+ if (!retryable || attempt >= maxAttempts) {
95
+ throw new Error(`${label}: HTTP ${res.status} after ${attempt} attempt(s): ${apiMessage}`);
96
+ }
97
+ const retryAfter = Number(res.headers.get('retry-after'));
98
+ await sleep(retryAfter > 0 ? retryAfter * 1000 : 2000 * 2 ** (attempt - 1));
99
+ }
100
+ }
101
+
102
+ async function loadProfiles(): Promise<Record<string, { voice?: string }>> {
103
+ if (profiles) return profiles;
104
+ const path = env.GEMINI_TTS_PROFILES || DEFAULT_PROFILES_PATH;
105
+ const file = Bun.file(path);
106
+ if (!(await file.exists())) throw new Error(`gemini-tts: profile map not found: ${path}`);
107
+ profiles = (await file.json()) as Record<string, { voice?: string }>;
108
+ return profiles;
109
+ }
110
+
111
+ const client: VoiceboxClient = {
112
+ url: baseUrl,
113
+
114
+ async health(): Promise<void> {
115
+ if (!env.GEMINI_API_KEY) throw new Error('gemini-tts: GEMINI_API_KEY is required');
116
+ if (!(GEMINI_TTS_MODELS as readonly string[]).includes(model)) {
117
+ throw new Error(
118
+ `gemini-tts: unsupported GEMINI_TTS_MODEL "${model}" (allowed: ${GEMINI_TTS_MODELS.join(', ')})`,
119
+ );
120
+ }
121
+ },
122
+
123
+ async resolveProfile(nameOrId: string): Promise<VoiceboxProfile> {
124
+ if (/^voice(key)?_/.test(nameOrId) && nameOrId !== PLACEHOLDER_VOICE) {
125
+ return { id: nameOrId, name: nameOrId };
126
+ }
127
+ const voice = (await loadProfiles())[nameOrId]?.voice;
128
+ if (!voice) throw new Error(`gemini-tts: unknown voice profile "${nameOrId}"`);
129
+ if (voice === PLACEHOLDER_VOICE) {
130
+ throw new Error(
131
+ `gemini-tts: profile "${nameOrId}" still holds the placeholder voice id — create the replicated voice (see plugins/generations/gemini-tts/README.md) and set it in GEMINI_TTS_PROFILES`,
132
+ );
133
+ }
134
+ return { id: voice, name: nameOrId };
135
+ },
136
+
137
+ async generate(body: VoiceboxGenerateBody): Promise<{ id: string }> {
138
+ const part: Record<string, unknown> = { type: 'text', text: body.text };
139
+ if (body.instruct) part.annotations = [{ type: 'speech_metadata', style: body.instruct }];
140
+ const json = await post(
141
+ {
142
+ model,
143
+ input: [{ type: 'user_input', content: [part] }],
144
+ response_format: { type: 'audio', mime_type: 'audio/wav', sample_rate: 24000 },
145
+ generation_config: { speech_config: [{ voice: body.profile_id }] },
146
+ },
147
+ `gemini-tts generate ("${excerpt(body.text)}")`,
148
+ );
149
+ const audio = findOutput(json, 'audio');
150
+ if (!audio?.data) throw new Error(`gemini-tts generate ("${excerpt(body.text)}"): response had no audio`);
151
+ seq += 1;
152
+ const id = `${json.id ?? 'gemini'}#${seq}`;
153
+ buffered.set(id, new Uint8Array(Buffer.from(audio.data, 'base64')));
154
+ return { id };
155
+ },
156
+
157
+ async waitUntilDone(id: string) {
158
+ const bytes = buffered.get(id);
159
+ if (!bytes) return { status: 'failed' as const, error: `unknown generation id ${id}` };
160
+ const wav = parseWav(bytes);
161
+ return { status: 'completed' as const, duration: wav.dataBytes.length / wav.byteRate };
162
+ },
163
+
164
+ async downloadAudio(id: string): Promise<Uint8Array> {
165
+ const bytes = buffered.get(id);
166
+ if (!bytes) throw new Error(`gemini-tts: unknown generation id ${id}`);
167
+ buffered.delete(id);
168
+ return bytes;
169
+ },
170
+ };
171
+
172
+ if (env.GEMINI_TTS_ASR !== 'off') {
173
+ const asrModel = env.GEMINI_TTS_ASR_MODEL || GEMINI_ASR_DEFAULT_MODEL;
174
+ client.transcribe = async (audio: Uint8Array) => {
175
+ const json = await post(
176
+ {
177
+ model: asrModel,
178
+ input: [
179
+ { type: 'text', text: TRANSCRIBE_PROMPT },
180
+ { type: 'audio', data: Buffer.from(audio).toString('base64'), mime_type: 'audio/wav' },
181
+ ],
182
+ },
183
+ 'gemini-tts transcribe',
184
+ );
185
+ return { text: findOutput(json, 'text')?.text?.trim() ?? '' };
186
+ };
187
+ }
188
+
189
+ return client;
190
+ }
@@ -0,0 +1,33 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { echoError } from '@gobing-ai/ts-utils';
3
+ import { processGeneratorIO } from '@gobing-ai/voice-gen';
4
+ import { createGeminiVoiceClient } from './gemini-client';
5
+
6
+ export * from './gemini-client';
7
+
8
+ export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
9
+ const { values } = parseArgs({
10
+ args: argv,
11
+ options: {
12
+ in: { type: 'string' },
13
+ out: { type: 'string' },
14
+ },
15
+ });
16
+
17
+ if (!values.in || !values.out) {
18
+ echoError('gemini-tts failed: Missing required arguments: --in and --out');
19
+ return 1;
20
+ }
21
+
22
+ try {
23
+ await processGeneratorIO(values.in, values.out, createGeminiVoiceClient(), undefined, 'kk:gemini-tts');
24
+ return 0;
25
+ } catch (err: unknown) {
26
+ echoError(`gemini-tts failed: ${err instanceof Error ? err.message : String(err)}`);
27
+ return 1;
28
+ }
29
+ }
30
+
31
+ if (import.meta.main) {
32
+ process.exit(await main());
33
+ }
@@ -0,0 +1,180 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { echo, echoError } from '@gobing-ai/ts-utils';
3
+ import { parseWav } from '@gobing-ai/voice-gen';
4
+ import { GEMINI_TTS_DEFAULT_MODEL } from './gemini-client';
5
+
6
+ const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
7
+ const VOICE_ID = /^(?:voice_|voicekey_)/;
8
+
9
+ export interface CreateReplicatedVoiceOptions {
10
+ source: Uint8Array;
11
+ consent: Uint8Array;
12
+ name: string;
13
+ store: boolean;
14
+ model: string;
15
+ env?: Record<string, string | undefined>;
16
+ fetch?: typeof fetch;
17
+ }
18
+
19
+ /**
20
+ * Reject a clip that is not 24 kHz mono 16-bit PCM WAV. Source clips must also be 10–30 s.
21
+ * The error names the measured rate, channels, bits, and duration.
22
+ */
23
+ export function validateVoiceWav(bytes: Uint8Array, role: 'source' | 'consent'): void {
24
+ let wav: ReturnType<typeof parseWav>;
25
+ try {
26
+ wav = parseWav(bytes);
27
+ } catch (err: unknown) {
28
+ const detail = err instanceof Error ? err.message : String(err);
29
+ throw new Error(
30
+ `gemini-tts voice-create: ${role} is not a RIFF WAV (${detail}); need 24000 Hz, 1 ch, 16-bit PCM`,
31
+ );
32
+ }
33
+ const duration = wav.byteRate > 0 ? wav.dataBytes.length / wav.byteRate : 0;
34
+ const problems: string[] = [];
35
+ if (wav.audioFormat !== 1) problems.push(`audioFormat ${wav.audioFormat} (need PCM 1)`);
36
+ if (wav.sampleRate !== 24000) problems.push(`${wav.sampleRate} Hz (need 24000)`);
37
+ if (wav.numChannels !== 1) problems.push(`${wav.numChannels} ch (need 1)`);
38
+ if (wav.bitsPerSample !== 16) problems.push(`${wav.bitsPerSample}-bit (need 16)`);
39
+ if (role === 'source' && (duration < 10 || duration > 30)) {
40
+ problems.push(`duration ${duration.toFixed(2)}s (need 10–30s)`);
41
+ }
42
+ if (problems.length > 0) {
43
+ throw new Error(
44
+ `gemini-tts voice-create: ${role} is ${wav.sampleRate} Hz, ${wav.numChannels} ch, ${wav.bitsPerSample}-bit, ${duration.toFixed(2)}s — ${problems.join('; ')}`,
45
+ );
46
+ }
47
+ }
48
+
49
+ function pickVoiceId(json: Record<string, unknown>): string | undefined {
50
+ const candidates: unknown[] = [json.name, json.id, json.key];
51
+ const nested = json.voice;
52
+ if (nested && typeof nested === 'object') {
53
+ const voice = nested as Record<string, unknown>;
54
+ candidates.push(voice.name, voice.id, voice.key);
55
+ }
56
+ for (const candidate of candidates) {
57
+ if (typeof candidate === 'string' && VOICE_ID.test(candidate)) return candidate;
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ function redact(text: string, secret: string): string {
63
+ return secret ? text.split(secret).join('[redacted]') : text;
64
+ }
65
+
66
+ /** POST /v1beta/voices and return the stored or stateless voice id. Never includes the API key in errors. */
67
+ export async function createReplicatedVoice(opts: CreateReplicatedVoiceOptions): Promise<{ id: string }> {
68
+ const env = opts.env ?? process.env;
69
+ const apiKey = env.GEMINI_API_KEY ?? '';
70
+ if (!apiKey) throw new Error('gemini-tts voice-create: GEMINI_API_KEY is required');
71
+ const baseUrl = (env.GOOGLE_BASE_URL || DEFAULT_BASE_URL).replace(/\/+$/, '');
72
+ const doFetch = opts.fetch ?? fetch;
73
+ let res: Response;
74
+ try {
75
+ res = await doFetch(`${baseUrl}/v1beta/voices`, {
76
+ method: 'POST',
77
+ headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
78
+ body: JSON.stringify({
79
+ store: opts.store,
80
+ voice: {
81
+ model: opts.model,
82
+ type: 'replicated',
83
+ display_name: opts.name,
84
+ replicated: {
85
+ source_audio: { mime_type: 'audio/wav', data: Buffer.from(opts.source).toString('base64') },
86
+ consent_audio: { mime_type: 'audio/wav', data: Buffer.from(opts.consent).toString('base64') },
87
+ },
88
+ },
89
+ }),
90
+ signal: AbortSignal.timeout(120_000),
91
+ });
92
+ } catch (err: unknown) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ throw new Error(`gemini-tts voice-create: request failed: ${redact(msg, apiKey)}`);
95
+ }
96
+ const raw = await res.text();
97
+ if (!res.ok) {
98
+ let apiMessage = raw.slice(0, 300);
99
+ try {
100
+ const parsed = JSON.parse(raw) as { error?: { message?: string } };
101
+ apiMessage = parsed.error?.message ?? apiMessage;
102
+ } catch {
103
+ // non-JSON error body — keep the raw excerpt
104
+ }
105
+ throw new Error(`gemini-tts voice-create: HTTP ${res.status}: ${redact(apiMessage, apiKey)}`);
106
+ }
107
+ let json: Record<string, unknown>;
108
+ try {
109
+ json = JSON.parse(raw) as Record<string, unknown>;
110
+ } catch {
111
+ throw new Error('gemini-tts voice-create: response was not JSON');
112
+ }
113
+ const id = pickVoiceId(json);
114
+ if (!id) {
115
+ throw new Error(`gemini-tts voice-create: response had no voice id (keys: ${Object.keys(json).join(', ')})`);
116
+ }
117
+ return { id };
118
+ }
119
+
120
+ async function readWav(path: string, role: 'source' | 'consent'): Promise<Uint8Array> {
121
+ const file = Bun.file(path);
122
+ if (!(await file.exists())) throw new Error(`gemini-tts voice-create: ${role} not found: ${path}`);
123
+ return new Uint8Array(await file.arrayBuffer());
124
+ }
125
+
126
+ export async function main(
127
+ argv: string[] = process.argv.slice(2),
128
+ deps: { env?: Record<string, string | undefined>; fetch?: typeof fetch } = {},
129
+ ): Promise<number> {
130
+ let values: { source?: string; consent?: string; name?: string; store?: string; model?: string };
131
+ try {
132
+ ({ values } = parseArgs({
133
+ args: argv,
134
+ options: {
135
+ source: { type: 'string' },
136
+ consent: { type: 'string' },
137
+ name: { type: 'string' },
138
+ store: { type: 'string' },
139
+ model: { type: 'string' },
140
+ },
141
+ }));
142
+ } catch (err: unknown) {
143
+ echoError(`gemini-tts voice-create: ${err instanceof Error ? err.message : String(err)}`);
144
+ return 1;
145
+ }
146
+ if (!values.source || !values.consent || !values.name) {
147
+ echoError('gemini-tts voice-create: missing required arguments: --source, --consent, and --name');
148
+ return 1;
149
+ }
150
+ const storeRaw = values.store ?? 'true';
151
+ if (storeRaw !== 'true' && storeRaw !== 'false') {
152
+ echoError(`gemini-tts voice-create: --store must be true or false (got "${storeRaw}")`);
153
+ return 1;
154
+ }
155
+ try {
156
+ const source = await readWav(values.source, 'source');
157
+ const consent = await readWav(values.consent, 'consent');
158
+ validateVoiceWav(source, 'source');
159
+ validateVoiceWav(consent, 'consent');
160
+ const env = deps.env ?? process.env;
161
+ const { id } = await createReplicatedVoice({
162
+ source,
163
+ consent,
164
+ name: values.name,
165
+ store: storeRaw === 'true',
166
+ model: values.model || env.GEMINI_TTS_MODEL || GEMINI_TTS_DEFAULT_MODEL,
167
+ env,
168
+ fetch: deps.fetch,
169
+ });
170
+ echo(id);
171
+ return 0;
172
+ } catch (err: unknown) {
173
+ echoError(err instanceof Error ? err.message : String(err));
174
+ return 1;
175
+ }
176
+ }
177
+
178
+ if (import.meta.main) {
179
+ process.exit(await main());
180
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../../tooling/typescript/base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "./src",
5
+ "outDir": "./dist"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
@@ -22896,7 +22896,7 @@ No documents provided for generation.`,
22896
22896
  format: "markdown",
22897
22897
  references: []
22898
22898
  };
22899
- async function processGeneratorIO(inputPath, outputPath, clientOverride, transcodeMp3) {
22899
+ async function processGeneratorIO(inputPath, outputPath, clientOverride, transcodeMp3, generator = "kk:voice-gen") {
22900
22900
  const fs = createNodeFileSystem();
22901
22901
  const outDir = dirname2(outputPath);
22902
22902
  const outStem = basename(outputPath, extname(outputPath));
@@ -23063,7 +23063,7 @@ async function processGeneratorIO(inputPath, outputPath, clientOverride, transco
23063
23063
  await fs.ensureDir(outDir);
23064
23064
  await Bun.write(audioPath, concatenatedWav);
23065
23065
  const metadata = {
23066
- generator: "kk:voice-gen",
23066
+ generator,
23067
23067
  audioPath,
23068
23068
  duration: totalDuration,
23069
23069
  segments: segmentMetadata
@@ -2,6 +2,9 @@
2
2
  "name": "@gobing-ai/voice-gen",
3
3
  "type": "module",
4
4
  "private": true,
5
+ "exports": {
6
+ ".": "./src/index.ts"
7
+ },
5
8
  "scripts": {
6
9
  "typecheck": "tsc --noEmit"
7
10
  },
@@ -43,6 +43,7 @@ export async function processGeneratorIO(
43
43
  outputPath: string,
44
44
  clientOverride?: VoiceboxClient,
45
45
  transcodeMp3?: Mp3Transcoder,
46
+ generator = 'kk:voice-gen',
46
47
  ): Promise<void> {
47
48
  const fs = createNodeFileSystem();
48
49
  const outDir = dirname(outputPath);
@@ -317,7 +318,7 @@ export async function processGeneratorIO(
317
318
  await Bun.write(audioPath, concatenatedWav);
318
319
 
319
320
  const metadata: Record<string, unknown> = {
320
- generator: 'kk:voice-gen',
321
+ generator,
321
322
  audioPath,
322
323
  duration: totalDuration,
323
324
  segments: segmentMetadata,
@@ -44,7 +44,8 @@ export interface VoiceboxClient {
44
44
  pollMs?: number,
45
45
  ): Promise<{ status: 'completed' | 'failed'; duration?: number; error?: string }>;
46
46
  downloadAudio(id: string): Promise<Uint8Array>;
47
- transcribe(audio: Uint8Array, language?: string): Promise<{ text: string; duration?: number }>;
47
+ /** Optional: backends without ASR omit it; callers skip fidelity/repetition checks. */
48
+ transcribe?(audio: Uint8Array, language?: string): Promise<{ text: string; duration?: number }>;
48
49
  }
49
50
 
50
51
  export interface VoiceboxClientOptions {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }
@@ -965,7 +965,11 @@ function dailyGenerate(args: string[]): void {
965
965
  const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '', vdriver = ''] = args;
966
966
  // 20260917 review: the workflow picks a voice driver by short name; both drivers' envs are
967
967
  // passed unconditionally so switching `vdriver` needs no shell changes. Unknown -> fail loud.
968
- const VOICE_DRIVERS: Record<string, string> = { ominivoice: 'omni-voice-gen', voicebox: 'voice-gen' };
968
+ const VOICE_DRIVERS: Record<string, string> = {
969
+ ominivoice: 'omni-voice-gen',
970
+ voicebox: 'voice-gen',
971
+ gemini: 'gemini-tts',
972
+ };
969
973
  const voiceGenerator = VOICE_DRIVERS[vdriver];
970
974
  if (!voiceGenerator) {
971
975
  failWith(
@@ -19,7 +19,7 @@ vars:
19
19
  limit: "10"
20
20
  language: "zh"
21
21
  voice_profile: "robin-news"
22
- # 20260917: voice driver switch — voicebox (default, voice-gen) | ominivoice (omni-voice-gen).
22
+ # 20260917: voice driver switch — voicebox (default, voice-gen) | ominivoice (omni-voice-gen) | gemini (gemini-tts).
23
23
  # 20260920: default flipped to voicebox — operator A/B at matched speech rate (~5.2 chars/s)
24
24
  # judged voicebox clearly better (ominivoice carries ~1min dead air over 16min + ASR-garble class).
25
25
  vdriver: "voicebox"
@@ -204,7 +204,7 @@ states:
204
204
  kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
205
205
 
206
206
  - id: generate
207
- description: "Invoke voice driver plugin (vdriver var: ominivoice -> omni-voice-gen, voicebox -> voice-gen) to generate audio"
207
+ description: "Invoke voice driver plugin (vdriver var: ominivoice -> omni-voice-gen, voicebox -> voice-gen, gemini -> gemini-tts) to generate audio"
208
208
  onEnter:
209
209
  - kind: shell
210
210
  options: