diff --git a/src/modules/ttsSettings.ts b/src/modules/ttsSettings.ts index ad9be38..2dd6991 100644 --- a/src/modules/ttsSettings.ts +++ b/src/modules/ttsSettings.ts @@ -1,4 +1,5 @@ import { join } from "node:path"; +import { AttachmentBuilder } from "discord.js"; import { respond } from "../audio/AudioService.js"; import type { Module } from "./types.js"; @@ -36,5 +37,32 @@ export const ttsSettings: Module = ({ audio, commands, tts, t, rootDir }) => { } }); + commands.register("lsvoices", (args, message) => { + if (args.length > 1) { + respond(audio, sysmsg, message, t("TOO_MANY_ARGUMENTS")); + return; + } + const sections: string[] = []; + for (const name of tts.list()) { + const engine = tts.get(name); + if (!engine) continue; + const lines = [`=== ${name} — ${engine.longName} ===`, `default: ${engine.getDefaultVoice()}`]; + if (engine.isFreeformVoice()) { + lines.push(`(accepts any voice string — see ${engine.longName} documentation)`); + } else { + const voices = engine.listVoices(); + lines.push(`voices (${voices.length}):`); + for (const v of voices) lines.push(` ${v}`); + } + sections.push(lines.join("\n")); + } + const body = sections.join("\n\n") + "\n"; + const file = new AttachmentBuilder(Buffer.from(body, "utf8"), { name: "voices.txt" }); + void message.reply({ + content: `Available TTS engines and voices (${tts.list().length} engines):`, + files: [file], + }); + }); + commands.register("flush", () => audio.queue.flush()); }; diff --git a/src/tts/BaseEngine.ts b/src/tts/BaseEngine.ts index 58dd506..f435289 100644 --- a/src/tts/BaseEngine.ts +++ b/src/tts/BaseEngine.ts @@ -37,6 +37,16 @@ export abstract class BaseEngine { return this.voices[voice] != null; } + /** Returns sorted lowercase voice names known to this engine. Empty if the engine accepts any voice. */ + listVoices(): string[] { + return Object.keys(this.voices).sort(); + } + + /** True when this engine has no static voice table and accepts any voice string. */ + isFreeformVoice(): boolean { + return Object.keys(this.voices).length === 0; + } + /** 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`);