Files
OmniRoute/open-sse/executors/geminiTts.ts
Markus Hartung 04dba0460e fix(responses-continuation): recover a real id/output for passthrough and translate-mode replies (#11434)
Retargetado para release/v3.8.51 (release/v3.8.50 está congelada — freeze issue #11439). Validado em lote combinado (batch-0824h2, junto de #11435/#11436/#11437) contra o tip de release/v3.8.51: typecheck:core limpo, gates estáticos OK, 127/127 testes focados passando.

Investigação sólida com repro real via container isolado, três causas independentes identificadas e corrigidas com testes de regressão dedicados para cada uma. Obrigado pela contribuição!
2026-08-24 19:57:12 -03:00

82 lines
2.7 KiB
TypeScript

import { Buffer } from "node:buffer";
import { extractInlineAudio, parsePcmSampleRate, pcmToWav } from "./vertexMedia.ts";
import { CORS_HEADERS } from "../utils/cors.ts";
import { upstreamErrorResponse } from "../utils/audioResponse.ts";
import { errorResponse } from "../utils/error.ts";
type GeminiTtsCredentials = {
apiKey?: string | null;
accessToken?: string | null;
};
export class GeminiTtsUpstreamError extends Error {
constructor(
public readonly response: Response,
public readonly body: string
) {
super(`Gemini TTS upstream error (${response.status})`);
}
}
export async function geminiGenerateSpeech(
credentials: GeminiTtsCredentials,
options: { model: string; text: string; voice: string }
): Promise<Buffer> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (credentials.apiKey) {
headers["x-goog-api-key"] = credentials.apiKey;
} else if (credentials.accessToken) {
headers.Authorization = `Bearer ${credentials.accessToken}`;
}
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(options.model)}:generateContent`,
{
method: "POST",
headers,
body: JSON.stringify({
contents: [{ parts: [{ text: options.text }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: options.voice },
},
},
},
}),
}
);
if (!response.ok) {
throw new GeminiTtsUpstreamError(response, await response.text());
}
const inline = extractInlineAudio(await response.json());
if (!inline) throw new Error("Gemini TTS response did not contain audio data");
return pcmToWav(Buffer.from(inline.base64, "base64"), parsePcmSampleRate(inline.mimeType));
}
export async function handleGeminiTtsSpeech(
credentials: GeminiTtsCredentials,
options: { model: string; text: string; voice?: unknown }
): Promise<Response> {
try {
const wav = await geminiGenerateSpeech(credentials, {
model: options.model,
text: options.text,
voice:
typeof options.voice === "string" && options.voice.trim() ? options.voice.trim() : "Kore",
});
return new Response(new Uint8Array(wav), {
status: 200,
headers: { ...CORS_HEADERS, "Content-Type": "audio/wav" },
});
} catch (error) {
if (error instanceof GeminiTtsUpstreamError) {
return upstreamErrorResponse(error.response, error.body);
}
const message = error instanceof Error ? error.message : String(error);
return errorResponse(500, `Speech request failed: ${message}`);
}
}