@markusylisiurunen/tau 0.3.64 → 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.
- package/dist/core/config/schema.js +2 -2
- package/dist/core/config/schema.js.map +1 -1
- package/dist/core/diff_review/snapshot.js +13 -9
- package/dist/core/diff_review/snapshot.js.map +1 -1
- package/dist/core/static/tau_docs/config-reference.md +3 -3
- package/dist/core/static/tau_docs/configuration.md +1 -1
- package/dist/core/static/tau_docs/credentials.md +2 -2
- package/dist/core/static/tau_docs/sdk-diff-review.md +1 -1
- package/dist/core/static/tau_docs/telegram.md +2 -3
- package/dist/core/static/tau_docs/troubleshooting.md +2 -2
- package/dist/core/static/tau_docs/tui.md +4 -2
- package/dist/core/telegram/adapter.js +0 -7
- package/dist/core/telegram/adapter.js.map +1 -1
- package/dist/core/telegram/cli.js +1 -3
- package/dist/core/telegram/cli.js.map +1 -1
- package/dist/core/telegram/runtime.js +0 -1
- package/dist/core/telegram/runtime.js.map +1 -1
- package/dist/core/utils/gemini_transcription.js +510 -97
- package/dist/core/utils/gemini_transcription.js.map +1 -1
- package/dist/core/utils/openai_transcription.js +6 -34
- package/dist/core/utils/openai_transcription.js.map +1 -1
- package/dist/core/utils/speech_to_text.js +23 -14
- package/dist/core/utils/speech_to_text.js.map +1 -1
- package/dist/core/utils/speech_to_text_keywords.js +32 -0
- package/dist/core/utils/speech_to_text_keywords.js.map +1 -0
- package/dist/core/version.js +2 -2
- package/dist/tui/listen_capture.js +2 -6
- package/dist/tui/listen_capture.js.map +1 -1
- package/dist/tui/session_chat_controller.js +3 -3
- package/dist/tui/session_chat_controller.js.map +1 -1
- package/package.json +8 -8
- package/dist/core/utils/mistral_transcription.js +0 -44
- 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
|
|
5
|
-
const
|
|
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
|
|
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
|
|
32
|
-
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
payload =
|
|
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
|
-
|
|
443
|
+
return [];
|
|
86
444
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
129
|
-
const parsed =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|