fix: polish tts auto mode + tests (#1667) (thanks @sebslight)

This commit is contained in:
Peter Steinberger 2026-01-25 04:27:43 +00:00
parent 2c1be8af4b
commit 32d370e92e
8 changed files with 41 additions and 17 deletions

View File

@ -10,6 +10,7 @@ Docs: https://docs.clawd.bot
### Changes ### Changes
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts - TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.clawd.bot/tts
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround). - Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
- Docs: add verbose installer troubleshooting guidance. - Docs: add verbose installer troubleshooting guidance.
- Docs: update Fly.io guide notes. - Docs: update Fly.io guide notes.

View File

@ -76,13 +76,16 @@ export const handleTtsCommands: CommandHandler = async (params, allowTextCommand
action === "on" ? "always" : action === "off" ? "off" : action, action === "on" ? "always" : action === "off" ? "off" : action,
); );
if (requestedAuto) { if (requestedAuto) {
if (params.sessionEntry && params.sessionStore && params.sessionKey) { const entry = params.sessionEntry;
params.sessionEntry.ttsAuto = requestedAuto; const sessionKey = params.sessionKey;
params.sessionEntry.updatedAt = Date.now(); const store = params.sessionStore;
params.sessionStore[params.sessionKey] = params.sessionEntry; if (entry && store && sessionKey) {
entry.ttsAuto = requestedAuto;
entry.updatedAt = Date.now();
store[sessionKey] = entry;
if (params.storePath) { if (params.storePath) {
await updateSessionStore(params.storePath, (store) => { await updateSessionStore(params.storePath, (store) => {
store[params.sessionKey] = params.sessionEntry; store[sessionKey] = entry;
}); });
} }
} }

View File

@ -5,6 +5,7 @@ import path from "node:path";
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent"; import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js";
import type { ClawdbotConfig } from "../../config/config.js"; import type { ClawdbotConfig } from "../../config/config.js";
import type { TtsAutoMode } from "../../config/types.tts.js";
import { import {
DEFAULT_RESET_TRIGGERS, DEFAULT_RESET_TRIGGERS,
deriveSessionMetaPatch, deriveSessionMetaPatch,
@ -128,7 +129,7 @@ export async function initSessionState(params: {
let persistedThinking: string | undefined; let persistedThinking: string | undefined;
let persistedVerbose: string | undefined; let persistedVerbose: string | undefined;
let persistedReasoning: string | undefined; let persistedReasoning: string | undefined;
let persistedTtsAuto: string | undefined; let persistedTtsAuto: TtsAutoMode | undefined;
let persistedModelOverride: string | undefined; let persistedModelOverride: string | undefined;
let persistedProviderOverride: string | undefined; let persistedProviderOverride: string | undefined;

View File

@ -57,7 +57,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
if (typeof tts.enabled !== "boolean") return; if (typeof tts.enabled !== "boolean") return;
tts.auto = tts.enabled ? "always" : "off"; tts.auto = tts.enabled ? "always" : "off";
delete tts.enabled; delete tts.enabled;
changes.push(`Moved messages.tts.enabled → messages.tts.auto (${tts.auto}).`); changes.push(`Moved messages.tts.enabled → messages.tts.auto (${String(tts.auto)}).`);
}, },
}, },
{ {

View File

@ -134,8 +134,9 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
threadParentType === ChannelType.GuildForum || threadParentType === ChannelType.GuildMedia; threadParentType === ChannelType.GuildForum || threadParentType === ChannelType.GuildMedia;
const forumParentSlug = const forumParentSlug =
isForumParent && threadParentName ? normalizeDiscordSlug(threadParentName) : ""; isForumParent && threadParentName ? normalizeDiscordSlug(threadParentName) : "";
const threadChannelId = threadChannel?.id;
const isForumStarter = const isForumStarter =
Boolean(threadChannel && isForumParent && forumParentSlug) && message.id === threadChannel.id; Boolean(threadChannelId && isForumParent && forumParentSlug) && message.id === threadChannelId;
const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null; const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null;
const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined; const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
const groupSubject = isDirectMessage ? undefined : groupChannel; const groupSubject = isDirectMessage ? undefined : groupChannel;

View File

@ -306,7 +306,7 @@ function resolveTtsAutoModeFromPrefs(prefs: TtsUserPrefs): TtsAutoMode | undefin
export function resolveTtsAutoMode(params: { export function resolveTtsAutoMode(params: {
config: ResolvedTtsConfig; config: ResolvedTtsConfig;
prefsPath: string; prefsPath: string;
sessionAuto?: TtsAutoMode | string; sessionAuto?: string;
}): TtsAutoMode { }): TtsAutoMode {
const sessionAuto = normalizeTtsAutoMode(params.sessionAuto); const sessionAuto = normalizeTtsAutoMode(params.sessionAuto);
if (sessionAuto) return sessionAuto; if (sessionAuto) return sessionAuto;
@ -372,7 +372,7 @@ function updatePrefs(prefsPath: string, update: (prefs: TtsUserPrefs) => void):
export function isTtsEnabled( export function isTtsEnabled(
config: ResolvedTtsConfig, config: ResolvedTtsConfig,
prefsPath: string, prefsPath: string,
sessionAuto?: TtsAutoMode | string, sessionAuto?: string,
): boolean { ): boolean {
return resolveTtsAutoMode({ config, prefsPath, sessionAuto }) !== "off"; return resolveTtsAutoMode({ config, prefsPath, sessionAuto }) !== "off";
} }
@ -1216,7 +1216,7 @@ export async function maybeApplyTtsToPayload(params: {
channel?: string; channel?: string;
kind?: "tool" | "block" | "final"; kind?: "tool" | "block" | "final";
inboundAudio?: boolean; inboundAudio?: boolean;
ttsAuto?: TtsAutoMode | string; ttsAuto?: string;
}): Promise<ReplyPayload> { }): Promise<ReplyPayload> {
const config = resolveTtsConfig(params.cfg); const config = resolveTtsConfig(params.cfg);
const prefsPath = resolveTtsPrefsPath(config); const prefsPath = resolveTtsPrefsPath(config);

18
src/types/node-edge-tts.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
declare module "node-edge-tts" {
export type EdgeTTSOptions = {
voice?: string;
lang?: string;
outputFormat?: string;
saveSubtitles?: boolean;
proxy?: string;
rate?: string;
pitch?: string;
volume?: string;
timeout?: number;
};
export class EdgeTTS {
constructor(options?: EdgeTTSOptions);
ttsPromise(text: string, outputPath: string): Promise<void>;
}
}

View File

@ -127,9 +127,9 @@ describe("web inbound media saves with extension", () => {
realSock.ev.emit("messages.upsert", upsert); realSock.ev.emit("messages.upsert", upsert);
// Allow a brief window for the async handler to fire on slower hosts. // Allow a brief window for the async handler to fire on slower hosts.
for (let i = 0; i < 10; i++) { for (let i = 0; i < 50; i++) {
if (onMessage.mock.calls.length > 0) break; if (onMessage.mock.calls.length > 0) break;
await new Promise((resolve) => setTimeout(resolve, 5)); await new Promise((resolve) => setTimeout(resolve, 10));
} }
expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledTimes(1);
@ -178,9 +178,9 @@ describe("web inbound media saves with extension", () => {
realSock.ev.emit("messages.upsert", upsert); realSock.ev.emit("messages.upsert", upsert);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 50; i++) {
if (onMessage.mock.calls.length > 0) break; if (onMessage.mock.calls.length > 0) break;
await new Promise((resolve) => setTimeout(resolve, 5)); await new Promise((resolve) => setTimeout(resolve, 10));
} }
expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledTimes(1);
@ -218,9 +218,9 @@ describe("web inbound media saves with extension", () => {
realSock.ev.emit("messages.upsert", upsert); realSock.ev.emit("messages.upsert", upsert);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 50; i++) {
if (onMessage.mock.calls.length > 0) break; if (onMessage.mock.calls.length > 0) break;
await new Promise((resolve) => setTimeout(resolve, 5)); await new Promise((resolve) => setTimeout(resolve, 10));
} }
expect(onMessage).toHaveBeenCalledTimes(1); expect(onMessage).toHaveBeenCalledTimes(1);