@markusylisiurunen/tau 0.3.63 → 0.3.65

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 (130) hide show
  1. package/dist/core/config/schema.js +2 -2
  2. package/dist/core/config/schema.js.map +1 -1
  3. package/dist/core/diff_review/bridge.js +1 -1
  4. package/dist/core/diff_review/bridge.js.map +1 -1
  5. package/dist/core/diff_review/index.js.map +1 -1
  6. package/dist/core/diff_review/protocol.js +11 -7
  7. package/dist/core/diff_review/protocol.js.map +1 -1
  8. package/dist/core/diff_review/result_text.js +14 -4
  9. package/dist/core/diff_review/result_text.js.map +1 -1
  10. package/dist/core/diff_review/snapshot.js +13 -9
  11. package/dist/core/diff_review/snapshot.js.map +1 -1
  12. package/dist/core/static/tau_docs/config-reference.md +3 -3
  13. package/dist/core/static/tau_docs/configuration.md +1 -1
  14. package/dist/core/static/tau_docs/credentials.md +2 -2
  15. package/dist/core/static/tau_docs/sdk-diff-review.md +15 -6
  16. package/dist/core/static/tau_docs/telegram.md +2 -3
  17. package/dist/core/static/tau_docs/troubleshooting.md +2 -2
  18. package/dist/core/static/tau_docs/tui.md +4 -2
  19. package/dist/core/telegram/adapter.js +0 -7
  20. package/dist/core/telegram/adapter.js.map +1 -1
  21. package/dist/core/telegram/cli.js +1 -3
  22. package/dist/core/telegram/cli.js.map +1 -1
  23. package/dist/core/telegram/runtime.js +0 -1
  24. package/dist/core/telegram/runtime.js.map +1 -1
  25. package/dist/core/utils/gemini_transcription.js +510 -97
  26. package/dist/core/utils/gemini_transcription.js.map +1 -1
  27. package/dist/core/utils/openai_transcription.js +6 -34
  28. package/dist/core/utils/openai_transcription.js.map +1 -1
  29. package/dist/core/utils/speech_to_text.js +23 -14
  30. package/dist/core/utils/speech_to_text.js.map +1 -1
  31. package/dist/core/utils/speech_to_text_keywords.js +32 -0
  32. package/dist/core/utils/speech_to_text_keywords.js.map +1 -0
  33. package/dist/core/version.js +2 -2
  34. package/dist/diff_tool/app/dist/assets/{angular-html-Ix0KPi7a.js → angular-html-DRzRrsA1.js} +1 -1
  35. package/dist/diff_tool/app/dist/assets/{angular-ts-C9FSwTpA.js → angular-ts-tbivEFEF.js} +1 -1
  36. package/dist/diff_tool/app/dist/assets/{apl-DKkOnH2I.js → apl-CY7Wo94D.js} +1 -1
  37. package/dist/diff_tool/app/dist/assets/{astro-DPET4gPf.js → astro-Bx9eET42.js} +1 -1
  38. package/dist/diff_tool/app/dist/assets/{blade-DhJUXJLU.js → blade-MrKrcBds.js} +1 -1
  39. package/dist/diff_tool/app/dist/assets/{c-67jpMvb2.js → c-C5P-tnLE.js} +1 -1
  40. package/dist/diff_tool/app/dist/assets/{chapel-MBDqDT37.js → chapel-DW9xLN1i.js} +1 -1
  41. package/dist/diff_tool/app/dist/assets/{cobol-M2TY0mLn.js → cobol-D5upXK5x.js} +1 -1
  42. package/dist/diff_tool/app/dist/assets/{coffee-BC0bY29j.js → coffee-CH-CbdAu.js} +1 -1
  43. package/dist/diff_tool/app/dist/assets/{cpp-DsCOGWNN.js → cpp-CYOejM6s.js} +1 -1
  44. package/dist/diff_tool/app/dist/assets/{crystal-BCNAyK7e.js → crystal-B1PdqQ4F.js} +1 -1
  45. package/dist/diff_tool/app/dist/assets/{css-Z80vApOh.js → css-B6kONN6r.js} +1 -1
  46. package/dist/diff_tool/app/dist/assets/{edge-Cu-rbWZy.js → edge-DO-0gFMz.js} +1 -1
  47. package/dist/diff_tool/app/dist/assets/{elixir-CAKwsmnn.js → elixir-BIwgFlwP.js} +1 -1
  48. package/dist/diff_tool/app/dist/assets/{elm-CMQXUUh5.js → elm-Dbo-IffC.js} +1 -1
  49. package/dist/diff_tool/app/dist/assets/{erb-B1_VD0-c.js → erb-DZ7kUx4Q.js} +1 -1
  50. package/dist/diff_tool/app/dist/assets/{git-rebase-D4h0IB-Z.js → git-rebase-Cf4aQom5.js} +1 -1
  51. package/dist/diff_tool/app/dist/assets/{glimmer-js-D37W__QR.js → glimmer-js-B8L9c2W6.js} +1 -1
  52. package/dist/diff_tool/app/dist/assets/{glimmer-ts-CJiHgwc1.js → glimmer-ts-CzAUKEr_.js} +1 -1
  53. package/dist/diff_tool/app/dist/assets/{glsl-DPWDnqEZ.js → glsl-BZnQq81v.js} +1 -1
  54. package/dist/diff_tool/app/dist/assets/{graphql-BERyG10r.js → graphql-QI7m1-dG.js} +1 -1
  55. package/dist/diff_tool/app/dist/assets/{hack-Dhrkqbqh.js → hack-oWgmL-1w.js} +1 -1
  56. package/dist/diff_tool/app/dist/assets/{haml-CgHqro8t.js → haml-Dsp05dm-.js} +1 -1
  57. package/dist/diff_tool/app/dist/assets/{handlebars-DA0oBJQC.js → handlebars-DkEP_XDs.js} +1 -1
  58. package/dist/diff_tool/app/dist/assets/{html-C07_gUIM.js → html-Jb4Hlhlf.js} +1 -1
  59. package/dist/diff_tool/app/dist/assets/{html-derivative-C2Iz7NdD.js → html-derivative-jFzowccf.js} +1 -1
  60. package/dist/diff_tool/app/dist/assets/{http-BIjFcXtf.js → http-CIm3Kw31.js} +1 -1
  61. package/dist/diff_tool/app/dist/assets/{hurl-BSo6-xEj.js → hurl-CLvYxNZa.js} +1 -1
  62. package/dist/diff_tool/app/dist/assets/{index-HwJ9ZF3R.js → index-J0S24TTC.js} +19 -19
  63. package/dist/diff_tool/app/dist/assets/{java-CoMQRgf3.js → java-Rw3L1ZVe.js} +1 -1
  64. package/dist/diff_tool/app/dist/assets/{javascript-59Bh6eFS.js → javascript-BDTOnMX6.js} +1 -1
  65. package/dist/diff_tool/app/dist/assets/{jinja-BbL8kFH_.js → jinja-V3XFOf5X.js} +1 -1
  66. package/dist/diff_tool/app/dist/assets/{jison-DkCA1Xy9.js → jison-DsX2fRlw.js} +1 -1
  67. package/dist/diff_tool/app/dist/assets/{json-K7B9fx2M.js → json-DXtaKgIN.js} +1 -1
  68. package/dist/diff_tool/app/dist/assets/{jsx-CYU0LgTf.js → jsx-C--z3Vz0.js} +1 -1
  69. package/dist/diff_tool/app/dist/assets/{julia-Cek7iS3T.js → julia-CYpeWsG7.js} +1 -1
  70. package/dist/diff_tool/app/dist/assets/{just-CV-zTYju.js → just-BuEYXcL7.js} +1 -1
  71. package/dist/diff_tool/app/dist/assets/{latex-CumIBD1i.js → latex-Cm1B1ddK.js} +1 -1
  72. package/dist/diff_tool/app/dist/assets/{liquid-6O_k925L.js → liquid-GkjL8esp.js} +1 -1
  73. package/dist/diff_tool/app/dist/assets/{lua-DSabO_9y.js → lua-CvYt7dZy.js} +1 -1
  74. package/dist/diff_tool/app/dist/assets/{marko-BQCJYZMU.js → marko-BMSo_3mq.js} +1 -1
  75. package/dist/diff_tool/app/dist/assets/{mdc-C52qECYt.js → mdc-oSXeVlop.js} +1 -1
  76. package/dist/diff_tool/app/dist/assets/{nginx-wGDK7kDX.js → nginx-WTmYQb7p.js} +1 -1
  77. package/dist/diff_tool/app/dist/assets/{nim-8L0XMTBj.js → nim-DukmWsYe.js} +1 -1
  78. package/dist/diff_tool/app/dist/assets/{org-qlylGQgn.js → org-B3Mg55pm.js} +1 -1
  79. package/dist/diff_tool/app/dist/assets/{perl-CykLITRy.js → perl-Biyv-RCp.js} +1 -1
  80. package/dist/diff_tool/app/dist/assets/{php-OrvNGJNb.js → php-DhLwDtyd.js} +1 -1
  81. package/dist/diff_tool/app/dist/assets/{pug-YNEJJlsa.js → pug-Bu1fHTcb.js} +1 -1
  82. package/dist/diff_tool/app/dist/assets/{qml-CcMVyzXP.js → qml-CyeLhJPf.js} +1 -1
  83. package/dist/diff_tool/app/dist/assets/{r-BME81pvY.js → r-oMvZ7vof.js} +1 -1
  84. package/dist/diff_tool/app/dist/assets/{razor-B6zaSHM_.js → razor-QR2R7Wy2.js} +1 -1
  85. package/dist/diff_tool/app/dist/assets/{regexp-hfj_QKqr.js → regexp-Dy_006lz.js} +1 -1
  86. package/dist/diff_tool/app/dist/assets/{rst-DW60Xb1U.js → rst-bSV3koz4.js} +1 -1
  87. package/dist/diff_tool/app/dist/assets/{ruby-CC0ZraKC.js → ruby-D4XPeMQn.js} +1 -1
  88. package/dist/diff_tool/app/dist/assets/{sas-9nl6_97c.js → sas-K00l5DQD.js} +1 -1
  89. package/dist/diff_tool/app/dist/assets/{scss-Otpp0Cxe.js → scss-Cw-YxsUO.js} +1 -1
  90. package/dist/diff_tool/app/dist/assets/{shellscript-DZKiAu0u.js → shellscript-Oo-I28wg.js} +1 -1
  91. package/dist/diff_tool/app/dist/assets/{shellsession-CFyCG6LV.js → shellsession-D8YpK5Gt.js} +1 -1
  92. package/dist/diff_tool/app/dist/assets/{soy-4B16Rf4R.js → soy-CirTT52K.js} +1 -1
  93. package/dist/diff_tool/app/dist/assets/{sql-BfAjAHn7.js → sql-C8VWaiZL.js} +1 -1
  94. package/dist/diff_tool/app/dist/assets/{stata-B_5JAo7J.js → stata-BJZ5oZlg.js} +1 -1
  95. package/dist/diff_tool/app/dist/assets/{surrealql-B0HCK9pb.js → surrealql-cWw1kS_r.js} +1 -1
  96. package/dist/diff_tool/app/dist/assets/{svelte-C0cCGsEl.js → svelte-qe4-sQol.js} +1 -1
  97. package/dist/diff_tool/app/dist/assets/{templ-D1R1J2lJ.js → templ-Ttp3sKu1.js} +1 -1
  98. package/dist/diff_tool/app/dist/assets/{tex--Wuw24L6.js → tex-CEAqnEVG.js} +1 -1
  99. package/dist/diff_tool/app/dist/assets/{ts-tags-Dq1FPY1M.js → ts-tags-DSeRvYgh.js} +1 -1
  100. package/dist/diff_tool/app/dist/assets/{tsx-VGY-Mp0k.js → tsx-BNFQ917s.js} +1 -1
  101. package/dist/diff_tool/app/dist/assets/{twig-BEkxP8Q6.js → twig-Dle5Xpmn.js} +1 -1
  102. package/dist/diff_tool/app/dist/assets/{typescript-CPyocJP3.js → typescript-BbQ7yQ6i.js} +1 -1
  103. package/dist/diff_tool/app/dist/assets/{typst-xRIkWHxZ.js → typst-Cbf-O2WN.js} +1 -1
  104. package/dist/diff_tool/app/dist/assets/{vue-C5KmXZMh.js → vue-YmceU--9.js} +1 -1
  105. package/dist/diff_tool/app/dist/assets/{vue-html-xbHMTmXj.js → vue-html-qwVM617a.js} +1 -1
  106. package/dist/diff_tool/app/dist/assets/{vue-vine-CArUes0e.js → vue-vine-CwQvVGBQ.js} +1 -1
  107. package/dist/diff_tool/app/dist/assets/{xml-CgD7OGIH.js → xml-CjaiHDlR.js} +1 -1
  108. package/dist/diff_tool/app/dist/assets/{xsl-DXL0y1Sw.js → xsl-C7AYdAd6.js} +1 -1
  109. package/dist/diff_tool/app/dist/assets/{yaml-BOSKQnUw.js → yaml-BF0Ptbfs.js} +1 -1
  110. package/dist/diff_tool/app/dist/index.html +1 -1
  111. package/dist/diff_tool/http_server.js +3 -3
  112. package/dist/diff_tool/http_server.js.map +1 -1
  113. package/dist/diff_tool/review_state.js +9 -6
  114. package/dist/diff_tool/review_state.js.map +1 -1
  115. package/dist/diff_tool/shared_types.js +3 -0
  116. package/dist/diff_tool/shared_types.js.map +1 -1
  117. package/dist/sdk/diff_review.d.ts +9 -0
  118. package/dist/sdk/diff_review.js +9 -3
  119. package/dist/sdk/diff_review.js.map +1 -1
  120. package/dist/tui/chat_controller/diff_review_service.js +6 -2
  121. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  122. package/dist/tui/chat_controller/diff_review_user_message.js +4 -2
  123. package/dist/tui/chat_controller/diff_review_user_message.js.map +1 -1
  124. package/dist/tui/listen_capture.js +2 -6
  125. package/dist/tui/listen_capture.js.map +1 -1
  126. package/dist/tui/session_chat_controller.js +3 -3
  127. package/dist/tui/session_chat_controller.js.map +1 -1
  128. package/package.json +8 -8
  129. package/dist/core/utils/mistral_transcription.js +0 -44
  130. package/dist/core/utils/mistral_transcription.js.map +0 -1
