src/voice/voiceSoundboard.ts

import { getVoiceSoundboardSettings } from "../settings/agentStack.ts"; import { SOUNDBOARD_MAX_CANDIDATES, dedupeSoundboardCandidates, findMentionedSoundboardReference, matchSoundboardReference, normalizeVoiceText, parsePreferredSoundboardReferences, shortError } from "./voiceSessionHelpers.ts"; import { SOUNDBOARD_CATALOG_REFRESH_MS, SOUNDBOARD_DECISION_TRANSCRIPT_MAX_CHARS } from "./voiceSessionManager.constants.ts"; import type { SoundboardCandidate, VoiceSession, VoiceSessionSoundboardState } from "./voiceSessionTypes.ts";

type VoiceSoundboardSettings = Record<string, unknown> | null;

type VoiceSoundboardStoreLike = { logAction: (entry: { kind: string; guildId?: string | null; channelId?: string | null; userId?: string | null; content: string; metadata?: Record<string, unknown>; }) => void; };

type GuildSoundLike = { available?: boolean; soundId?: string | null; name?: string | null; };

type GuildSoundboardCollectionLike = { forEach: (callback: (sound: GuildSoundLike) => void) => void; };

type GuildSoundboardLike = { soundboardSounds?: { fetch?: () => Promise; }; } | null;

type VoiceSoundboardSessionLike = Pick< VoiceSession, "ending" | "guildId" | "textChannelId" | "id" | "mode" | "settingsSnapshot"

& { soundboard?: VoiceSessionSoundboardState | null; };

type SoundboardPlayResult = { ok: boolean; reason?: string | null; message?: string | null; };

interface VoiceSoundboardHost { client: { user?: { id?: string | null; } | null; guilds: { cache: { get: (guildId: string) => GuildSoundboardLike; }; }; }; store: VoiceSoundboardStoreLike; soundboardDirector: { play: (args: { session: VoiceSoundboardSessionLike; settings: VoiceSoundboardSettings; soundId: string; sourceGuildId?: string | null; reason?: string; }) => Promise; }; }

function ensureSoundboardState(session: VoiceSoundboardSessionLike | null | undefined) { if (!session) return null; session.soundboard = session.soundboard || { playCount: 0, lastPlayedAt: 0, catalogCandidates: [], catalogFetchedAt: 0, lastDirectiveKey: "", lastDirectiveAt: 0 }; return session.soundboard; }

export function normalizeSoundboardRefs(soundboardRefs: unknown[] = []) { return (Array.isArray(soundboardRefs) ? soundboardRefs : []) .map((entry) => String(entry || "") .trim() .slice(0, 180) ) .filter(Boolean) .slice(0, 12); }

export async function maybeTriggerAssistantDirectedSoundboard( host: VoiceSoundboardHost, { session, settings, userId = null, transcript = "", requestedRef = "", source = "voice_transcript" }: { session?: VoiceSoundboardSessionLike | null; settings?: VoiceSoundboardSettings; userId?: string | null; transcript?: string; requestedRef?: string; source?: string; } ) { if (!session || session.ending) return;

const resolvedSettings = settings || session.settingsSnapshot || null; if (!getVoiceSoundboardSettings(resolvedSettings).enabled) return; const normalizedRef = String(requestedRef || "").trim().slice(0, 180); if (!normalizedRef) return;

const normalizedTranscript = normalizeVoiceText(transcript, SOUNDBOARD_DECISION_TRANSCRIPT_MAX_CHARS); const soundboardState = ensureSoundboardState(session); if (!soundboardState) return;

const directiveKey = [ String(source || "voice_transcript").trim().toLowerCase(), normalizedRef.toLowerCase(), String(normalizedTranscript || "").trim().toLowerCase() ].join("|"); const now = Date.now(); if ( directiveKey && directiveKey === String(soundboardState.lastDirectiveKey || "") && now - Number(soundboardState.lastDirectiveAt || 0) < 6_000 ) { return; } soundboardState.lastDirectiveKey = directiveKey; soundboardState.lastDirectiveAt = now;

const candidateInfo = await resolveSoundboardCandidates(host, { session, settings: resolvedSettings }); const candidates = Array.isArray(candidateInfo?.candidates) ? candidateInfo.candidates : []; const candidateSource = String(candidateInfo?.source || "none"); const byReference = matchSoundboardReference(candidates, normalizedRef); const byMention = byReference ? null : findMentionedSoundboardReference(candidates, normalizedRef); const byName = byReference || byMention ? null : candidates.find((entry) => String(entry?.name || "").trim().toLowerCase() === normalizedRef.toLowerCase()) || candidates.find((entry) => String(entry?.name || "") .trim() .toLowerCase() .includes(normalizedRef.toLowerCase()) ); const matched = byReference || byMention || byName || null;

host.store.logAction({ kind: "voice_runtime", guildId: session.guildId, channelId: session.textChannelId, userId: userId || host.client.user?.id || null, content: "voice_soundboard_directive_decision", metadata: { sessionId: session.id, mode: session.mode, source: String(source || "voice_transcript"), transcript: normalizedTranscript || null, requestedRef: normalizedRef, candidateCount: candidates.length, candidateSource, matchedReference: matched?.reference || null } });

if (!matched) return;

const result = await host.soundboardDirector.play({ session, settings: resolvedSettings, soundId: matched.soundId, sourceGuildId: matched.sourceGuildId, reason: assistant_directive_${String(source || "voice_transcript").slice(0, 50)} });

host.store.logAction({ kind: result.ok ? "voice_runtime" : "voice_error", guildId: session.guildId, channelId: session.textChannelId, userId: userId || host.client.user?.id || null, content: result.ok ? "voice_soundboard_directive_played" : "voice_soundboard_directive_failed", metadata: { sessionId: session.id, mode: session.mode, source: String(source || "voice_transcript"), transcript: normalizedTranscript || null, requestedRef: normalizedRef, soundId: matched.soundId, sourceGuildId: matched.sourceGuildId, reason: result.reason || null, error: result.ok ? null : shortError(result.message || "") } }); }

