@markusylisiurunen/tau 0.3.76 → 0.3.77

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 (124) hide show
  1. package/dist/core/cli.js +0 -3
  2. package/dist/core/cli.js.map +1 -1
  3. package/dist/core/config/builtin_themes.js +0 -30
  4. package/dist/core/config/builtin_themes.js.map +1 -1
  5. package/dist/core/config/content_loader.js +10 -115
  6. package/dist/core/config/content_loader.js.map +1 -1
  7. package/dist/core/config/index.js +1 -1
  8. package/dist/core/config/index.js.map +1 -1
  9. package/dist/core/config/paths.js +0 -1
  10. package/dist/core/config/paths.js.map +1 -1
  11. package/dist/core/config/runtime.js +1 -1
  12. package/dist/core/config/runtime.js.map +1 -1
  13. package/dist/core/config/runtime_config_snapshot.js +0 -1
  14. package/dist/core/config/runtime_config_snapshot.js.map +1 -1
  15. package/dist/core/config/schema.js +8 -84
  16. package/dist/core/config/schema.js.map +1 -1
  17. package/dist/core/config/skills_loader.js +0 -8
  18. package/dist/core/config/skills_loader.js.map +1 -1
  19. package/dist/core/config/theme_variants.js.map +1 -1
  20. package/dist/core/config/virtual_bundle.js +1 -4
  21. package/dist/core/config/virtual_bundle.js.map +1 -1
  22. package/dist/core/debug.js +0 -2
  23. package/dist/core/debug.js.map +1 -1
  24. package/dist/core/index.js +0 -1
  25. package/dist/core/index.js.map +1 -1
  26. package/dist/core/models/catalog.js +2 -3
  27. package/dist/core/models/catalog.js.map +1 -1
  28. package/dist/core/personas.js +3 -81
  29. package/dist/core/personas.js.map +1 -1
  30. package/dist/core/static/index.js +0 -3
  31. package/dist/core/static/index.js.map +1 -1
  32. package/dist/core/static/tau_docs/config-reference.md +17 -59
  33. package/dist/core/static/tau_docs/configuration.md +5 -10
  34. package/dist/core/static/tau_docs/credentials.md +9 -28
  35. package/dist/core/static/tau_docs/image-generation.md +1 -1
  36. package/dist/core/static/tau_docs/node-sdk.md +1 -1
  37. package/dist/core/static/tau_docs/ownership-and-scope.md +3 -3
  38. package/dist/core/static/tau_docs/pdf-unpacking.md +1 -1
  39. package/dist/core/static/tau_docs/personas.md +3 -1
  40. package/dist/core/static/tau_docs/prompts-and-project-context.md +0 -10
  41. package/dist/core/static/tau_docs/remote-sessions.md +5 -21
  42. package/dist/core/static/tau_docs/session-protocol-methods.md +1 -7
  43. package/dist/core/static/tau_docs/skills.md +0 -22
  44. package/dist/core/static/tau_docs/speech-generation.md +1 -1
  45. package/dist/core/static/tau_docs/telegram.md +4 -9
  46. package/dist/core/static/tau_docs/troubleshooting.md +15 -15
  47. package/dist/core/static/tau_docs/tui.md +9 -24
  48. package/dist/core/telegram/adapter.js +17 -20
  49. package/dist/core/telegram/adapter.js.map +1 -1
  50. package/dist/core/telegram/cli.js +4 -5
  51. package/dist/core/telegram/cli.js.map +1 -1
  52. package/dist/core/telegram/runtime.js +3 -2
  53. package/dist/core/telegram/runtime.js.map +1 -1
  54. package/dist/core/telegram/tts.js +11 -9
  55. package/dist/core/telegram/tts.js.map +1 -1
  56. package/dist/core/tool/image_generate.js +37 -14
  57. package/dist/core/tool/image_generate.js.map +1 -1
  58. package/dist/core/tool/media.js +84 -4
  59. package/dist/core/tool/media.js.map +1 -1
  60. package/dist/core/tool/pdf_unpack.js +17 -10
  61. package/dist/core/tool/pdf_unpack.js.map +1 -1
  62. package/dist/core/tool/speech_generate.js +30 -13
  63. package/dist/core/tool/speech_generate.js.map +1 -1
  64. package/dist/core/utils/gemini_transcription.js +5 -95
  65. package/dist/core/utils/gemini_transcription.js.map +1 -1
  66. package/dist/core/utils/{gemini_speech.js → speech.js} +167 -428
  67. package/dist/core/utils/speech.js.map +1 -0
  68. package/dist/core/utils/speech_to_text.js +24 -52
  69. package/dist/core/utils/speech_to_text.js.map +1 -1
  70. package/dist/core/utils/speech_to_text_keywords.js +65 -0
  71. package/dist/core/utils/speech_to_text_keywords.js.map +1 -1
  72. package/dist/core/version.js +1 -1
  73. package/dist/execution/fly_sprite_execution_environment.js +4 -2
  74. package/dist/execution/fly_sprite_execution_environment.js.map +1 -1
  75. package/dist/main.js +6 -78
  76. package/dist/main.js.map +1 -1
  77. package/dist/protocol/index.d.ts +1 -1
  78. package/dist/protocol/index.js.map +1 -1
  79. package/dist/protocol/session_protocol.d.ts +2 -15
  80. package/dist/protocol/session_protocol.js +1 -20
  81. package/dist/protocol/session_protocol.js.map +1 -1
  82. package/dist/sdk/local_client.js +0 -6
  83. package/dist/sdk/local_client.js.map +1 -1
  84. package/dist/tui/chat_view.js +1 -3
  85. package/dist/tui/chat_view.js.map +1 -1
  86. package/dist/tui/listen_capture.js +7 -26
  87. package/dist/tui/listen_capture.js.map +1 -1
  88. package/dist/tui/session_chat_app.js +3 -19
  89. package/dist/tui/session_chat_app.js.map +1 -1
  90. package/dist/tui/session_chat_controller.js +58 -29
  91. package/dist/tui/session_chat_controller.js.map +1 -1
  92. package/dist/tui/speech_playback.js +10 -8
  93. package/dist/tui/speech_playback.js.map +1 -1
  94. package/dist/tui/ui/pending_messages.js +2 -1
  95. package/dist/tui/ui/pending_messages.js.map +1 -1
  96. package/package.json +3 -3
  97. package/dist/core/install/cli.js +0 -260
  98. package/dist/core/install/cli.js.map +0 -1
  99. package/dist/core/models/tau_model_overrides.js +0 -4
  100. package/dist/core/models/tau_model_overrides.js.map +0 -1
  101. package/dist/core/utils/gemini_speech.js.map +0 -1
  102. package/dist/core/utils/openai_transcription.js +0 -594
  103. package/dist/core/utils/openai_transcription.js.map +0 -1
  104. package/dist/execution/cloudflare_sandbox_execution_environment.js +0 -563
  105. package/dist/execution/cloudflare_sandbox_execution_environment.js.map +0 -1
  106. package/dist/execution/sandbox_tool_helpers.js +0 -30
  107. package/dist/execution/sandbox_tool_helpers.js.map +0 -1
  108. package/dist/starter/prompts/approval-review-branch.md +0 -7
  109. package/dist/starter/prompts/approval-review-current-changes.md +0 -7
  110. package/dist/starter/prompts/approval-review-last-commit.md +0 -7
  111. package/dist/starter/prompts/commit-all.md +0 -9
  112. package/dist/starter/prompts/commit-staged.md +0 -9
  113. package/dist/starter/prompts/guided-review-branch.md +0 -7
  114. package/dist/starter/prompts/guided-review-current-changes.md +0 -7
  115. package/dist/starter/prompts/guided-review-last-commit.md +0 -7
  116. package/dist/starter/prompts/plan.md +0 -7
  117. package/dist/starter/prompts/review-branch.md +0 -7
  118. package/dist/starter/prompts/review-current-changes.md +0 -7
  119. package/dist/starter/prompts/review-last-commit.md +0 -7
  120. package/dist/starter/prompts/rewrite-prompt.md +0 -89
  121. package/dist/starter/skills/approval-review/SKILL.md +0 -219
  122. package/dist/starter/skills/code-review/SKILL.md +0 -102
  123. package/dist/starter/skills/guided-review/SKILL.md +0 -152
  124. package/dist/starter/skills/plan/SKILL.md +0 -53
