feat: default TTS model overrides on (#1559) (thanks @Glucksberg)

Co-authored-by: Glucksberg <80581902+Glucksberg@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-24 09:40:18 +00:00
parent 4074fa0471
commit 6765fd15eb
11 changed files with 1187 additions and 290 deletions

View File

@@ -273,80 +273,26 @@ function buildChatCommands(): ChatCommandDefinition[] {
argsMenu: "auto",
}),
defineChatCommand({
key: "audio",
nativeName: "audio",
description: "Convert text to a TTS audio reply.",
textAlias: "/audio",
key: "tts",
nativeName: "tts",
description: "Control text-to-speech (TTS).",
textAlias: "/tts",
args: [
{
name: "text",
description: "Text to speak",
name: "action",
description: "on | off | status | provider | limit | summary | audio | help",
type: "string",
choices: ["on", "off", "status", "provider", "limit", "summary", "audio", "help"],
},
{
name: "value",
description: "Provider, limit, or text",
type: "string",
captureRemaining: true,
},
],
}),
defineChatCommand({
key: "tts_on",
nativeName: "tts_on",
description: "Enable text-to-speech for replies.",
textAlias: "/tts_on",
}),
defineChatCommand({
key: "tts_off",
nativeName: "tts_off",
description: "Disable text-to-speech for replies.",
textAlias: "/tts_off",
}),
defineChatCommand({
key: "tts_provider",
nativeName: "tts_provider",
description: "Set or show the TTS provider.",
textAlias: "/tts_provider",
args: [
{
name: "provider",
description: "openai or elevenlabs",
type: "string",
choices: ["openai", "elevenlabs"],
},
],
argsMenu: "auto",
}),
defineChatCommand({
key: "tts_limit",
nativeName: "tts_limit",
description: "Set or show the max TTS text length.",
textAlias: "/tts_limit",
args: [
{
name: "maxLength",
description: "Max chars before summarizing",
type: "number",
},
],
}),
defineChatCommand({
key: "tts_summary",
nativeName: "tts_summary",
description: "Enable or disable TTS auto-summary.",
textAlias: "/tts_summary",
args: [
{
name: "mode",
description: "on or off",
type: "string",
choices: ["on", "off"],
},
],
argsMenu: "auto",
}),
defineChatCommand({
key: "tts_status",
nativeName: "tts_status",
description: "Show TTS status and last attempt.",
textAlias: "/tts_status",
}),
defineChatCommand({
key: "stop",
nativeName: "stop",

View File

@@ -18,22 +18,39 @@ import {
textToSpeech,
} from "../../tts/tts.js";
function parseCommandArg(normalized: string, command: string): string | null {
if (normalized === command) return "";
if (normalized.startsWith(`${command} `)) return normalized.slice(command.length).trim();
return null;
type ParsedTtsCommand = {
action: string;
args: string;
};
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
// Accept `/tts` and `/tts <action> [args]` as a single control surface.
if (normalized === "/tts") return { action: "status", args: "" };
if (!normalized.startsWith("/tts ")) return null;
const rest = normalized.slice(5).trim();
if (!rest) return { action: "status", args: "" };
const [action, ...tail] = rest.split(/\s+/);
return { action: action.toLowerCase(), args: tail.join(" ").trim() };
}
function ttsUsage(): ReplyPayload {
// Keep usage in one place so help/validation stays consistent.
return {
text:
"⚙️ Usage: /tts <on|off|status|provider|limit|summary|audio> [value]" +
"\nExamples:\n" +
"/tts on\n" +
"/tts provider openai\n" +
"/tts limit 2000\n" +
"/tts summary off\n" +
"/tts audio Hello from Clawdbot",
};
}
export const handleTtsCommands: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
const normalized = params.command.commandBodyNormalized;
if (
!normalized.startsWith("/tts_") &&
normalized !== "/audio" &&
!normalized.startsWith("/audio ")
) {
return null;
}
const parsed = parseTtsCommand(params.command.commandBodyNormalized);
if (!parsed) return null;
if (!params.command.isAuthorizedSender) {
logVerbose(
@@ -44,36 +61,42 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
const config = resolveTtsConfig(params.cfg);
const prefsPath = resolveTtsPrefsPath(config);
const action = parsed.action;
const args = parsed.args;
if (normalized === "/tts_on") {
if (action === "help") {
return { shouldContinue: false, reply: ttsUsage() };
}
if (action === "on") {
setTtsEnabled(prefsPath, true);
return { shouldContinue: false, reply: { text: "🔊 TTS enabled." } };
}
if (normalized === "/tts_off") {
if (action === "off") {
setTtsEnabled(prefsPath, false);
return { shouldContinue: false, reply: { text: "🔇 TTS disabled." } };
}
const audioArg = parseCommandArg(normalized, "/audio");
if (audioArg !== null) {
if (!audioArg.trim()) {
return { shouldContinue: false, reply: { text: "⚙️ Usage: /audio <text>" } };
if (action === "audio") {
if (!args.trim()) {
return { shouldContinue: false, reply: ttsUsage() };
}
const start = Date.now();
const result = await textToSpeech({
text: audioArg,
text: args,
cfg: params.cfg,
channel: params.command.channel,
prefsPath,
});
if (result.success && result.audioPath) {
// Store last attempt for `/tts status`.
setLastTtsAttempt({
timestamp: Date.now(),
success: true,
textLength: audioArg.length,
textLength: args.length,
summarized: false,
provider: result.provider,
latencyMs: result.latencyMs,
@@ -85,10 +108,11 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
return { shouldContinue: false, reply: payload };
}
// Store failure details for `/tts status`.
setLastTtsAttempt({
timestamp: Date.now(),
success: false,
textLength: audioArg.length,
textLength: args.length,
summarized: false,
error: result.error,
latencyMs: Date.now() - start,
@@ -99,10 +123,9 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
};
}
const providerArg = parseCommandArg(normalized, "/tts_provider");
if (providerArg !== null) {
if (action === "provider") {
const currentProvider = getTtsProvider(config, prefsPath);
if (!providerArg.trim()) {
if (!args.trim()) {
const fallback = currentProvider === "openai" ? "elevenlabs" : "openai";
const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai"));
const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs"));
@@ -115,17 +138,14 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
`Fallback: ${fallback}\n` +
`OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` +
`ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` +
`Usage: /tts_provider openai | elevenlabs`,
`Usage: /tts provider openai | elevenlabs`,
},
};
}
const requested = providerArg.trim().toLowerCase();
const requested = args.trim().toLowerCase();
if (requested !== "openai" && requested !== "elevenlabs") {
return {
shouldContinue: false,
reply: { text: "⚙️ Usage: /tts_provider openai | elevenlabs" },
};
return { shouldContinue: false, reply: ttsUsage() };
}
setTtsProvider(prefsPath, requested);
@@ -136,21 +156,17 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
};
}
const limitArg = parseCommandArg(normalized, "/tts_limit");
if (limitArg !== null) {
if (!limitArg.trim()) {
if (action === "limit") {
if (!args.trim()) {
const currentLimit = getTtsMaxLength(prefsPath);
return {
shouldContinue: false,
reply: { text: `📏 TTS limit: ${currentLimit} characters.` },
};
}
const next = Number.parseInt(limitArg.trim(), 10);
const next = Number.parseInt(args.trim(), 10);
if (!Number.isFinite(next) || next < 100 || next > 10_000) {
return {
shouldContinue: false,
reply: { text: "⚙️ Usage: /tts_limit <100-10000>" },
};
return { shouldContinue: false, reply: ttsUsage() };
}
setTtsMaxLength(prefsPath, next);
return {
@@ -159,18 +175,17 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
};
}
const summaryArg = parseCommandArg(normalized, "/tts_summary");
if (summaryArg !== null) {
if (!summaryArg.trim()) {
if (action === "summary") {
if (!args.trim()) {
const enabled = isSummarizationEnabled(prefsPath);
return {
shouldContinue: false,
reply: { text: `📝 TTS auto-summary: ${enabled ? "on" : "off"}.` },
};
}
const requested = summaryArg.trim().toLowerCase();
const requested = args.trim().toLowerCase();
if (requested !== "on" && requested !== "off") {
return { shouldContinue: false, reply: { text: "⚙️ Usage: /tts_summary on|off" } };
return { shouldContinue: false, reply: ttsUsage() };
}
setSummarizationEnabled(prefsPath, requested === "on");
return {
@@ -181,7 +196,7 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
};
}
if (normalized === "/tts_status") {
if (action === "status") {
const enabled = isTtsEnabled(config, prefsPath);
const provider = getTtsProvider(config, prefsPath);
const hasKey = Boolean(resolveTtsApiKey(config, provider));
@@ -210,5 +225,5 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
return { shouldContinue: false, reply: { text: lines.join("\n") } };
}
return null;
return { shouldContinue: false, reply: ttsUsage() };
};