export async function resolveSoundboardCandidates( host: VoiceSoundboardHost, { session = null, settings, guild = null }: { session?: VoiceSoundboardSessionLike | null; settings: VoiceSoundboardSettings; guild?: GuildSoundboardLike; } ) { const preferred = parsePreferredSoundboardReferences(getVoiceSoundboardSettings(settings).preferredSoundIds); if (preferred.length) { return { source: "preferred", candidates: preferred.slice(0, SOUNDBOARD_MAX_CANDIDATES) }; }

const guildCandidates = await fetchGuildSoundboardCandidates(host, { session, guild }); if (guildCandidates.length) { return { source: "guild_catalog", candidates: guildCandidates.slice(0, SOUNDBOARD_MAX_CANDIDATES) }; }

return { source: "none", candidates: [] }; }

export async function fetchGuildSoundboardCandidates( host: VoiceSoundboardHost, { session = null, guild = null }: { session?: VoiceSoundboardSessionLike | null; guild?: GuildSoundboardLike; } = {} ) { if (session && session.ending) return []; const now = Date.now();

let cached: SoundboardCandidate[] = []; const soundboardState = ensureSoundboardState(session); if (soundboardState) { cached = Array.isArray(soundboardState.catalogCandidates) ? soundboardState.catalogCandidates.filter(Boolean) : []; const lastFetchedAt = Number(soundboardState.catalogFetchedAt || 0); if (lastFetchedAt > 0 && now - lastFetchedAt < SOUNDBOARD_CATALOG_REFRESH_MS) { return cached; } }

const resolvedGuild = guild || host.client.guilds.cache.get(String(session?.guildId || "")); if (!resolvedGuild?.soundboardSounds?.fetch) { return cached; }

try { const fetched = await resolvedGuild.soundboardSounds.fetch(); const candidates: SoundboardCandidate[] = []; fetched.forEach((sound) => { if (!sound || sound.available === false) return; const soundId = String(sound.soundId || "").trim(); if (!soundId) return; const name = String(sound.name || "").trim(); candidates.push({ soundId, sourceGuildId: null, reference: soundId, name: name || null, origin: "guild_catalog" }); });

const deduped = dedupeSoundboardCandidates(candidates).slice(0, SOUNDBOARD_MAX_CANDIDATES);
if (soundboardState) {
  soundboardState.catalogCandidates = deduped;
  soundboardState.catalogFetchedAt = now;
}
return deduped;

} catch (error) { if (session && soundboardState) { host.store.logAction({ kind: "voice_error", guildId: session.guildId, channelId: session.textChannelId, userId: host.client.user?.id || null, content: voice_soundboard_catalog_fetch_failed: ${String((error as Error)?.message || error)}, metadata: { sessionId: session.id } }); soundboardState.catalogFetchedAt = now; } return cached; } }