79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
|
|
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<string, GoogleVoiceMeta> = {};
|
||
|
|
|
||
|
|
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<void> {
|
||
|
|
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<string> {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|