@@ -1,14 +1,16 @@
1
1
  import { z } from "zod";
2
- const GEMINI_GENERATE_CONTENT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
3
- export const GEMINI_SPEECH_PLAYBACK_RATE = 1.15;
4
- const DEFAULT_GEMINI_SPEECH_REWRITE_MODEL = "gemini-3.8-flash";
5
- const DEFAULT_GEMINI_SPEECH_REWRITE_THINKING_LEVEL = "low";
6
- const DEFAULT_GEMINI_SPEECH_TTS_MODEL = "gemini-3.1-flash-tts-preview";
7
- const DEFAULT_GEMINI_TTS_VOICE_NAME = "Despina";
8
- export const GEMINI_SPEECH_SAMPLE_RATE_HZ = 24000;
9
- export const GEMINI_SPEECH_CHANNEL_COUNT = 1;
10
- export const GEMINI_SPEECH_BITS_PER_SAMPLE = 16;
11
- const DEFAULT_TTS_MAX_ATTEMPTS = 3;
2
+ const OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
3
+ export const SPEECH_PLAYBACK_RATE = 1.15;
4
+ const SPEECH_REWRITE_MODEL = "gpt-6-luna";
5
+ const ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1";
6
+ const SPEECH_MODEL = "eleven_v4_turbo";
7
+ const DEFAULT_VOICE_ID = "QtY3JBOUKEB5xzrRfOKc";
8
+ const FALLBACK_VOICE_ID = "AaOhDHYJ1XLZk74lXhdE";
9
+ const SPEECH_DELIVERY_NOTE = "[Brisk but relaxed, speaking naturally to a colleague] ";
10
+ const SPEECH_REQUEST_TIMEOUT_MS = 120_000;
11
+ export const SPEECH_SAMPLE_RATE_HZ = 24000;
12
+ export const SPEECH_CHANNEL_COUNT = 1;
13
+ export const SPEECH_BITS_PER_SAMPLE = 16;
12
14
  const SPEECH_REWRITE_TIMEOUT_MS = 60_000;
13
15
  const COMPLETE_TTS_CONCURRENCY = 3;
