careervivid 2.1.13 โ†’ 2.1.16

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.
@@ -1 +1 @@
1
- {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAO7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CAosBf"}
1
+ {"version":3,"file":"repl.d.ts","sourceRoot":"","sources":["../../../src/commands/agent/repl.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAS7G,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,GAAE,MAAM,GAAG,IAAW,QAwBtF;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,WAAW,GAAG,sBAAsB,GAAG,IAAI,EACnD,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,EAC9K,gBAAgB,EAAE,WAAW,EAC7B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,GAAG,EAAE,GACX,OAAO,CAAC,IAAI,CAAC,CA6uBf"}
@@ -4,10 +4,10 @@ import ora from "ora";
4
4
  import { isSafeCommand } from "../../agent/tools/coding.js";
5
5
  import { CareerVividProxyEngine } from "../../agent/CareerVividProxyEngine.js";
6
6
  import { CV_MODELS } from "./configurator.js";
7
- import { loadConfig, getGeminiKey, getProviderKey, setProviderKey } from "../../config.js";
7
+ import { loadConfig, getProviderKey, setProviderKey } from "../../config.js";
8
8
  import { auditLog, writeSessionSummary, SESSION_ID } from "../../agent/agentAuditLog.js";
9
9
  import { runShellEscape } from "../../lib/shell.js";
10
- import { isVoiceEnabled, setVoiceEnabled, setLastResponse, getLastResponse, speakText, stopPlayback } from "../../lib/tts.js";
10
+ import { isVoiceEnabled, setVoiceEnabled, setLastResponse, getLastResponse, speakText, stopPlayback, getCurrentVoice, setCurrentVoice, getCurrentTtsModel, setCurrentTtsModel, AVAILABLE_VOICES, AVAILABLE_TTS_MODELS } from "../../lib/tts.js";
11
11
  const { prompt } = pkg;
12
12
  export function printCreditStatus(remaining, limit = null) {
13
13
  if (remaining === null)
@@ -198,16 +198,60 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
198
198
  if (cmd === "voice") {
199
199
  if (arg === "on") {
200
200
  setVoiceEnabled(true);
201
- console.log(chalk.green("\n ๐Ÿ”Š Voice enabled. Agent responses will be spoken aloud.\n"));
201
+ console.log(chalk.green(`\n ๐Ÿ”Š Voice enabled (${getCurrentVoice()} ยท ${getCurrentTtsModel()}).\n`));
202
202
  }
203
203
  else if (arg === "off") {
204
204
  setVoiceEnabled(false);
205
205
  stopPlayback();
206
206
  console.log(chalk.yellow("\n ๐Ÿ”‡ Voice disabled.\n"));
207
207
  }
208
+ else if (arg === "list-voices" || arg === "voices") {
209
+ console.log(chalk.cyan("\n Available voices:"));
210
+ for (const v of AVAILABLE_VOICES) {
211
+ const active = v === getCurrentVoice() ? chalk.green(" โ† active") : "";
212
+ console.log(chalk.dim(` ${v}${active}`));
213
+ }
214
+ console.log(chalk.dim("\n Usage: /voice set-voice Puck\n"));
215
+ }
216
+ else if (arg.startsWith("set-voice ")) {
217
+ const name = arg.slice("set-voice ".length).trim();
218
+ const match = AVAILABLE_VOICES.find(v => v.toLowerCase() === name.toLowerCase());
219
+ if (!match) {
220
+ console.log(chalk.red(`\n Unknown voice: "${name}". Run /voice list-voices to see options.\n`));
221
+ }
222
+ else {
223
+ setCurrentVoice(match);
224
+ console.log(chalk.green(`\n ๐ŸŽต Voice set to ${chalk.bold(match)}.\n`));
225
+ }
226
+ }
227
+ else if (arg === "list-models" || arg === "models") {
228
+ console.log(chalk.cyan("\n Available TTS models:"));
229
+ for (const m of AVAILABLE_TTS_MODELS) {
230
+ const active = m === getCurrentTtsModel() ? chalk.green(" โ† active") : "";
231
+ console.log(chalk.dim(` ${m}${active}`));
232
+ }
233
+ console.log(chalk.dim("\n Usage: /voice set-model gemini-2.5-pro-preview-tts\n"));
234
+ }
235
+ else if (arg.startsWith("set-model ")) {
236
+ const name = arg.slice("set-model ".length).trim();
237
+ const match = AVAILABLE_TTS_MODELS.find(m => m === name);
238
+ if (!match) {
239
+ console.log(chalk.red(`\n Unknown model: "${name}". Run /voice list-models to see options.\n`));
240
+ }
241
+ else {
242
+ setCurrentTtsModel(match);
243
+ console.log(chalk.green(`\n โš™๏ธ TTS model set to ${chalk.bold(match)}.\n`));
244
+ }
245
+ }
208
246
  else {
209
247
  const status = isVoiceEnabled() ? chalk.green("on") : chalk.yellow("off");
210
- console.log(chalk.dim(`\n Voice is currently ${status}. Usage: /voice on or /voice off\n`));
248
+ console.log(chalk.dim(`\n Voice is ${status} ยท Voice: ${chalk.white(getCurrentVoice())} ยท Model: ${chalk.white(getCurrentTtsModel())}`));
249
+ console.log(chalk.dim("\n Commands:"));
250
+ console.log(chalk.dim(" /voice on | off"));
251
+ console.log(chalk.dim(" /voice set-voice <name> e.g. /voice set-voice Puck"));
252
+ console.log(chalk.dim(" /voice list-voices"));
253
+ console.log(chalk.dim(" /voice set-model <name> e.g. /voice set-model gemini-2.5-pro-preview-tts"));
254
+ console.log(chalk.dim(" /voice list-models\n"));
211
255
  }
212
256
  return ask();
213
257
  }
@@ -217,8 +261,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
217
261
  console.log(chalk.dim("\n Nothing to speak yet. Ask the agent something first.\n"));
218
262
  }
219
263
  else {
220
- const geminiKey = getGeminiKey() || process.env.GEMINI_API_KEY;
221
- speakText(last, geminiKey ?? undefined).catch(() => { });
264
+ speakText(last).catch(() => { });
222
265
  console.log(chalk.dim("\n ๐Ÿ”Š Speaking last response...\n"));
223
266
  }
224
267
  return ask();
@@ -525,8 +568,7 @@ export async function askLoop(engine, options, selectedProvider, selectedModel,
525
568
  if (responseAccumulator) {
526
569
  setLastResponse(responseAccumulator);
527
570
  if (isVoiceEnabled()) {
528
- const geminiKey = getGeminiKey() || process.env.GEMINI_API_KEY;
529
- speakText(responseAccumulator, geminiKey ?? undefined).catch(() => { });
571
+ speakText(responseAccumulator).catch(() => { });
530
572
  }
531
573
  }
532
574
  // โ”€โ”€ Clean turn separator after every AI reply โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
package/dist/lib/tts.d.ts CHANGED
@@ -1,26 +1,27 @@
1
1
  /**
2
2
  * tts.ts โ€” Text-to-Speech engine for the CareerVivid REPL
3
3
  *
4
- * Uses the Gemini API (TTS model) to synthesize speech from text,
5
- * writes the result to a temp WAV file, and plays it via the OS
6
- * audio player (afplay on macOS, aplay on Linux) without blocking
7
- * the main thread.
4
+ * Authenticates using the user's CareerVivid API key (cv_live_...) to fetch
5
+ * a short-lived Gemini key from the backend โ€” exactly like `cv interview`.
6
+ * No separate GEMINI_API_KEY required.
8
7
  *
9
8
  * Toggle: /voice on | /voice off
10
- * Speak: /speak (reads the last agent response)
9
+ * Replay: /speak
11
10
  */
11
+ export declare const AVAILABLE_VOICES: readonly ["Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Aoede", "Orbit", "Stellar", "Leda", "Orus"];
12
+ export declare const AVAILABLE_TTS_MODELS: readonly ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"];
12
13
  export declare function isVoiceEnabled(): boolean;
13
14
  export declare function setVoiceEnabled(on: boolean): void;
14
15
  export declare function setLastResponse(text: string): void;
15
16
  export declare function getLastResponse(): string;
16
- /**
17
- * Stops any currently playing audio immediately.
18
- */
17
+ export declare function getCurrentVoice(): string;
18
+ export declare function setCurrentVoice(v: string): void;
19
+ export declare function getCurrentTtsModel(): string;
20
+ export declare function setCurrentTtsModel(m: string): void;
19
21
  export declare function stopPlayback(): void;
20
22
  /**
21
- * Synthesizes `text` via Gemini TTS and plays it asynchronously.
22
- * Does nothing if voice is disabled. Silently ignores errors so the
23
- * REPL is never disrupted by TTS failures.
23
+ * Synthesizes `text` via Gemini TTS using the CareerVivid API key for auth.
24
+ * Non-blocking โ€” errors are silently swallowed so the REPL is never disrupted.
24
25
  */
25
- export declare function speakText(text: string, apiKey?: string): Promise<void>;
26
+ export declare function speakText(text: string, _unusedKey?: string): Promise<void>;
26
27
  //# sourceMappingURL=tts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../src/lib/tts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAaH,wBAAgB,cAAc,YAA2B;AACzD,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,QAAwB;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,QAA0B;AACtE,wBAAgB,eAAe,WAA2B;AAI1D;;GAEG;AACH,wBAAgB,YAAY,SAK3B;AAoED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../src/lib/tts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgBH,eAAO,MAAM,gBAAgB,sGAWnB,CAAC;AAEX,eAAO,MAAM,oBAAoB,yEAGvB,CAAC;AAYX,wBAAgB,cAAc,YAA2B;AACzD,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,QAAwB;AACnE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,QAA0B;AACtE,wBAAgB,eAAe,WAA2B;AAC1D,wBAAgB,eAAe,WAA2B;AAC1D,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,QAAuB;AAChE,wBAAgB,kBAAkB,WAA8B;AAChE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,QAA0B;AA8BtE,wBAAgB,YAAY,SAK3B;AA2DD;;;GAGG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDhF"}
package/dist/lib/tts.js CHANGED
@@ -1,41 +1,89 @@
1
1
  /**
2
2
  * tts.ts โ€” Text-to-Speech engine for the CareerVivid REPL
3
3
  *
4
- * Uses the Gemini API (TTS model) to synthesize speech from text,
5
- * writes the result to a temp WAV file, and plays it via the OS
6
- * audio player (afplay on macOS, aplay on Linux) without blocking
7
- * the main thread.
4
+ * Authenticates using the user's CareerVivid API key (cv_live_...) to fetch
5
+ * a short-lived Gemini key from the backend โ€” exactly like `cv interview`.
6
+ * No separate GEMINI_API_KEY required.
8
7
  *
9
8
  * Toggle: /voice on | /voice off
10
- * Speak: /speak (reads the last agent response)
9
+ * Replay: /speak
11
10
  */
12
11
  import { writeFileSync, unlinkSync } from "fs";
13
12
  import { spawn } from "child_process";
14
13
  import { tmpdir } from "os";
15
14
  import { join } from "path";
16
15
  import { GoogleGenAI, Modality } from "@google/genai";
16
+ import { getApiKey } from "../config.js";
17
+ // โ”€โ”€ Backend endpoint (same as interview token vend) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
+ const TTS_TOKEN_URL = process.env.CV_FUNCTIONS_URL
19
+ ? `${process.env.CV_FUNCTIONS_URL}/cliGetInterviewToken`
20
+ : "https://us-west1-jastalk-firebase.cloudfunctions.net/cliGetInterviewToken";
21
+ // โ”€โ”€ Available options โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
+ export const AVAILABLE_VOICES = [
23
+ "Zephyr", // Bright, energetic
24
+ "Puck", // Upbeat, playful
25
+ "Charon", // Informative, measured
26
+ "Kore", // Firm, confident
27
+ "Fenrir", // Excitable, dynamic
28
+ "Aoede", // Breezy, easy-going
29
+ "Orbit", // Friendly, relaxed
30
+ "Stellar", // Smooth, polished
31
+ "Leda", // Warm, natural
32
+ "Orus", // Confident, authoritative
33
+ ];
34
+ export const AVAILABLE_TTS_MODELS = [
35
+ "gemini-2.5-flash-preview-tts", // Fast, efficient (default)
36
+ "gemini-2.5-pro-preview-tts", // Higher quality, slower
37
+ ];
17
38
  // โ”€โ”€ State โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
39
  let voiceEnabled = false;
19
40
  let lastResponse = "";
20
41
  let playbackProcess = null;
42
+ let currentVoice = "Zephyr";
43
+ let currentTtsModel = "gemini-2.5-flash-preview-tts";
44
+ // Cache the Gemini key for the session so we don't hit the endpoint every turn
45
+ let cachedGeminiKey = null;
21
46
  export function isVoiceEnabled() { return voiceEnabled; }
22
47
  export function setVoiceEnabled(on) { voiceEnabled = on; }
23
48
  export function setLastResponse(text) { lastResponse = text; }
24
49
  export function getLastResponse() { return lastResponse; }
50
+ export function getCurrentVoice() { return currentVoice; }
51
+ export function setCurrentVoice(v) { currentVoice = v; }
52
+ export function getCurrentTtsModel() { return currentTtsModel; }
53
+ export function setCurrentTtsModel(m) { currentTtsModel = m; }
54
+ // โ”€โ”€ Gemini key via CV API key โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
55
+ async function fetchGeminiKey() {
56
+ if (cachedGeminiKey)
57
+ return cachedGeminiKey;
58
+ const apiKey = getApiKey();
59
+ if (!apiKey)
60
+ return null;
61
+ try {
62
+ const res = await fetch(TTS_TOKEN_URL, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({ apiKey, role: "tts" }),
66
+ });
67
+ if (!res.ok)
68
+ return null;
69
+ const data = await res.json();
70
+ if (data?.geminiKey) {
71
+ cachedGeminiKey = data.geminiKey;
72
+ return cachedGeminiKey;
73
+ }
74
+ }
75
+ catch {
76
+ // Network error โ€” fall through
77
+ }
78
+ return null;
79
+ }
25
80
  // โ”€โ”€ Audio Playback โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
- /**
27
- * Stops any currently playing audio immediately.
28
- */
29
81
  export function stopPlayback() {
30
82
  if (playbackProcess && !playbackProcess.killed) {
31
83
  playbackProcess.kill("SIGKILL");
32
84
  playbackProcess = null;
33
85
  }
34
86
  }
35
- /**
36
- * Plays a WAV buffer using the OS audio player (non-blocking).
37
- * afplay on macOS, aplay on Linux, powershell on Windows.
38
- */
39
87
  function playWav(wavBuffer) {
40
88
  const tmpFile = join(tmpdir(), `cv-tts-${Date.now()}.wav`);
41
89
  writeFileSync(tmpFile, wavBuffer);
@@ -51,30 +99,28 @@ function playWav(wavBuffer) {
51
99
  playerArgs = ["-q", tmpFile];
52
100
  }
53
101
  else {
54
- // Windows fallback via PowerShell
55
102
  playerCmd = "powershell";
56
103
  playerArgs = ["-c", `(New-Object System.Media.SoundPlayer '${tmpFile}').PlaySync()`];
57
104
  }
58
- stopPlayback(); // Stop any previous playback
105
+ stopPlayback();
59
106
  const child = spawn(playerCmd, playerArgs, { stdio: "ignore", detached: false });
60
107
  playbackProcess = child;
61
108
  child.on("close", () => {
62
109
  try {
63
110
  unlinkSync(tmpFile);
64
111
  }
65
- catch { /* ignore cleanup errors */ }
112
+ catch { /* ignore */ }
66
113
  if (playbackProcess === child)
67
114
  playbackProcess = null;
68
115
  });
69
116
  child.on("error", () => {
70
- // Silently ignore โ€” player may not be installed
71
117
  try {
72
118
  unlinkSync(tmpFile);
73
119
  }
74
120
  catch { /* ignore */ }
75
121
  });
76
122
  }
77
- // โ”€โ”€ WAV Header Builder โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
123
+ // โ”€โ”€ WAV Builder โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
78
124
  function buildWavHeader(dataLength, sampleRate = 24000, channels = 1, bitsPerSample = 16) {
79
125
  const byteRate = sampleRate * channels * bitsPerSample / 8;
80
126
  const blockAlign = channels * bitsPerSample / 8;
@@ -94,40 +140,39 @@ function buildWavHeader(dataLength, sampleRate = 24000, channels = 1, bitsPerSam
94
140
  header.writeUInt32LE(dataLength, 40);
95
141
  return header;
96
142
  }
97
- // โ”€โ”€ TTS Synthesis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
143
+ // โ”€โ”€ TTS Synthesis โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
98
144
  /**
99
- * Synthesizes `text` via Gemini TTS and plays it asynchronously.
100
- * Does nothing if voice is disabled. Silently ignores errors so the
101
- * REPL is never disrupted by TTS failures.
145
+ * Synthesizes `text` via Gemini TTS using the CareerVivid API key for auth.
146
+ * Non-blocking โ€” errors are silently swallowed so the REPL is never disrupted.
102
147
  */
103
- export async function speakText(text, apiKey) {
148
+ export async function speakText(text, _unusedKey) {
104
149
  if (!text.trim())
105
150
  return;
106
- const key = apiKey || process.env.GEMINI_API_KEY;
107
- if (!key)
108
- return; // No key โ€” silently skip
109
- // Strip markdown so the audio sounds natural
151
+ const geminiKey = await fetchGeminiKey();
152
+ if (!geminiKey)
153
+ return; // No key available โ€” silently skip
154
+ // Strip markdown for natural-sounding speech
110
155
  const cleaned = text
111
- .replace(/```[\s\S]*?```/g, "") // code blocks
112
- .replace(/`[^`]+`/g, "") // inline code
113
- .replace(/\*\*(.*?)\*\*/g, "$1") // bold
114
- .replace(/\*(.*?)\*/g, "$1") // italic
115
- .replace(/^[#>โ€ข\-*]\s*/gm, "") // headings, quotes, bullets
156
+ .replace(/```[\s\S]*?```/g, "")
157
+ .replace(/`[^`]+`/g, "")
158
+ .replace(/\*\*(.*?)\*\*/g, "$1")
159
+ .replace(/\*(.*?)\*/g, "$1")
160
+ .replace(/^[#>โ€ข\-*]\s*/gm, "")
116
161
  .replace(/\s+/g, " ")
117
162
  .trim()
118
- .slice(0, 1000); // Cap to ~1000 chars to keep TTS fast
163
+ .slice(0, 1000);
119
164
  if (!cleaned)
120
165
  return;
121
166
  try {
122
- const ai = new GoogleGenAI({ apiKey: key });
167
+ const ai = new GoogleGenAI({ apiKey: geminiKey });
123
168
  const response = await ai.models.generateContent({
124
- model: "gemini-2.5-flash-preview-tts",
169
+ model: currentTtsModel,
125
170
  contents: [{ parts: [{ text: cleaned }] }],
126
171
  config: {
127
172
  responseModalities: [Modality.AUDIO],
128
173
  speechConfig: {
129
174
  voiceConfig: {
130
- prebuiltVoiceConfig: { voiceName: "Zephyr" },
175
+ prebuiltVoiceConfig: { voiceName: currentVoice },
131
176
  },
132
177
  },
133
178
  },
@@ -142,12 +187,12 @@ export async function speakText(text, apiKey) {
142
187
  if (audioParts.length === 0)
143
188
  return;
144
189
  const pcmData = Buffer.concat(audioParts);
145
- const wavHeader = buildWavHeader(pcmData.length);
146
- const wavBuffer = Buffer.concat([wavHeader, pcmData]);
147
- // Fire and forget โ€” does not block the REPL
190
+ const wavBuffer = Buffer.concat([buildWavHeader(pcmData.length), pcmData]);
148
191
  playWav(wavBuffer);
149
192
  }
150
193
  catch {
151
- // Silently swallow TTS errors โ€” never disrupt the agent session
194
+ // Silently ignore โ€” TTS errors must never crash the agent REPL
195
+ // Invalidate cached key so we retry fetching on the next call
196
+ cachedGeminiKey = null;
152
197
  }
153
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "careervivid",
3
- "version": "2.1.13",
3
+ "version": "2.1.16",
4
4
  "description": "Official CLI for CareerVivid โ€” AI voice interviews, autonomous job applications, resume editing, and portfolio publishing from your terminal",
5
5
  "type": "module",
6
6
  "bin": {