@@ -1,8 +1,20 @@
1
+ import WebSocket from "ws";
1
2
  import { z } from "zod";
2
3
  import { formatSpeechToTextContext } from "./speech_to_text_context.js";
4
+ import { normalizeSpeechToTextKeywords, SPEECH_TO_TEXT_KEYWORD_INSTRUCTIONS, } from "./speech_to_text_keywords.js";
3
5
  const GEMINI_GENERATE_CONTENT_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
4
- const GEMINI_TRANSCRIPTION_MODEL = "gemini-3.7-flash";
5
- const GEMINI_TRANSCRIPTION_THINKING_LEVEL = "low";
6
+ const GEMINI_FILE_UPLOAD_URL = "https://generativelanguage.googleapis.com/upload/v1beta/files";
7
+ const GEMINI_INTERACTIONS_URL = "https://generativelanguage.googleapis.com/v1beta/interactions";
8
+ const GEMINI_FILES_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
9
+ const GEMINI_LIVE_TRANSCRIPTION_URL = "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent";
10
+ const GEMINI_TRANSCRIPTION_MODEL = "gemini-3.5-transcribe";
11
+ const GEMINI_LIVE_TRANSCRIPTION_MODEL = "gemini-3.5-transcribe-live";
12
+ const GEMINI_TRANSCRIPTION_KEYWORD_MODEL = "gemini-3.7-flash";
13
+ const GEMINI_TRANSCRIPTION_MAX_KEYWORD_CHARACTERS_TOTAL = 10_000;
14
+ const GEMINI_TRANSCRIPTION_KEYWORD_TIMEOUT_MS = 15_000;
15
+ const GEMINI_TRANSCRIPTION_CONNECT_TIMEOUT_MS = 15_000;
16
+ const GEMINI_TRANSCRIPTION_COMPLETION_TIMEOUT_MS = 30_000;
17
+ const GEMINI_FILE_DELETE_TIMEOUT_MS = 5_000;
6
18
  const DEFAULT_GEMINI_AUDIO_MIME_TYPE = "audio/wav";
