import { existsSync } from "node:fs"; import { writeFile } from "node:fs/promises"; import textToSpeech from "@google-cloud/text-to-speech"; import type { TextToSpeechClient } from "@google-cloud/text-to-speech"; import { config } from "../config.js"; import { BaseEngine, type VoiceParams } from "./BaseEngine.js"; interface GoogleVoiceMeta { name: string; lang: string; } export class GoogleEngine extends BaseEngine { private client: TextToSpeechClient | undefined; protected override voices: Record = {}; constructor() { super("google", "Google Cloud TTS", "wav"); void this.populateVoiceList(); } override getDefaultVoice(): string { return "en-us-wavenet-a"; } private credentialsAvailable(): boolean { return ( !!config.GOOGLE_APPLICATION_CREDENTIALS && existsSync(config.GOOGLE_APPLICATION_CREDENTIALS) ); } private getClient(): TextToSpeechClient { if (!this.credentialsAvailable()) { throw new Error( "Google Cloud TTS unavailable: GOOGLE_APPLICATION_CREDENTIALS must point to a readable file", ); } this.client ??= new textToSpeech.TextToSpeechClient(); return this.client; } private async populateVoiceList(): Promise { if (!this.credentialsAvailable()) return; try { const [result] = await this.getClient().listVoices({}); for (const voice of result.voices ?? []) { if (!voice.name || !voice.languageCodes?.[0]) continue; this.voices[voice.name.toLowerCase()] = { name: voice.name, lang: voice.languageCodes[0], }; } } catch (err) { console.error("Google Cloud TTS: failed to populate voice list:", err); } } override async getSpeechFile( text: string, filepath: string, voice: string = this.getDefaultVoice(), _params: VoiceParams = {}, ): Promise { const meta = this.voices[voice]; if (!meta) throw new Error(`Google: unknown voice "${voice}"`); const [response] = await this.getClient().synthesizeSpeech({ input: { text }, voice: { name: meta.name, languageCode: meta.lang }, audioConfig: { audioEncoding: "LINEAR16" }, }); if (!response.audioContent) { throw new Error("Google: synthesizeSpeech returned no audioContent"); } await writeFile(filepath, response.audioContent); return filepath; } }