import { writeFile } from "node:fs/promises"; export type VoiceParams = Record; /** * Common contract for every TTS provider. Subclasses override either * `getSpeech` (returning a fetch-like Response) or `getSpeechFile` (writing * directly to disk) — the default `getSpeechFile` pipes `getSpeech` to a file. */ export abstract class BaseEngine { /** Short ID used in env / commands (e.g. "azure"). */ readonly shortName: string; /** Human-readable name shown in messages. */ readonly longName: string; /** Output file extension without the dot (e.g. "mp3"). */ readonly fileExtension: string; protected voices: Record = {}; constructor(shortName: string, longName: string, fileExtension: string) { this.shortName = shortName; this.longName = longName; this.fileExtension = fileExtension; } /** Maps a user-friendly voice name to the provider's internal identifier. */ getInternalVoiceName(str: string): string { const v = this.voices[str]; if (v == null) return str; return typeof v === "string" ? v : v.name; } abstract getDefaultVoice(): string; validateVoice(voice: string): boolean { if (Object.keys(this.voices).length === 0) return true; return this.voices[voice] != null; } /** Default implementation: subclass should override either this or getSpeechFile. */ async getSpeech(_text: string, _voice?: string, _params?: VoiceParams): Promise { throw new Error(`${this.shortName}: getSpeech not implemented`); } async getSpeechFile( text: string, filepath: string, voice: string = this.getDefaultVoice(), params: VoiceParams = {}, ): Promise { const data = await this.getSpeech(text, voice, params); const buf = Buffer.from(await data.arrayBuffer()); await writeFile(filepath, buf); return filepath; } }