7
19
  const errorPayloadSchema = z.object({
8
20
  error: z
@@ -11,15 +23,7 @@ const errorPayloadSchema = z.object({
11
23
  })
12
24
  .optional(),
13
25
  });
14
- const GEMINI_TRANSCRIPTION_RESPONSE_SCHEMA = {
15
- type: "OBJECT",
16
- properties: {
17
- transcription: { type: "STRING" },
18
- },
19
- required: ["transcription"],
20
- };
21
- const textPartSchema = z.object({ text: z.string() });
22
- const apiResponseSchema = z.object({
26
+ const generateContentResponseSchema = z.object({
23
27
  candidates: z
24
28
  .array(z.object({
25
29
  content: z.object({
@@ -28,122 +32,531 @@ const apiResponseSchema = z.object({
28
32
  }))
29
33
  .optional(),
30
34
  });
31
- const transcriptionResultSchema = z.object({
32
- transcription: z.string().trim().min(1),
35
+ const textPartSchema = z.object({ text: z.string() });
36
+ const transcriptionKeywordsSchema = z
37
+ .object({
38
+ keywords: z.array(z.string()),
39
+ })
40
+ .strict();
41
+ const uploadedFileSchema = z.object({
42
+ file: z.object({
43
+ name: z.string().trim().min(1),
44
+ uri: z.string().trim().min(1),
45
+ }),
46
+ });
47
+ const interactionSchema = z.object({
48
+ steps: z.array(z.unknown()).optional(),
33
49
  });
50
+ const interactionModelOutputSchema = z.object({
51
+ type: z.literal("model_output"),
52
+ content: z.array(z.unknown()),
53
+ });
54
+ const interactionTextSchema = z.object({
55
+ type: z.literal("text"),
56
+ text: z.string(),
57
+ });
58
+ const liveMessageSchema = z
59
+ .object({
60
+ setupComplete: z.object({}).optional(),
61
+ serverContent: z
62
+ .object({
63
+ inputTranscription: z
64
+ .object({
65
+ text: z.string().trim().min(1),
66
+ })
67
+ .optional(),
68
+ })
69
+ .passthrough()
70
+ .optional(),
71
+ error: z
72
+ .object({
73
+ message: z.string().trim().min(1),
74
+ })
75
+ .optional(),
76
+ })
77
+ .passthrough();
78
+ class GeminiStreamingTranscriptionImpl {
79
+ socket;
80
+ keywordAbortController = new AbortController();
81
+ keywordsPromise;
82
+ ready = false;
83
+ aborted = false;
84
+ failure;
85
+ completedTranscript;
86
+ hasAudio = false;
87
+ pendingAudio = [];
88
+ readyTimeout;
89
+ completionTimeout;
90
+ resolveReady;
91
+ rejectReady;
92
+ resolveCompletion;
93
+ rejectCompletion;
94
+ readyPromise;
95
+ constructor(options) {
96
+ const apiKey = options.apiKey.trim();
97
+ if (!apiKey) {
98
+ throw new Error("missing Gemini API key");
99
+ }
100
+ this.keywordsPromise = prepareGeminiTranscriptionKeywords({
101
+ apiKey,
102
+ context: options.context,
103
+ signal: this.keywordAbortController.signal,
104
+ fetchImpl: options.fetchImpl,
105
+ });
106
+ const webSocketFactory = options.webSocketFactory ?? defaultWebSocketFactory;
107
+ this.socket = webSocketFactory(`${GEMINI_LIVE_TRANSCRIPTION_URL}?key=${encodeURIComponent(apiKey)}`);
108
+ this.readyPromise = new Promise((resolve, reject) => {
109
+ this.resolveReady = resolve;
110
+ this.rejectReady = reject;
111
+ });
112
+ void this.readyPromise.catch(() => { });
113
+ this.readyTimeout = setTimeout(() => {
114
+ this.fail(new Error("timed out opening Gemini transcription session"));
115
+ this.socket.terminate();
116
+ }, GEMINI_TRANSCRIPTION_CONNECT_TIMEOUT_MS);
117
+ this.readyTimeout.unref?.();
118
+ this.socket.on("open", () => {
119
+ if (this.readyTimeout)
120
+ clearTimeout(this.readyTimeout);
121
+ this.readyTimeout = undefined;
122
+ void this.configureSession();
123
+ });
124
+ this.socket.on("message", (data) => this.handleMessage(data));
125
+ this.socket.on("error", (error) => {
126
+ this.fail(new Error(`Gemini transcription connection failed: ${error.message}`));
127
+ });
128
+ this.socket.on("close", (code, reason) => {
129
+ if (this.aborted || this.completedTranscript)
130
+ return;
131
+ const detail = reason.toString("utf8").trim();
132
+ this.fail(new Error(detail
133
+ ? `Gemini transcription connection closed (${code}): ${detail}`
134
+ : `Gemini transcription connection closed (${code})`));
135
+ });
136
+ }
137
+ appendAudio(audio) {
138
+ if (audio.length === 0 || this.aborted || this.failure || this.completedTranscript)
139
+ return;
140
+ this.hasAudio = true;
141
+ if (!this.ready) {
142
+ this.pendingAudio.push(audio);
143
+ return;
144
+ }
145
+ this.sendAudio(audio);
146
+ }
147
+ async finish(options = {}) {
148
+ const abortListener = () => this.abort();
149
+ if (options.signal?.aborted) {
150
+ abortListener();
151
+ }
152
+ else {
153
+ options.signal?.addEventListener("abort", abortListener, { once: true });
154
+ }
155
+ try {
156
+ await this.readyPromise;
157
+ if (this.failure)
158
+ throw this.failure;
159
+ if (this.aborted)
160
+ throw new Error("Gemini transcription was aborted");
161
+ if (this.completedTranscript)
162
+ return this.completedTranscript;
163
+ if (!this.hasAudio)
164
+ throw new Error("Gemini transcription received no audio");
165
+ const completion = new Promise((resolve, reject) => {
166
+ this.resolveCompletion = resolve;
167
+ this.rejectCompletion = reject;
168
+ });
169
+ this.completionTimeout = setTimeout(() => {
170
+ this.fail(new Error("timed out waiting for Gemini transcription"));
171
+ this.socket.terminate();
172
+ }, GEMINI_TRANSCRIPTION_COMPLETION_TIMEOUT_MS);
173
+ this.completionTimeout.unref?.();
174
+ this.send({ realtimeInput: { activityEnd: {} } });
175
+ return await completion;
176
+ }
177
+ finally {
178
+ options.signal?.removeEventListener("abort", abortListener);
179
+ }
180
+ }
181
+ abort() {
182
+ if (this.aborted || this.completedTranscript)
183
+ return;
184
+ const error = new Error("Gemini transcription was aborted");
185
+ this.aborted = true;
186
+ this.pendingAudio = [];
187
+ this.keywordAbortController.abort(error);
188
+ this.clearTimers();
189
+ this.rejectReady?.(error);
190
+ this.rejectCompletion?.(error);
191
+ this.clearWaiters();
192
+ this.socket.close();
193
+ }
194
+ async configureSession() {
195
+ const keywords = await this.keywordsPromise;
196
+ if (this.aborted || this.failure)
197
+ return;
198
+ this.readyTimeout = setTimeout(() => {
199
+ this.fail(new Error("timed out opening Gemini transcription session"));
200
+ this.socket.terminate();
201
+ }, GEMINI_TRANSCRIPTION_CONNECT_TIMEOUT_MS);
202
+ this.readyTimeout.unref?.();
203
+ this.send({
204
+ setup: {
205
+ model: `models/${GEMINI_LIVE_TRANSCRIPTION_MODEL}`,
206
+ generationConfig: {
207
+ responseModalities: ["TEXT"],
208
+ },
209
+ inputAudioTranscription: {
210
+ languageCodes: [],
211
+ ...(keywords.length > 0 ? { customVocabulary: keywords } : {}),
212
+ mode: "SMART",
213
+ },
214
+ realtimeInputConfig: {
215
+ automaticActivityDetection: {
216
+ disabled: true,
217
+ },
218
+ },
219
+ },
220
+ });
221
+ }
222
+ handleMessage(data) {
223
+ let payload;
224
+ try {
225
+ payload = JSON.parse(formatWebSocketMessage(data));
226
+ }
227
+ catch {
228
+ this.fail(new Error("Gemini transcription returned malformed JSON"));
229
+ return;
230
+ }
231
+ const event = liveMessageSchema.safeParse(payload);
232
+ if (!event.success) {
233
+ this.fail(new Error("Gemini transcription returned a malformed event"));
234
+ return;
235
+ }
236
+ if (event.data.error) {
237
+ this.fail(new Error(event.data.error.message));
238
+ return;
239
+ }
240
+ if (event.data.setupComplete && !this.ready) {
241
+ this.ready = true;
242
+ if (this.readyTimeout)
243
+ clearTimeout(this.readyTimeout);
244
+ this.readyTimeout = undefined;
245
+ this.send({ realtimeInput: { activityStart: {} } });
246
+ const pendingAudio = this.pendingAudio;
247
+ this.pendingAudio = [];
248
+ for (const audio of pendingAudio) {
249
+ this.sendAudio(audio);
250
+ }
251
+ if (this.failure)
252
+ return;
253
+ this.resolveReady?.();
254
+ this.resolveReady = undefined;
255
+ this.rejectReady = undefined;
256
+ return;
257
+ }
258
+ const transcript = event.data.serverContent?.inputTranscription?.text;
259
+ if (transcript) {
260
+ this.completedTranscript = transcript;
261
+ this.clearTimers();
262
+ this.resolveCompletion?.(transcript);
263
+ this.clearWaiters();
264
+ this.socket.close();
265
+ }
266
+ }
267
+ sendAudio(audio) {
268
+ this.send({
269
+ realtimeInput: {
270
+ audio: {
271
+ data: audio.toString("base64"),
272
+ mimeType: "audio/pcm;rate=16000",
273
+ },
274
+ },
275
+ });
276
+ }
277
+ send(event) {
278
+ if (this.failure || this.aborted)
279
+ return;
280
+ try {
281
+ this.socket.send(JSON.stringify(event), (error) => {
282
+ if (error) {
283
+ this.fail(new Error(`failed to send Gemini transcription audio: ${error.message}`));
284
+ }
285
+ });
286
+ }
287
+ catch (error) {
288
+ this.fail(new Error(`failed to send Gemini transcription audio: ${error.message}`));
289
+ }
290
+ }
291
+ fail(error) {
292
+ if (this.failure || this.aborted || this.completedTranscript)
293
+ return;
294
+ this.failure = error;
295
+ this.pendingAudio = [];
296
+ this.keywordAbortController.abort(error);
297
+ this.clearTimers();
298
+ this.rejectReady?.(error);
299
+ this.rejectCompletion?.(error);
300
+ this.clearWaiters();
301
+ this.socket.terminate();
302
+ }
303
+ clearTimers() {
304
+ if (this.readyTimeout)
305
+ clearTimeout(this.readyTimeout);
306
+ if (this.completionTimeout)
307
+ clearTimeout(this.completionTimeout);
308
+ this.readyTimeout = undefined;
309
+ this.completionTimeout = undefined;
310
+ }
311
+ clearWaiters() {
312
+ this.resolveReady = undefined;
313
+ this.rejectReady = undefined;
314
+ this.resolveCompletion = undefined;
315
+ this.rejectCompletion = undefined;
316
+ }
317
+ }
318
+ export function startGeminiTranscription(options) {
319
+ return new GeminiStreamingTranscriptionImpl(options);
320
+ }
34
321
  export async function transcribeGeminiAudio(options) {
35
322
  const apiKey = options.apiKey.trim();
36
323
  if (!apiKey) {
37
324
  throw new Error("missing Gemini API key");
38
325
  }
39
326
  const fetchFn = options.fetchImpl ?? fetch;
40
- const response = await fetchFn(`${GEMINI_GENERATE_CONTENT_BASE_URL}/${GEMINI_TRANSCRIPTION_MODEL}:generateContent`, {
41
- method: "POST",
42
- headers: {
43
- "Content-Type": "application/json",
44
- "x-goog-api-key": apiKey,
45
- },
46
- signal: options.signal,
47
- body: JSON.stringify({
48
- systemInstruction: {
49
- parts: [
327
+ const [keywords, uploadedFile] = await Promise.all([
328
+ prepareGeminiTranscriptionKeywords({
329
+ apiKey,
330
+ context: options.context,
331
+ signal: options.signal,
332
+ fetchImpl: fetchFn,
333
+ }),
334
+ uploadGeminiAudio({
335
+ apiKey,
336
+ audio: options.audio,
337
+ mimeType: options.mimeType ?? DEFAULT_GEMINI_AUDIO_MIME_TYPE,
338
+ signal: options.signal,
339
+ fetchImpl: fetchFn,
340
+ }),
341
+ ]);
342
+ try {
343
+ const response = await fetchFn(GEMINI_INTERACTIONS_URL, {
344
+ method: "POST",
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ "x-goog-api-key": apiKey,
348
+ },
349
+ signal: options.signal,
350
+ body: JSON.stringify({
351
+ model: GEMINI_TRANSCRIPTION_MODEL,
352
+ input: [
50
353
  {
51
- text: buildTranscriptionSystemInstruction(),
354
+ type: "audio",
355
+ uri: uploadedFile.uri,
356
+ mime_type: options.mimeType ?? DEFAULT_GEMINI_AUDIO_MIME_TYPE,
52
357
  },
53
358
  ],
359
+ generation_config: {
360
+ transcription_config: {
361
+ language_codes: [],
362
+ ...(keywords.length > 0 ? { custom_vocabulary: keywords } : {}),
363
+ mode: "smart",
364
+ },
365
+ },
366
+ store: false,
367
+ }),
368
+ });
369
+ const { payload, responseText } = await readResponsePayload(response);
370
+ if (!response.ok) {
371
+ throw createResponseError(payload, responseText, response.status);
372
+ }
373
+ const transcript = extractInteractionText(payload)?.trim();
374
+ if (!transcript) {
375
+ throw new Error("transcription result was empty or malformed");
376
+ }
377
+ return transcript;
378
+ }
379
+ finally {
380
+ await deleteGeminiFile({
381
+ apiKey,
382
+ fileName: uploadedFile.name,
383
+ fetchImpl: fetchFn,
384
+ });
385
+ }
386
+ }
387
+ async function prepareGeminiTranscriptionKeywords(args) {
388
+ const formattedContext = formatSpeechToTextContext(args.context);
389
+ if (!formattedContext)
390
+ return [];
391
+ try {
392
+ const signals = [AbortSignal.timeout(GEMINI_TRANSCRIPTION_KEYWORD_TIMEOUT_MS)];
393
+ if (args.signal)
394
+ signals.push(args.signal);
395
+ const response = await (args.fetchImpl ?? fetch)(`${GEMINI_GENERATE_CONTENT_BASE_URL}/${GEMINI_TRANSCRIPTION_KEYWORD_MODEL}:generateContent`, {
396
+ method: "POST",
397
+ headers: {
398
+ "Content-Type": "application/json",
399
+ "x-goog-api-key": args.apiKey,
54
400
  },
55
- contents: [
56
- {
57
- parts: [
58
- {
59
- text: buildTranscriptionPrompt(options.context),
60
- },
61
- {
62
- inlineData: {
63
- mimeType: options.mimeType ?? DEFAULT_GEMINI_AUDIO_MIME_TYPE,
64
- data: options.audio.toString("base64"),
401
+ signal: AbortSignal.any(signals),
402
+ body: JSON.stringify({
403
+ systemInstruction: {
404
+ parts: [{ text: SPEECH_TO_TEXT_KEYWORD_INSTRUCTIONS }],
405
+ },
406
+ contents: [
407
+ {
408
+ parts: [{ text: formattedContext }],
409
+ },
410
+ ],
411
+ generationConfig: {
412
+ responseMimeType: "application/json",
413
+ responseSchema: {
414
+ type: "OBJECT",
415
+ properties: {
416
+ keywords: {
417
+ type: "ARRAY",
418
+ items: { type: "STRING" },
65
419
  },
66
420
  },
67
- ],
68
- },
69
- ],
70
- generationConfig: {
71
- responseMimeType: "application/json",
72
- responseSchema: GEMINI_TRANSCRIPTION_RESPONSE_SCHEMA,
73
- thinkingConfig: {
74
- thinkingLevel: GEMINI_TRANSCRIPTION_THINKING_LEVEL,
421
+ required: ["keywords"],
422
+ },
423
+ thinkingConfig: {
424
+ thinkingLevel: "low",
425
+ },
75
426
  },
76
- },
77
- }),
78
- });
79
- const responseText = await response.text();
80
- let payload;
81
- try {
82
- payload = responseText ? JSON.parse(responseText) : undefined;
427
+ }),
428
+ });
429
+ if (!response.ok) {
430
+ await response.body?.cancel().catch(() => { });
431
+ return [];
432
+ }
433
+ const payload = (await response.json());
434
+ const outputText = extractGenerateContentText(payload);
435
+ const parsedKeywords = transcriptionKeywordsSchema.safeParse(outputText ? JSON.parse(outputText) : undefined);
436
+ return parsedKeywords.success
437
+ ? normalizeSpeechToTextKeywords(parsedKeywords.data.keywords, {
438
+ maxTotalCharacters: GEMINI_TRANSCRIPTION_MAX_KEYWORD_CHARACTERS_TOTAL,
439
+ })
440
+ : [];
83
441
  }
84
442
  catch {
85
- payload = undefined;
443
+ return [];
86
444
  }
87
- if (!response.ok) {
88
- const parsed = errorPayloadSchema.safeParse(payload);
89
- const fallbackMessage = responseText.trim() || `HTTP ${response.status}`;
90
- throw new Error(parsed.success ? (parsed.data.error?.message ?? fallbackMessage) : fallbackMessage);
445
+ }
446
+ async function uploadGeminiAudio(args) {
447
+ const startResponse = await args.fetchImpl(GEMINI_FILE_UPLOAD_URL, {
448
+ method: "POST",
449
+ headers: {
450
+ "Content-Type": "application/json",
451
+ "x-goog-api-key": args.apiKey,
452
+ "X-Goog-Upload-Protocol": "resumable",
453
+ "X-Goog-Upload-Command": "start",
454
+ "X-Goog-Upload-Header-Content-Length": String(args.audio.byteLength),
455
+ "X-Goog-Upload-Header-Content-Type": args.mimeType,
456
+ },
457
+ signal: args.signal,
458
+ body: JSON.stringify({ file: { display_name: "tau-speech" } }),
459
+ });
460
+ if (!startResponse.ok) {
461
+ const { payload, responseText } = await readResponsePayload(startResponse);
462
+ throw createResponseError(payload, responseText, startResponse.status);
91
463
  }
92
- const text = extractGeminiText(payload);
93
- if (!text) {
94
- throw new Error("transcription result was empty or malformed");
464
+ const uploadUrl = startResponse.headers.get("x-goog-upload-url")?.trim();
465
+ await startResponse.body?.cancel().catch(() => { });
466
+ if (!uploadUrl) {
467
+ throw new Error("Gemini file upload did not return an upload URL");
95
468
  }
96
- return text;
97
- }
98
- function buildTranscriptionSystemInstruction() {
99
- return [
100
- "You are a speech-to-text engine.",
101
- "Transcribe the speaker's intended message for insertion into a chat input.",
102
- "Detect the speaker's language and transcribe in that same language; never translate unless the speaker explicitly asks for translation.",
103
- "Preserve the speaker's wording and register as spoken, including colloquial forms, dialect, and informal language; do not normalize informal speech into formal standard language.",
104
- "Use natural punctuation and capitalization where helpful, without changing the speaker's wording or register.",
105
- "Lightly clean only speech artifacts that do not affect meaning, such as filler words, repeated stutters, obvious false starts, and unintelligible mumbling.",
106
- "Do not rewrite, paraphrase, summarize, answer the speaker, add labels, add timestamps, or describe background sounds.",
107
- ].join("\n");
469
+ const uploadResponse = await args.fetchImpl(uploadUrl, {
470
+ method: "POST",
471
+ headers: {
472
+ "Content-Length": String(args.audio.byteLength),
473
+ "Content-Type": args.mimeType,
474
+ "X-Goog-Upload-Offset": "0",
475
+ "X-Goog-Upload-Command": "upload, finalize",
476
+ },
477
+ signal: args.signal,
478
+ body: Uint8Array.from(args.audio),
479
+ });
480
+ const { payload, responseText } = await readResponsePayload(uploadResponse);
481
+ if (!uploadResponse.ok) {
482
+ throw createResponseError(payload, responseText, uploadResponse.status);
483
+ }
484
+ const uploadedFile = uploadedFileSchema.safeParse(payload);
485
+ if (!uploadedFile.success) {
486
+ throw new Error("Gemini file upload returned a malformed response");
487
+ }
488
+ return uploadedFile.data.file;
108
489
  }
109
- function buildTranscriptionPrompt(context) {
110
- const formattedContext = formatSpeechToTextContext(context);
111
- return [
112
- "Transcribe the attached audio into the transcription field.",
113
- "Return only the lightly cleaned transcript text, with no timestamps or commentary.",
114
- formattedContext
115
- ? [
116
- "Use the recent conversation context below only to resolve likely words, names, acronyms, terminology, and references in the audio.",
117
- "Do not transcribe the context itself, and do not add words from the context that were not spoken.",
118
- "If the audio is ambiguous, prefer the transcription that best fits this context.",
119
- "",
120
- "Recent conversation context:",
121
- formattedContext,
122
- ].join("\n")
123
- : undefined,
124
- ]
125
- .filter((line) => line !== undefined)
126
- .join("\n");
490
+ async function deleteGeminiFile(args) {
491
+ try {
492
+ const response = await args.fetchImpl(`${GEMINI_FILES_BASE_URL}/${args.fileName}`, {
493
+ method: "DELETE",
494
+ headers: { "x-goog-api-key": args.apiKey },
495
+ signal: AbortSignal.timeout(GEMINI_FILE_DELETE_TIMEOUT_MS),
496
+ });
497
+ await response.body?.cancel().catch(() => { });
498
+ }
499
+ catch {
500
+ // best-effort remote cleanup
501
+ }
127
502
  }
128
- function extractGeminiText(payload) {
129
- const parsed = apiResponseSchema.safeParse(payload);
130
- if (!parsed.success) {
503
+ function extractGenerateContentText(payload) {
504
+ const parsed = generateContentResponseSchema.safeParse(payload);
505
+ if (!parsed.success)
131
506
  return undefined;
132
- }
133
- const responseText = (parsed.data.candidates?.[0]?.content.parts ?? [])
507
+ return (parsed.data.candidates?.[0]?.content.parts ?? [])
134
508
  .map((part) => {
135
509
  const parsedPart = textPartSchema.safeParse(part);
136
510
  return parsedPart.success ? parsedPart.data.text : "";
137
511
  })
138
512
  .join("");
139
- let transcriptionPayload;
513
+ }
514
+ function extractInteractionText(payload) {
515
+ const parsed = interactionSchema.safeParse(payload);
516
+ if (!parsed.success)
517
+ return undefined;
518
+ return (parsed.data.steps ?? [])
519
+ .flatMap((step) => {
520
+ const modelOutput = interactionModelOutputSchema.safeParse(step);
521
+ return modelOutput.success ? modelOutput.data.content : [];
522
+ })
523
+ .flatMap((content) => {
524
+ const text = interactionTextSchema.safeParse(content);
525
+ return text.success ? [text.data.text] : [];
526
+ })
527
+ .join("");
528
+ }
529
+ async function readResponsePayload(response) {
530
+ const responseText = await response.text();
140
531
  try {
141
- transcriptionPayload = responseText ? JSON.parse(responseText) : undefined;
532
+ return {
533
+ payload: responseText ? JSON.parse(responseText) : undefined,
534
+ responseText,
535
+ };
142
536
  }
143
537
  catch {
144
- return undefined;
538
+ return { payload: undefined, responseText };
145
539
  }
146
- const transcription = transcriptionResultSchema.safeParse(transcriptionPayload);
147
- return transcription.success ? transcription.data.transcription : undefined;
540
+ }
541
+ function createResponseError(payload, responseText, status) {
542
+ const parsed = errorPayloadSchema.safeParse(payload);
543
+ return new Error(parsed.success
544
+ ? (parsed.data.error?.message ?? (responseText.trim() || `HTTP ${status}`))
545
+ : responseText.trim() || `HTTP ${status}`);
546
+ }
547
+ function formatWebSocketMessage(data) {
548
+ if (typeof data === "string")
549
+ return data;
550
+ if (Buffer.isBuffer(data))
551
+ return data.toString("utf8");
552
+ if (data instanceof ArrayBuffer)
553
+ return Buffer.from(data).toString("utf8");
554
+ if (ArrayBuffer.isView(data)) {
555
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
556
+ }
557
+ throw new Error("unsupported WebSocket message");
558
+ }
559
+ function defaultWebSocketFactory(url) {
560
+ return new WebSocket(url);
148
561
  }
149
562
  //# sourceMappingURL=gemini_transcription.js.map