diff --git a/extensions/discord/src/voice/log-preview.test.ts b/extensions/discord/src/voice/log-preview.test.ts new file mode 100644 index 00000000000..04e01161312 --- /dev/null +++ b/extensions/discord/src/voice/log-preview.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; +import { formatVoiceLogPreview } from "./log-preview.js"; + +describe("formatVoiceLogPreview", () => { + it("collapses whitespace and trims the preview", () => { + expect(formatVoiceLogPreview(" hello \n world\t")).toBe("hello world"); + }); + + it("truncates long previews after 500 characters", () => { + const preview = formatVoiceLogPreview("x".repeat(501)); + expect(preview).toBe(`${"x".repeat(500)}...`); + }); +}); diff --git a/extensions/discord/src/voice/log-preview.ts b/extensions/discord/src/voice/log-preview.ts new file mode 100644 index 00000000000..dccb618343a --- /dev/null +++ b/extensions/discord/src/voice/log-preview.ts @@ -0,0 +1,9 @@ +const DISCORD_VOICE_LOG_PREVIEW_CHARS = 500; + +export function formatVoiceLogPreview(text: string): string { + const oneLine = text.replace(/\s+/g, " ").trim(); + if (oneLine.length <= DISCORD_VOICE_LOG_PREVIEW_CHARS) { + return oneLine; + } + return `${oneLine.slice(0, DISCORD_VOICE_LOG_PREVIEW_CHARS)}...`; +} diff --git a/extensions/discord/src/voice/manager.ts b/extensions/discord/src/voice/manager.ts index 6946caa4e6c..76d3355e7c8 100644 --- a/extensions/discord/src/voice/manager.ts +++ b/extensions/discord/src/voice/manager.ts @@ -35,6 +35,7 @@ import { resolveDiscordVoiceIngressContext, runDiscordVoiceAgentTurn, } from "./ingress.js"; +import { formatVoiceLogPreview } from "./log-preview.js"; import { DiscordRealtimeVoiceSession, type DiscordVoiceMode, @@ -67,7 +68,6 @@ import { import { DiscordVoiceSpeakerContextResolver } from "./speaker-context.js"; const logger = createSubsystemLogger("discord/voice"); -const VOICE_LOG_PREVIEW_CHARS = 500; const FOLLOW_USERS_RECONCILE_INTERVAL_MS = 10_000; const FOLLOW_USERS_RECONCILE_MAX_GUILDS_PER_RUN = 4; const FOLLOW_USERS_RECONCILE_MAX_REST_LOOKUPS_PER_RUN = 32; @@ -96,14 +96,6 @@ type VoiceChannelResidency = { channelId: string; }; -function formatVoiceLogPreview(text: string): string { - const oneLine = text.replace(/\s+/g, " ").trim(); - if (oneLine.length <= VOICE_LOG_PREVIEW_CHARS) { - return oneLine; - } - return `${oneLine.slice(0, VOICE_LOG_PREVIEW_CHARS)}...`; -} - function isVoiceConnectionDestroyed( connection: DiscordVoiceConnection, voiceSdk: DiscordVoiceSdk, diff --git a/extensions/discord/src/voice/realtime.ts b/extensions/discord/src/voice/realtime.ts index 04b00d447ba..7e09d9d3efa 100644 --- a/extensions/discord/src/voice/realtime.ts +++ b/extensions/discord/src/voice/realtime.ts @@ -51,6 +51,7 @@ import { convertDiscordPcm48kStereoToRealtimePcm24kMono, convertRealtimePcm24kMonoToDiscordPcm48kStereo, } from "./audio.js"; +import { formatVoiceLogPreview } from "./log-preview.js"; import { formatVoiceIngressPrompt } from "./prompt.js"; import { loadDiscordVoiceSdk } from "./sdk-runtime.js"; import { @@ -81,7 +82,6 @@ const DISCORD_REALTIME_RECENT_AGENT_PROXY_CONSULT_LIMIT = 16; const DISCORD_REALTIME_RECENT_AGENT_PROXY_CONSULT_TTL_MS = 15_000; const DISCORD_REALTIME_IGNORED_WAKE_NAME_CONTEXT_TTL_MS = 10_000; const DISCORD_REALTIME_WAKE_NAME_FOLLOWUP_TTL_MS = 10_000; -const DISCORD_REALTIME_LOG_PREVIEW_CHARS = 500; const DISCORD_REALTIME_DEFAULT_MIN_BARGE_IN_AUDIO_END_MS = 250; const DISCORD_REALTIME_FORCED_CONSULT_FALLBACK_DELAY_MS = 200; const DISCORD_REALTIME_DUPLICATE_ERROR_SUPPRESS_MS = 60_000; @@ -139,14 +139,6 @@ type AgentProxyConsultState = { type AgentProxyConsultHandle = RealtimeVoiceForcedConsultHandle; -function formatRealtimeLogPreview(text: string): string { - const oneLine = text.replace(/\s+/g, " ").trim(); - if (oneLine.length <= DISCORD_REALTIME_LOG_PREVIEW_CHARS) { - return oneLine; - } - return `${oneLine.slice(0, DISCORD_REALTIME_LOG_PREVIEW_CHARS)}...`; -} - function formatRealtimeInterruptionLog(event: RealtimeVoiceBridgeEvent): string | undefined { const detail = event.detail ? ` ${event.detail}` : ""; if (event.direction === "client") { @@ -522,7 +514,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession { onTranscript: (role, text, isFinal) => { if (isFinal && text.trim()) { logger.info( - `discord voice: realtime ${role} transcript (${text.length} chars): ${formatRealtimeLogPreview(text)}`, + `discord voice: realtime ${role} transcript (${text.length} chars): ${formatVoiceLogPreview(text)}`, ); } if (isFinal && role === "assistant") { @@ -1420,7 +1412,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession { if (skipReason) { const context = this.consumePendingSpeakerContext(); logger.info( - `discord voice: realtime forced agent consult skipped reason=${skipReason} chars=${question.length} speaker=${context?.speakerLabel ?? "unknown"} transcript=${formatRealtimeLogPreview(question)}`, + `discord voice: realtime forced agent consult skipped reason=${skipReason} chars=${question.length} speaker=${context?.speakerLabel ?? "unknown"} transcript=${formatVoiceLogPreview(question)}`, ); return undefined; } diff --git a/extensions/discord/src/voice/segment.ts b/extensions/discord/src/voice/segment.ts index 48daae39295..7a80388e1c8 100644 --- a/extensions/discord/src/voice/segment.ts +++ b/extensions/discord/src/voice/segment.ts @@ -8,6 +8,7 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime"; import { maybeControlDiscordVoiceAgentRun } from "./agent-control.js"; import { createDiscordOpusPlaybackStream } from "./audio.js"; import { resolveDiscordVoiceIngressContext, runDiscordVoiceAgentTurn } from "./ingress.js"; +import { formatVoiceLogPreview } from "./log-preview.js"; import { formatVoiceIngressPrompt } from "./prompt.js"; import { loadDiscordVoiceSdk } from "./sdk-runtime.js"; import { @@ -19,17 +20,8 @@ import { import type { DiscordVoiceSpeakerContextResolver } from "./speaker-context.js"; import { synthesizeVoiceReplyAudio, transcribeVoiceAudio } from "./tts.js"; -const VOICE_TRANSCRIPT_LOG_PREVIEW_CHARS = 500; const logger = createSubsystemLogger("discord/voice"); -function formatVoiceTranscriptLogPreview(text: string): string { - const oneLine = text.replace(/\s+/g, " ").trim(); - if (oneLine.length <= VOICE_TRANSCRIPT_LOG_PREVIEW_CHARS) { - return oneLine; - } - return `${oneLine.slice(0, VOICE_TRANSCRIPT_LOG_PREVIEW_CHARS)}...`; -} - export async function processDiscordVoiceSegment(params: { entry: VoiceSessionEntry; wavPath: string; @@ -78,7 +70,7 @@ export async function processDiscordVoiceSegment(params: { `transcription ok (${transcript.length} chars): guild ${entry.guildId} channel ${entry.channelId}`, ); logVoiceVerbose( - `transcript from ${ingress.speakerLabel} (${userId}) in guild ${entry.guildId} channel ${entry.channelId}: ${formatVoiceTranscriptLogPreview(transcript)}`, + `transcript from ${ingress.speakerLabel} (${userId}) in guild ${entry.guildId} channel ${entry.channelId}: ${formatVoiceLogPreview(transcript)}`, ); if (params.transcripts) { await params.transcripts.onUtterance({