@gobing-ai/knowledge-kit 0.0.21 → 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.
Files changed (71) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +161 -142
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -3
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -3
  6. package/plugins/generations/daily-article-gen/dist/index.js +20 -3
  7. package/plugins/generations/dailynews-gen/dist/index.js +20 -3
  8. package/plugins/generations/episode-plan-gen/dist/index.js +20 -3
  9. package/plugins/generations/gemini-tts/README.md +101 -0
  10. package/plugins/generations/gemini-tts/dist/index.js +23282 -0
  11. package/plugins/generations/gemini-tts/package.json +16 -0
  12. package/plugins/generations/gemini-tts/plugin.json +6 -0
  13. package/plugins/generations/gemini-tts/profiles.json +3 -0
  14. package/plugins/generations/gemini-tts/src/gemini-client.ts +190 -0
  15. package/plugins/generations/gemini-tts/src/index.ts +33 -0
  16. package/plugins/generations/gemini-tts/src/voice-create.ts +180 -0
  17. package/plugins/generations/gemini-tts/tsconfig.json +8 -0
  18. package/plugins/generations/image-gen/dist/index.js +147 -69
  19. package/plugins/generations/image-gen/src/presets.ts +2 -2
  20. package/plugins/generations/image-gen/src/providers/agnes.ts +6 -2
  21. package/plugins/generations/image-gen/src/providers/azure.ts +6 -2
  22. package/plugins/generations/image-gen/src/providers/dashscope.ts +10 -3
  23. package/plugins/generations/image-gen/src/providers/google.ts +70 -29
  24. package/plugins/generations/image-gen/src/providers/huggingface.ts +3 -0
  25. package/plugins/generations/image-gen/src/providers/jimeng.ts +9 -2
  26. package/plugins/generations/image-gen/src/providers/minimax.ts +10 -2
  27. package/plugins/generations/image-gen/src/providers/openai.ts +9 -3
  28. package/plugins/generations/image-gen/src/providers/openrouter.ts +16 -4
  29. package/plugins/generations/image-gen/src/providers/replicate.ts +13 -3
  30. package/plugins/generations/image-gen/src/providers/seedream.ts +4 -1
  31. package/plugins/generations/image-gen/src/providers/timeouts.ts +14 -0
  32. package/plugins/generations/image-gen/src/providers/types.ts +10 -0
  33. package/plugins/generations/image-gen/src/providers/zai.ts +4 -1
  34. package/plugins/generations/news-report-gen/dist/index.js +20 -3
  35. package/plugins/generations/voice-gen/dist/index.js +38 -12
  36. package/plugins/generations/voice-gen/package.json +3 -0
  37. package/plugins/generations/voice-gen/src/index.ts +2 -1
  38. package/plugins/generations/voice-gen/src/voicebox-client.ts +20 -5
  39. package/plugins/generations/voice-gen/src/voicebox-server.ts +2 -1
  40. package/plugins/ingestions/aihot-ingest/dist/index.js +25 -5
  41. package/plugins/ingestions/aihot-ingest/src/client.ts +5 -0
  42. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -3
  43. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -3
  44. package/plugins/ingestions/web-search/dist/index.js +22 -3
  45. package/plugins/ingestions/web-search/src/index.ts +4 -0
  46. package/plugins/kk/plugin.json +1 -1
  47. package/plugins/kk/scripts/kk-workflow-stages.ts +40 -8
  48. package/plugins/kk/scripts/render-md.ts +7 -1
  49. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +2 -2
  50. package/plugins/publishings/emdash-pub/dist/index.js +76 -19
  51. package/plugins/publishings/emdash-pub/src/index.ts +4 -0
  52. package/plugins/publishings/infoq-pub/dist/index.js +74 -21
  53. package/plugins/publishings/juejin-pub/dist/index.js +74 -21
  54. package/plugins/publishings/medium-pub/dist/index.js +77 -19
  55. package/plugins/publishings/medium-pub/src/index.ts +5 -0
  56. package/plugins/publishings/podcast-pub/dist/index.js +130 -34
  57. package/plugins/publishings/podcast-pub/src/index.ts +20 -5
  58. package/plugins/publishings/podcast-pub/src/map.ts +11 -0
  59. package/plugins/publishings/podcast-pub/src/show-notes.ts +66 -13
  60. package/plugins/publishings/qiita-pub/dist/index.js +75 -18
  61. package/plugins/publishings/qiita-pub/src/index.ts +4 -0
  62. package/plugins/publishings/substack-pub/dist/index.js +74 -21
  63. package/plugins/publishings/surfdash-pub/dist/index.js +99 -62
  64. package/plugins/publishings/surfdash-pub/src/index.ts +60 -19
  65. package/plugins/publishings/wechatmp-pub/dist/index.js +79 -22
  66. package/plugins/publishings/wechatmp-pub/src/index.ts +12 -3
  67. package/plugins/publishings/x-pub/dist/index.js +56 -37
  68. package/plugins/publishings/x-pub/src/index.ts +4 -0
  69. package/plugins/publishings/xhs-pub/dist/index.js +74 -21
  70. package/plugins/publishings/zenn-pub/dist/index.js +103 -25
  71. package/plugins/publishings/zenn-pub/src/index.ts +43 -9
@@ -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
+ }