14
16
  const MAX_SPEECH_SEGMENT_SECONDS = 120;
@@ -17,42 +19,18 @@ const MAX_SPEECH_SEGMENT_WEIGHT = MAX_SPEECH_SEGMENT_SECONDS * ESTIMATED_SPEECH_
17
19
  const MAX_SPEECH_SOURCE_CHARACTERS = 10_000;
18
20
  const MAX_SPOKEN_TEXT_CHARACTERS = 10_000;
19
21
  const MAX_SPEECH_PCM_BYTES = 32 * 1024 * 1024;
20
- const TTS_MAX_OUTPUT_TOKENS = 8192;
21
- const RETRYABLE_TTS_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
22
22
  const errorPayloadSchema = z.object({
23
23
  error: z
24
24
  .object({
25
25
  message: z.string().trim().min(1).optional(),
26
- status: z.string().trim().min(1).optional(),
27
- code: z.number().int().optional(),
28
26
  })
29
27
  .optional(),
30
28
  });
31
- class GeminiApiError extends Error {
32
- status;
33
- constructor(message, status) {
34
- super(message);
35
- this.name = "GeminiApiError";
36
- this.status = status;
37
- }
38
- }
39
- class GeminiTtsResponseError extends Error {
40
- constructor(message) {
41
- super(message);
42
- this.name = "GeminiTtsResponseError";
43
- }
44
- }
45
- class GeminiTtsOutputLimitError extends Error {
46
- constructor() {
47
- super("Gemini TTS reached its output token limit");
48
- this.name = "GeminiTtsOutputLimitError";
49
- }
50
- }
51
- export async function* generateGeminiSpeechAudio(options) {
29
+ export async function* generateSpeechAudio(options) {
52
30
  const abortController = createLinkedAbortController(options.signal);
53
31
  let completed = false;
54
32
  try {
55
- const prepared = await prepareGeminiSpeech(options, abortController.signal);
33
+ const prepared = await prepareSpeech(options, abortController.signal);
56
34
  for await (const chunk of synthesizeSpeechAudioSegmentsInOrder({
57
35
  ...prepared,
58
36
  signal: abortController.signal,
@@ -65,9 +43,9 @@ export async function* generateGeminiSpeechAudio(options) {
65
43
  total: prepared.spokenSegments.length,
66
44
  audio: encodeWaveFile({
67
45
  pcmAudio: chunk.pcmAudio,
68
- sampleRateHz: GEMINI_SPEECH_SAMPLE_RATE_HZ,
69
- channelCount: GEMINI_SPEECH_CHANNEL_COUNT,
70
- bitsPerSample: GEMINI_SPEECH_BITS_PER_SAMPLE,
46
+ sampleRateHz: SPEECH_SAMPLE_RATE_HZ,
47
+ channelCount: SPEECH_CHANNEL_COUNT,
48
+ bitsPerSample: SPEECH_BITS_PER_SAMPLE,
71
49
  }),
72
50
  mimeType: "audio/wav",
73
51
  };
@@ -81,12 +59,12 @@ export async function* generateGeminiSpeechAudio(options) {
81
59
  abortController.dispose();
82
60
  }
83
61
  }
84
- export async function* streamGeminiSpeechPcm(options) {
62
+ export async function* streamSpeechPcm(options) {
85
63
  const abortController = createLinkedAbortController(options.signal);
86
64
  let completed = false;
87
65
  let totalPcmBytes = 0;
88
66
  try {
89
- const prepared = await prepareGeminiSpeech(options, abortController.signal);
67
+ const prepared = await prepareSpeech(options, abortController.signal);
90
68
  const total = prepared.spokenSegments.length;
91
69
  const accountAudio = (audio) => {
92
70
  totalPcmBytes += audio.length;
@@ -101,23 +79,16 @@ export async function* streamGeminiSpeechPcm(options) {
101
79
  accountAudio,
102
80
  }).then((audio) => ({ audio }), (error) => ({ error }));
103
81
  let ready = 0;
104
- const firstStream = streamSpeechSegmentWithRetries({
82
+ const firstStream = streamSpeechSegment({
105
83
  ...prepared,
106
84
  spokenText: prepared.spokenSegments[0],
107
85
  signal: abortController.signal,
108
86
  initialBufferBytes: Math.max(0, Math.trunc(options.initialBufferBytes ?? 0)),
109
87
  });
110
- const firstIterator = firstStream[Symbol.asyncIterator]();
111
- let nextAudio = firstIterator.next();
112
88
  let prefetched = total > 1 ? prefetch(1) : undefined;
113
- while (true) {
114
- const next = await nextAudio;
115
- if (next.done) {
116
- break;
117
- }
118
- accountAudio(next.value);
119
- yield { index: 0, total, audio: next.value };
120
- nextAudio = firstIterator.next();
89
+ for await (const audio of firstStream) {
90
+ accountAudio(audio);
91
+ yield { index: 0, total, audio };
121
92
  }
122
93
  ready += 1;
123
94
  await options.onSegmentProgress?.({ ready, total });
@@ -126,7 +97,7 @@ export async function* streamGeminiSpeechPcm(options) {
126
97
  if ("error" in outcome) {
127
98
  throw outcome.error instanceof Error
128
99
  ? outcome.error
129
- : new Error("Gemini TTS request failed");
100
+ : new Error("ElevenLabs speech request failed");
130
101
  }
131
102
  prefetched = index + 1 < total ? prefetch(index + 1) : undefined;
132
103
  yield { index, total, audio: outcome.audio };
@@ -142,7 +113,7 @@ export async function* streamGeminiSpeechPcm(options) {
142
113
  abortController.dispose();
143
114
  }
144
115
  }
145
- async function prepareGeminiSpeech(options, signal) {
116
+ async function prepareSpeech(options, signal) {
146
117
  const sourceText = options.sourceText.trim();
147
118
  if (!sourceText) {
148
119
  throw new Error("speech source text was empty");
@@ -150,9 +121,13 @@ async function prepareGeminiSpeech(options, signal) {
150
121
  if (exceedsUnicodeCharacterLimit(sourceText, MAX_SPEECH_SOURCE_CHARACTERS)) {
151
122
  throw new Error("speech source text exceeds 10,000 characters");
152
123
  }
153
- const apiKey = options.apiKey.trim();
154
- if (!apiKey) {
155
- throw new Error("missing Gemini API key");
124
+ const apiKey = options.elevenLabsApiKey.trim();
125
+ const openAIApiKey = options.openAIApiKey.trim();
126
+ if (!apiKey || !openAIApiKey) {
127
+ throw new Error("OpenAI and ElevenLabs API keys are required for speech");
128
+ }
129
+ if (options.voiceId !== undefined && !options.voiceId.trim()) {
130
+ throw new Error("speech voice ID must not be empty");
156
131
  }
157
132
  const fetchImpl = options.fetchImpl ?? fetch;
158
133
  await options.onStageChange?.("rewriting");
@@ -162,8 +137,7 @@ async function prepareGeminiSpeech(options, signal) {
162
137
  let spokenText;
163
138
  try {
164
139
  spokenText = await rewriteTextForSpeech({
165
- apiKey,
166
- model: options.rewriteModel ?? DEFAULT_GEMINI_SPEECH_REWRITE_MODEL,
140
+ apiKey: openAIApiKey,
167
141
  sourceText,
168
142
  fetchImpl,
169
143
  signal: rewriteController.signal,
@@ -184,43 +158,62 @@ async function prepareGeminiSpeech(options, signal) {
184
158
  }
185
159
  const spokenSegments = splitSpeechSegments(spokenText);
186
160
  await options.onStageChange?.("generating");
161
+ const voiceId = await resolveSpeechVoice({
162
+ apiKey,
163
+ voiceId: options.voiceId,
164
+ fetchImpl,
165
+ signal,
166
+ });
187
167
  await options.onSegmentProgress?.({ ready: 0, total: spokenSegments.length });
188
168
  return {
189
169
  apiKey,
190
- model: options.ttsModel ?? DEFAULT_GEMINI_SPEECH_TTS_MODEL,
191
- voiceName: options.voiceName ?? DEFAULT_GEMINI_TTS_VOICE_NAME,
170
+ voiceId,
192
171
  spokenSegments,
193
172
  fetchImpl,
194
- maxAttempts: options.maxTtsAttempts ?? DEFAULT_TTS_MAX_ATTEMPTS,
195
173
  };
196
174
  }
197
175
  async function rewriteTextForSpeech(args) {
198
- const payload = await requestGeminiGenerateContent({
199
- apiKey: args.apiKey,
200
- model: args.model,
201
- fetchImpl: args.fetchImpl,
202
- signal: args.signal,
203
- body: {
204
- contents: [
205
- {
206
- parts: [
207
- {
208
- text: buildSpeechRewritePrompt(args.sourceText),
209
- },
210
- ],
211
- },
212
- ],
213
- generationConfig: {
214
- thinkingConfig: {
215
- thinkingLevel: DEFAULT_GEMINI_SPEECH_REWRITE_THINKING_LEVEL,
216
- },
217
- },
176
+ const response = await args.fetchImpl(OPENAI_RESPONSES_URL, {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ Authorization: `Bearer ${args.apiKey}`,
218
181
  },
182
+ body: JSON.stringify({
183
+ model: SPEECH_REWRITE_MODEL,
184
+ input: buildSpeechRewritePrompt(args.sourceText),
185
+ reasoning: { effort: "none" },
186
+ store: false,
187
+ }),
188
+ signal: args.signal,
219
189
  });
220
- const rewrittenText = extractGeminiText(payload).trim();
221
- if (!rewrittenText) {
222
- throw new Error("Gemini rewrite returned empty text");
190
+ const payload = await response.json();
191
+ if (!response.ok) {
192
+ const parsed = errorPayloadSchema.safeParse(payload);
193
+ throw new Error(parsed.success && parsed.data.error?.message
194
+ ? parsed.data.error.message
195
+ : `OpenAI speech rewrite failed (HTTP ${response.status})`);
196
+ }
197
+ if (!isObject(payload) || payload.status !== "completed" || !Array.isArray(payload.output)) {
198
+ throw new Error("OpenAI speech rewrite did not complete");
199
+ }
200
+ const text = [];
201
+ for (const item of payload.output) {
202
+ if (!isObject(item) || item.type !== "message")
203
+ continue;
204
+ if (item.status !== "completed" || !Array.isArray(item.content)) {
205
+ throw new Error("OpenAI speech rewrite returned an incomplete message");
206
+ }
207
+ for (const part of item.content) {
208
+ if (!isObject(part) || part.type !== "output_text" || typeof part.text !== "string") {
209
+ throw new Error("OpenAI speech rewrite did not return text");
210
+ }
211
+ text.push(part.text);
212
+ }
223
213
  }
214
+ const rewrittenText = text.join("").trim();
215
+ if (!rewrittenText)
216
+ throw new Error("OpenAI speech rewrite returned empty text");
224
217
  return rewrittenText;
225
218
  }
226
219
  async function* synthesizeSpeechAudioSegmentsInOrder(args) {
@@ -299,7 +292,7 @@ async function* synthesizeSpeechAudioSegmentsInOrder(args) {
299
292
  continue;
300
293
  }
301
294
  if (failure !== undefined) {
302
- throw failure instanceof Error ? failure : new Error("Gemini TTS request failed");
295
+ throw failure instanceof Error ? failure : new Error("ElevenLabs speech request failed");
303
296
  }
304
297
  await waitForWake();
305
298
  }
@@ -310,323 +303,120 @@ async function* synthesizeSpeechAudioSegmentsInOrder(args) {
310
303
  }
311
304
  }
312
305
  async function synthesizeSpeechAudioSegment(args) {
313
- const maxAttempts = Math.max(1, Math.trunc(args.maxAttempts));
314
- let lastError;
315
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
316
- try {
317
- const payload = await requestGeminiGenerateContent({
318
- apiKey: args.apiKey,
319
- model: args.model,
320
- fetchImpl: args.fetchImpl,
321
- signal: args.signal,
322
- body: buildSpeechSynthesisRequest(args.spokenText, args.voiceName),
323
- });
324
- assertGeminiTtsCompleted(payload);
325
- const audioData = extractGeminiInlineAudioData(payload);
326
- if (!audioData) {
327
- throw new GeminiTtsResponseError("Gemini TTS response did not include audio data");
306
+ const chunks = [];
307
+ for await (const audio of requestSpeechStream(args)) {
308
+ chunks.push(audio);
309
+ }
310
+ return Buffer.concat(chunks);
311
+ }
312
+ async function* streamSpeechSegment(args) {
313
+ const bufferedAudio = [];
314
+ let bufferedAudioBytes = 0;
315
+ let emittedAudio = false;
316
+ for await (const audio of requestSpeechStream(args)) {
317
+ if (!emittedAudio) {
318
+ bufferedAudio.push(audio);
319
+ bufferedAudioBytes += audio.length;
320
+ if (bufferedAudioBytes < args.initialBufferBytes) {
321
+ continue;
328
322
  }
329
- return Buffer.from(audioData, "base64");
323
+ emittedAudio = true;
324
+ yield Buffer.concat(bufferedAudio);
325
+ bufferedAudio.length = 0;
330
326
  }
331
- catch (error) {
332
- lastError = error;
333
- if (args.signal?.aborted || !isRetryableTtsError(error) || attempt >= maxAttempts) {
334
- throw error;
335
- }
336
- await waitForRetryDelay(attempt, args.signal);
327
+ else {
328
+ yield audio;
337
329
  }
338
330
  }
339
- throw lastError instanceof Error ? lastError : new Error("Gemini TTS request failed");
340
- }
341
- async function* streamSpeechSegmentWithRetries(args) {
342
- const maxAttempts = Math.max(1, Math.trunc(args.maxAttempts));
343
- let lastError;
344
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
345
- const bufferedAudio = [];
346
- let bufferedAudioBytes = 0;
347
- let emittedAudio = false;
348
- try {
349
- for await (const audio of requestGeminiSpeechStream(args)) {
350
- if (!emittedAudio && bufferedAudioBytes < args.initialBufferBytes) {
351
- bufferedAudio.push(audio);
352
- bufferedAudioBytes += audio.length;
353
- if (bufferedAudioBytes < args.initialBufferBytes) {
354
- continue;
355
- }
356
- emittedAudio = true;
357
- yield Buffer.concat(bufferedAudio);
358
- continue;
359
- }
360
- emittedAudio = true;
361
- yield audio;
362
- }
363
- if (bufferedAudio.length > 0 && !emittedAudio) {
364
- emittedAudio = true;
365
- yield Buffer.concat(bufferedAudio);
366
- }
367
- return;
368
- }
369
- catch (error) {
370
- lastError = error;
371
- if (emittedAudio ||
372
- args.signal?.aborted ||
373
- !isRetryableTtsError(error) ||
374
- attempt >= maxAttempts) {
375
- throw error;
376
- }
377
- await waitForRetryDelay(attempt, args.signal);
378
- }
331
+ if (!emittedAudio && bufferedAudio.length > 0) {
332
+ yield Buffer.concat(bufferedAudio);
379
333
  }
380
- throw lastError instanceof Error ? lastError : new Error("Gemini TTS request failed");
381
334
  }
382
335
  async function collectStreamingSpeechSegment(args) {
383
- const maxAttempts = Math.max(1, Math.trunc(args.maxAttempts));
384
- let lastError;
385
- for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
386
- const chunks = [];
387
- try {
388
- for await (const audio of requestGeminiSpeechStream(args)) {
389
- chunks.push(audio);
390
- }
391
- }
392
- catch (error) {
393
- lastError = error;
394
- if (args.signal?.aborted || !isRetryableTtsError(error) || attempt >= maxAttempts) {
395
- throw error;
396
- }
397
- await waitForRetryDelay(attempt, args.signal);
398
- continue;
399
- }
400
- const audio = Buffer.concat(chunks);
336
+ const chunks = [];
337
+ for await (const audio of requestSpeechStream(args)) {
401
338
  args.accountAudio(audio);
402
- return audio;
339
+ chunks.push(audio);
403
340
  }
404
- throw lastError instanceof Error ? lastError : new Error("Gemini TTS request failed");
341
+ return Buffer.concat(chunks);
405
342
  }
406
- async function* requestGeminiSpeechStream(args) {
407
- const response = await requestGeminiResponse({
408
- apiKey: args.apiKey,
409
- model: args.model,
410
- method: "streamGenerateContent?alt=sse",
411
- fetchImpl: args.fetchImpl,
412
- signal: args.signal,
413
- body: buildSpeechSynthesisRequest(args.spokenText, args.voiceName),
414
- });
415
- if (!response.body) {
416
- throw new GeminiTtsResponseError("Gemini TTS streaming response did not include a body");
417
- }
418
- let receivedAudio = false;
419
- let completed = false;
420
- for await (const payload of parseGeminiSse(response.body)) {
421
- const finishReason = getGeminiFinishReason(payload);
422
- if (finishReason) {
423
- assertGeminiTtsFinishReason(finishReason);
424
- completed = finishReason === "STOP";
343
+ async function resolveSpeechVoice(args) {
344
+ const voiceIds = args.voiceId === undefined ? [DEFAULT_VOICE_ID, FALLBACK_VOICE_ID] : [args.voiceId.trim()];
345
+ for (const voiceId of voiceIds) {
346
+ const response = await args.fetchImpl(`${ELEVENLABS_BASE_URL}/voices/${encodeURIComponent(voiceId)}`, {
347
+ headers: { "xi-api-key": args.apiKey },
348
+ signal: AbortSignal.any([args.signal, AbortSignal.timeout(SPEECH_REQUEST_TIMEOUT_MS)]),
349
+ });
350
+ if (response.ok) {
351
+ const payload = await response.json();
352
+ if (!isObject(payload) || payload.voice_id !== voiceId) {
353
+ throw new Error("ElevenLabs returned invalid voice metadata");
354
+ }
355
+ return voiceId;
425
356
  }
426
- for (const audioData of extractGeminiInlineAudioDataParts(payload)) {
427
- receivedAudio = true;
428
- yield Buffer.from(audioData, "base64");
357
+ const payload = await response.json().catch(() => undefined);
358
+ const detail = isObject(payload) && isObject(payload.detail) ? payload.detail : undefined;
359
+ const unavailable = (response.status === 400 || response.status === 404) && detail?.status === "voice_not_found";
360
+ if (!unavailable) {
361
+ throw new Error(`ElevenLabs voice lookup failed (HTTP ${response.status})`);
429
362
  }
430
363
  }
431
- if (!receivedAudio) {
432
- throw new GeminiTtsResponseError("Gemini TTS response did not include audio data");
433
- }
434
- if (!completed) {
435
- throw new GeminiTtsResponseError("Gemini TTS stream ended without a stop response");
436
- }
364
+ throw new Error(args.voiceId === undefined
365
+ ? "Neither Maisie nor Caleb is available from ElevenLabs"
366
+ : "The configured ElevenLabs speech voice is unavailable");
437
367
  }
438
- function buildSpeechSynthesisRequest(spokenText, voiceName) {
439
- return {
440
- contents: [
441
- {
442
- parts: [
443
- {
444
- text: buildSpeechSynthesisPrompt(spokenText),
445
- },
446
- ],
447
- },
448
- ],
449
- generationConfig: {
450
- responseModalities: ["AUDIO"],
451
- maxOutputTokens: TTS_MAX_OUTPUT_TOKENS,
452
- speechConfig: {
453
- voiceConfig: {
454
- prebuiltVoiceConfig: {
455
- voiceName,
456
- },
457
- },
458
- },
459
- },
460
- };
461
- }
462
- async function requestGeminiGenerateContent(args) {
463
- const response = await requestGeminiResponse({ ...args, method: "generateContent" });
464
- const responseText = await response.text();
465
- try {
466
- return responseText ? JSON.parse(responseText) : undefined;
467
- }
468
- catch {
469
- return undefined;
470
- }
471
- }
472
- async function requestGeminiResponse(args) {
473
- const response = await args.fetchImpl(`${GEMINI_GENERATE_CONTENT_BASE_URL}/${encodeURIComponent(args.model)}:${args.method}`, {
368
+ async function* requestSpeechStream(args) {
369
+ const timeout = AbortSignal.timeout(SPEECH_REQUEST_TIMEOUT_MS);
370
+ const signal = args.signal ? AbortSignal.any([args.signal, timeout]) : timeout;
371
+ const response = await args.fetchImpl(`${ELEVENLABS_BASE_URL}/text-to-speech/${encodeURIComponent(args.voiceId)}/stream?output_format=pcm_24000`, {
474
372
  method: "POST",
475
- headers: {
476
- "Content-Type": "application/json",
477
- "x-goog-api-key": args.apiKey,
478
- },
479
- body: JSON.stringify(args.body),
480
- signal: args.signal,
373
+ headers: { "Content-Type": "application/json", "xi-api-key": args.apiKey },
374
+ body: JSON.stringify({
375
+ model_id: SPEECH_MODEL,
376
+ text: SPEECH_DELIVERY_NOTE + args.spokenText,
377
+ }),
378
+ signal,
481
379
  });
482
- if (response.ok) {
483
- return response;
484
- }
485
- const responseText = await response.text();
486
- let payload;
487
- try {
488
- payload = responseText ? JSON.parse(responseText) : undefined;
489
- }
490
- catch {
491
- payload = undefined;
492
- }
493
- const parsed = errorPayloadSchema.safeParse(payload);
494
- const fallbackMessage = responseText.trim() || `HTTP ${response.status}`;
495
- const message = parsed.success
496
- ? (parsed.data.error?.message ?? fallbackMessage)
497
- : fallbackMessage;
498
- throw new GeminiApiError(message, response.status);
499
- }
500
- async function* parseGeminiSse(body) {
501
- const reader = body.getReader();
502
- const decoder = new TextDecoder();
503
- let buffered = "";
380
+ if (!response.ok) {
381
+ await response.body?.cancel();
382
+ throw new Error(`ElevenLabs speech generation failed (HTTP ${response.status})`);
383
+ }
384
+ const contentType = response.headers.get("content-type")?.split(";")[0]?.trim();
385
+ if (contentType !== "audio/pcm" || !response.body) {
386
+ await response.body?.cancel();
387
+ throw new Error("ElevenLabs speech response did not include PCM audio");
388
+ }
389
+ const reader = response.body.getReader();
390
+ let totalBytes = 0;
391
+ let pendingByte = Buffer.alloc(0);
504
392
  let completed = false;
505
393
  try {
506
394
  while (true) {
395
+ signal.throwIfAborted();
507
396
  const { done, value } = await reader.read();
508
- buffered += decoder.decode(value, { stream: !done });
509
- let boundary = findSseEventBoundary(buffered);
510
- while (boundary) {
511
- const event = buffered.slice(0, boundary.index);
512
- buffered = buffered.slice(boundary.index + boundary.length);
513
- const payload = parseSseEvent(event);
514
- if (payload !== undefined) {
515
- yield payload;
516
- }
517
- boundary = findSseEventBoundary(buffered);
518
- }
519
- if (done) {
520
- const payload = parseSseEvent(buffered);
521
- if (payload !== undefined) {
522
- yield payload;
523
- }
524
- completed = true;
525
- return;
397
+ if (done)
398
+ break;
399
+ totalBytes += value.length;
400
+ if (totalBytes > MAX_SPEECH_PCM_BYTES) {
401
+ throw new Error("generated speech audio exceeds the 32 MiB limit");
526
402
  }
403
+ const audio = Buffer.concat([pendingByte, value]);
404
+ const alignedLength = audio.length - (audio.length % 2);
405
+ pendingByte = Buffer.from(audio.subarray(alignedLength));
406
+ if (alignedLength > 0)
407
+ yield audio.subarray(0, alignedLength);
408
+ }
409
+ if (totalBytes === 0 || pendingByte.length > 0) {
410
+ throw new Error("ElevenLabs returned empty or incomplete PCM audio");
527
411
  }
412
+ completed = true;
528
413
  }
529
414
  finally {
530
- if (!completed) {
415
+ if (!completed)
531
416
  await reader.cancel().catch(() => { });
532
- }
533
417
  reader.releaseLock();
534
418
  }
535
419
  }
536
- function findSseEventBoundary(text) {
537
- const match = /\r?\n\r?\n/u.exec(text);
538
- return match ? { index: match.index, length: match[0].length } : undefined;
539
- }
540
- function parseSseEvent(event) {
541
- const data = event
542
- .split(/\r?\n/u)
543
- .filter((line) => line.startsWith("data:"))
544
- .map((line) => line.slice(5).trimStart())
545
- .join("\n")
546
- .trim();
547
- if (!data || data === "[DONE]") {
548
- return undefined;
549
- }
550
- try {
551
- return JSON.parse(data);
552
- }
553
- catch {
554
- throw new GeminiTtsResponseError("Gemini TTS returned malformed streaming data");
555
- }
556
- }
557
- function extractGeminiText(payload) {
558
- if (!isObject(payload) || !Array.isArray(payload.candidates)) {
559
- return "";
560
- }
561
- for (const candidate of payload.candidates) {
562
- if (!isObject(candidate) ||
563
- !isObject(candidate.content) ||
564
- !Array.isArray(candidate.content.parts)) {
565
- continue;
566
- }
567
- const text = candidate.content.parts
568
- .map((part) => (isObject(part) && typeof part.text === "string" ? part.text : ""))
569
- .join("")
570
- .trim();
571
- if (text) {
572
- return text;
573
- }
574
- }
575
- return "";
576
- }
577
- function assertGeminiTtsCompleted(payload) {
578
- const finishReason = getGeminiFinishReason(payload);
579
- if (finishReason) {
580
- assertGeminiTtsFinishReason(finishReason);
581
- }
582
- }
583
- function assertGeminiTtsFinishReason(finishReason) {
584
- if (finishReason === "MAX_TOKENS") {
585
- throw new GeminiTtsOutputLimitError();
586
- }
587
- if (finishReason !== "STOP") {
588
- throw new GeminiTtsResponseError(`Gemini TTS stopped with finish reason '${finishReason}'`);
589
- }
590
- }
591
- function getGeminiFinishReason(payload) {
592
- if (!isObject(payload) || !Array.isArray(payload.candidates)) {
593
- return undefined;
594
- }
595
- for (const candidate of payload.candidates) {
596
- if (isObject(candidate) && typeof candidate.finishReason === "string") {
597
- return candidate.finishReason;
598
- }
599
- }
600
- return undefined;
601
- }
602
- function extractGeminiInlineAudioData(payload) {
603
- return extractGeminiInlineAudioDataParts(payload)[0];
604
- }
605
- function extractGeminiInlineAudioDataParts(payload) {
606
- if (!isObject(payload) || !Array.isArray(payload.candidates)) {
607
- return [];
608
- }
609
- const audioData = [];
610
- for (const candidate of payload.candidates) {
611
- if (!isObject(candidate) ||
612
- !isObject(candidate.content) ||
613
- !Array.isArray(candidate.content.parts)) {
614
- continue;
615
- }
616
- for (const part of candidate.content.parts) {
617
- if (!isObject(part) ||
618
- !isObject(part.inlineData) ||
619
- typeof part.inlineData.data !== "string") {
620
- continue;
621
- }
622
- const data = part.inlineData.data.trim();
623
- if (data) {
624
- audioData.push(data);
625
- }
626
- }
627
- }
628
- return audioData;
629
- }
630
420
  function buildSpeechRewritePrompt(sourceText) {
631
421
  return [
632
422
  "Rewrite the assistant response below so it sounds natural when spoken aloud.",
@@ -640,7 +430,7 @@ function buildSpeechRewritePrompt(sourceText) {
640
430
  "Rewrite a code identifier only when its literal form would be difficult to follow aloud, and preserve its exact meaning.",
641
431
  "",
642
432
  "Examples of good rewrites:",
643
- '- `src/core/utils/gemini_speech.ts:372` → "gemini_speech.ts, line 372"',
433
+ '- `src/core/utils/speech.ts:372` → "speech.ts, line 372"',
644
434
  '- `src/tui/session_chat_controller.ts:1819-1855` → "session_chat_controller.ts, lines 1819 to 1855"',
645
435
  '- `src/core/session/compaction.ts` → "compaction.ts"',
646
436
  '- `/Users/markus/.config/tau/config.json` → "the tau config.json in your home directory"',
@@ -751,57 +541,6 @@ function closestSpeechBoundary(candidates, cumulativeWeights, idealWeight) {
751
541
  function isSpeechSentenceBoundary(previous, next) {
752
542
  return /[。!?]/u.test(previous) || (/[.!?;:]/u.test(previous) && /\s/u.test(next));
753
543
  }
754
- function buildSpeechSynthesisPrompt(spokenText) {
755
- return [
756
- "Synthesize speech audio for the labeled transcript below.",
757
- "Speak only the transcript. Do not speak the instructions or section labels.",
758
- "",
759
- "### DIRECTOR'S NOTES",
760
- "Style: Clear, natural, conversational.",
761
- "Pacing: Brisk conversational speed. Keep it clear, confident, and energetic without sounding rushed.",
762
- "",
763
- "### TRANSCRIPT",
764
- spokenText,
765
- ].join("\n");
766
- }
767
- function isRetryableTtsError(error) {
768
- if (error instanceof GeminiTtsOutputLimitError) {
769
- return false;
770
- }
771
- if (error instanceof GeminiTtsResponseError) {
772
- return true;
773
- }
774
- if (error instanceof GeminiApiError) {
775
- return error.status !== undefined && RETRYABLE_TTS_STATUS_CODES.has(error.status);
776
- }
777
- if (error instanceof Error) {
778
- return error.name !== "AbortError";
779
- }
780
- return false;
781
- }
782
- async function waitForRetryDelay(attempt, signal) {
783
- const durationMs = Math.min(1000, 150 * 2 ** (attempt - 1));
784
- await new Promise((resolve, reject) => {
785
- const timeout = setTimeout(() => {
786
- signal?.removeEventListener("abort", onAbort);
787
- resolve();
788
- }, durationMs);
789
- timeout.unref?.();
790
- const onAbort = () => {
791
- clearTimeout(timeout);
792
- reject(abortError());
793
- };
794
- if (!signal) {
795
- return;
796
- }
797
- if (signal.aborted) {
798
- clearTimeout(timeout);
799
- reject(abortError());
800
- return;
801
- }
802
- signal.addEventListener("abort", onAbort, { once: true });
803
- });
804
- }
805
544
  function createLinkedAbortController(parent) {
806
545
  const controller = new AbortController();
807
546
  const onAbort = () => controller.abort(parent?.reason);
@@ -846,4 +585,4 @@ function abortError() {
846
585
  error.name = "AbortError";
847
586
  return error;
848
587
  }
849
- //# sourceMappingURL=gemini_speech.js.map
588
+ //# sourceMappingURL=speech.js.map