feat: implement tier-a4 tts voice output replies

This commit is contained in:
William Valentin
2026-02-18 10:22:28 -08:00
parent 3eb07875f1
commit a71aa5992d
11 changed files with 482 additions and 4 deletions
+88
View File
@@ -0,0 +1,88 @@
import type { OutboundAttachment } from '../channels/types.js';
export type TtsOutputFormat = 'mp3' | 'wav' | 'opus';
export interface TtsSynthesisConfig {
endpoint?: string;
apiKey?: string;
model?: string;
voice?: string;
format?: TtsOutputFormat;
}
function outputFormatToMimeType(format: TtsOutputFormat): string {
switch (format) {
case 'wav':
return 'audio/wav';
case 'opus':
return 'audio/ogg';
case 'mp3':
default:
return 'audio/mpeg';
}
}
function outputFormatToExtension(format: TtsOutputFormat): string {
switch (format) {
case 'wav':
return 'wav';
case 'opus':
return 'ogg';
case 'mp3':
default:
return 'mp3';
}
}
/** Synthesize speech via an OpenAI-compatible /v1/audio/speech endpoint. */
export async function synthesizeSpeechAttachment(
text: string,
config: TtsSynthesisConfig,
): Promise<OutboundAttachment | null> {
const trimmed = text.trim();
if (!trimmed) {
return null;
}
if (!config.endpoint) {
return null;
}
const format = config.format ?? 'mp3';
const model = config.model ?? 'gpt-4o-mini-tts';
const voice = config.voice ?? 'alloy';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (config.apiKey) {
headers.Authorization = `Bearer ${config.apiKey}`;
}
const response = await fetch(config.endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model,
voice,
input: trimmed,
response_format: format,
}),
});
if (!response.ok) {
const detail = await response.text().catch(() => '');
throw new Error(
`TTS request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 200)}` : ''}`,
);
}
const audioBytes = await response.arrayBuffer();
const data = Buffer.from(audioBytes).toString('base64');
const extension = outputFormatToExtension(format);
return {
mimeType: outputFormatToMimeType(format),
data,
filename: `flynn-reply-${Date.now()}.${extension}`,
};
}