






















@@ -56,8 +56,13 @@ type DiscordRealtimeVoiceConfig = NonNullable<DiscordAccountConfig["voice"]>["re
5656type PendingSpeakerTurn = {
5757context: DiscordRealtimeSpeakerContext;
5858hasAudio: boolean;
59+inputDiscordBytes: number;
60+inputRealtimeBytes: number;
61+inputChunks: number;
5962interruptedPlayback: boolean;
6063closed: boolean;
64+startedAt: number;
65+lastAudioAt?: number;
6166};
62676368type PendingAgentProxyConsultContext = {
@@ -264,8 +269,12 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
264269private recentAgentProxyConsultContexts: RecentAgentProxyConsultContext[] = [];
265270private readonly pendingSpeakerTurns: PendingSpeakerTurn[] = [];
266271private outputAudioTimestampMs = 0;
272+private outputAudioDiscordBytes = 0;
273+private outputAudioRealtimeBytes = 0;
274+private outputAudioChunks = 0;
275+private outputAudioStartedAt: number | undefined;
267276private readonly playerIdleHandler = () => {
268-this.resetOutputStream();
277+this.resetOutputStream("player-idle");
269278};
270279271280constructor(
@@ -346,7 +355,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
346355audioSink: {
347356isOpen: () => !this.stopped,
348357sendAudio: (audio) => this.sendOutputAudio(audio),
349-clearAudio: () => this.clearOutputAudio(),
358+clearAudio: () => this.clearOutputAudio("provider-clear-audio"),
350359},
351360onTranscript: (role, text, isFinal) => {
352361if (isFinal && text.trim()) {
@@ -402,7 +411,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
402411this.pendingAgentProxyConsultContexts = [];
403412this.recentAgentProxyConsultContexts = [];
404413this.pendingSpeakerTurns.length = 0;
405-this.clearOutputAudio();
414+this.clearOutputAudio("session-close");
406415this.bridge?.close();
407416this.bridge = null;
408417const voiceSdk = loadDiscordVoiceSdk();
@@ -413,15 +422,23 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
413422const turn: PendingSpeakerTurn = {
414423context: { ...context, userId },
415424hasAudio: false,
425+inputDiscordBytes: 0,
426+inputRealtimeBytes: 0,
427+inputChunks: 0,
416428interruptedPlayback: false,
417429closed: false,
430+startedAt: Date.now(),
418431};
419432this.pendingSpeakerTurns.push(turn);
433+logger.info(
434+`discord voice: realtime speaker turn opened guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${userId} speaker=${context.speakerLabel} owner=${context.senderIsOwner} pendingTurns=${this.pendingSpeakerTurns.length}`,
435+);
420436this.prunePendingSpeakerTurns();
421437return {
422438sendInputAudio: (discordPcm48kStereo) =>
423439this.sendInputAudioForTurn(turn, discordPcm48kStereo),
424440close: () => {
441+this.logSpeakerTurnClosed(turn);
425442turn.closed = true;
426443this.prunePendingSpeakerTurns();
427444},
@@ -435,22 +452,40 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
435452turn.hasAudio = true;
436453const realtimePcm = convertDiscordPcm48kStereoToRealtimePcm24kMono(discordPcm48kStereo);
437454if (realtimePcm.length > 0) {
455+turn.inputDiscordBytes += discordPcm48kStereo.length;
456+turn.inputRealtimeBytes += realtimePcm.length;
457+turn.inputChunks += 1;
458+turn.lastAudioAt = Date.now();
459+if (turn.inputChunks === 1) {
460+logger.info(
461+`discord voice: realtime input audio started guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} discordBytes=${discordPcm48kStereo.length} realtimeBytes=${realtimePcm.length} outputAudioMs=${Math.floor(this.outputAudioTimestampMs)} outputActive=${this.isOutputAudioActive()}`,
462+);
463+}
438464if (!turn.interruptedPlayback && this.isBargeInEnabled()) {
439465turn.interruptedPlayback = true;
440466logVoiceVerbose(
441467`realtime barge-in from active speaker audio: guild ${this.params.entry.guildId} channel ${this.params.entry.channelId} user ${turn.context.userId}`,
442468);
443-this.handleBargeIn();
469+logger.info(
470+`discord voice: realtime barge-in detected source=active-speaker-audio guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} outputAudioMs=${Math.floor(this.outputAudioTimestampMs)} outputActive=${this.isOutputAudioActive()} discordBytes=${discordPcm48kStereo.length} realtimeBytes=${realtimePcm.length}`,
471+);
472+this.handleBargeIn("active-speaker-audio");
444473}
445474this.bridge.sendAudio(realtimePcm);
446475}
447476}
448477449-handleBargeIn(): void {
478+handleBargeIn(reason = "barge-in"): void {
450479if (!this.isBargeInEnabled()) {
480+logger.info(
481+`discord voice: realtime barge-in ignored reason=${reason} bargeIn=false guild=${this.params.entry.guildId} channel=${this.params.entry.channelId}`,
482+);
451483return;
452484}
453485this.syncOutputAudioTimestamp();
486+logger.info(
487+`discord voice: realtime barge-in requested reason=${reason} guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} outputAudioMs=${Math.floor(this.outputAudioTimestampMs)} outputActive=${this.isOutputAudioActive()} playbackChunks=${this.outputAudioChunks}`,
488+);
454489this.bridge?.handleBargeIn({ audioPlaybackActive: true });
455490}
456491@@ -474,6 +509,9 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
474509this.syncOutputAudioTimestamp();
475510const stream = this.ensureOutputStream();
476511stream.write(discordPcm);
512+this.outputAudioDiscordBytes += discordPcm.length;
513+this.outputAudioRealtimeBytes += realtimePcm24kMono.length;
514+this.outputAudioChunks += 1;
477515this.outputAudioTimestampMs += pcm16MonoDurationMs(
478516realtimePcm24kMono,
479517REALTIME_VOICE_AUDIO_FORMAT_PCM16_24KHZ.sampleRateHz,
@@ -487,9 +525,12 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
487525const voiceSdk = loadDiscordVoiceSdk();
488526const stream = new PassThrough();
489527this.outputStream = stream;
528+this.outputAudioStartedAt = Date.now();
490529stream.once("close", () => {
491530if (this.outputStream === stream) {
531+this.logOutputAudioStopped("stream-close");
492532this.outputStream = null;
533+this.resetOutputAudioStats();
493534}
494535});
495536const resource = voiceSdk.createAudioResource(stream, {
@@ -503,23 +544,60 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
503544return stream;
504545}
505546506-private clearOutputAudio(): void {
507-this.resetOutputStream();
547+private clearOutputAudio(reason = "clear"): void {
548+this.resetOutputStream(reason);
508549this.params.entry.player.stop(true);
509550}
510551511-private resetOutputStream(): void {
552+private resetOutputStream(reason = "reset"): void {
512553const stream = this.outputStream;
554+this.logOutputAudioStopped(reason);
513555this.outputStream = null;
514-this.outputAudioTimestampMs = 0;
556+this.resetOutputAudioStats();
515557stream?.end();
516558stream?.destroy();
517559}
518560561+private logOutputAudioStopped(reason: string): void {
562+const audioMs = Math.floor(this.outputAudioTimestampMs);
563+const chunks = this.outputAudioChunks;
564+const discordBytes = this.outputAudioDiscordBytes;
565+const realtimeBytes = this.outputAudioRealtimeBytes;
566+const elapsedMs = this.outputAudioStartedAt ? Date.now() - this.outputAudioStartedAt : 0;
567+if (this.outputStream || chunks > 0 || audioMs > 0) {
568+logger.info(
569+`discord voice: realtime audio playback stopped reason=${reason} guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} audioMs=${audioMs} elapsedMs=${elapsedMs} chunks=${chunks} discordBytes=${discordBytes} realtimeBytes=${realtimeBytes}`,
570+);
571+}
572+}
573+574+private resetOutputAudioStats(): void {
575+this.outputAudioTimestampMs = 0;
576+this.outputAudioDiscordBytes = 0;
577+this.outputAudioRealtimeBytes = 0;
578+this.outputAudioChunks = 0;
579+this.outputAudioStartedAt = undefined;
580+}
581+519582private syncOutputAudioTimestamp(): void {
520583this.bridge?.setMediaTimestamp(Math.floor(this.outputAudioTimestampMs));
521584}
522585586+private isOutputAudioActive(): boolean {
587+return Boolean(this.outputStream && !this.outputStream.destroyed) || this.outputAudioChunks > 0;
588+}
589+590+private logSpeakerTurnClosed(turn: PendingSpeakerTurn): void {
591+if (turn.closed) {
592+return;
593+}
594+const elapsedMs = Date.now() - turn.startedAt;
595+const sinceLastAudioMs = turn.lastAudioAt ? Date.now() - turn.lastAudioAt : undefined;
596+logger.info(
597+`discord voice: realtime speaker turn closed guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} user=${turn.context.userId} speaker=${turn.context.speakerLabel} owner=${turn.context.senderIsOwner} hasAudio=${turn.hasAudio} chunks=${turn.inputChunks} discordBytes=${turn.inputDiscordBytes} realtimeBytes=${turn.inputRealtimeBytes} elapsedMs=${elapsedMs}${sinceLastAudioMs === undefined ? "" : ` sinceLastAudioMs=${sinceLastAudioMs}`} interruptedPlayback=${turn.interruptedPlayback}`,
598+);
599+}
600+523601private handleToolCall(
524602event: RealtimeVoiceToolCallEvent,
525603session: RealtimeVoiceBridgeSession,
@@ -711,8 +789,11 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
711789`discord voice: realtime forced agent consult starting chars=${question.length} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId} speaker=${context.speakerLabel} owner=${context.senderIsOwner}`,
712790);
713791this.syncOutputAudioTimestamp();
792+logger.info(
793+`discord voice: realtime barge-in requested reason=forced-agent-consult guild=${this.params.entry.guildId} channel=${this.params.entry.channelId} outputAudioMs=${Math.floor(this.outputAudioTimestampMs)} outputActive=${this.isOutputAudioActive()} playbackChunks=${this.outputAudioChunks} force=true`,
794+);
714795this.bridge?.handleBargeIn({ audioPlaybackActive: true, force: true });
715-this.clearOutputAudio();
796+this.clearOutputAudio("forced-agent-consult");
716797pending.recent.handledByForcedPlayback = true;
717798try {
718799const promise = this.runAgentTurn({
@@ -728,7 +809,7 @@ export class DiscordRealtimeVoiceSession implements VoiceRealtimeSession {
728809`discord voice: realtime forced agent consult answer (${text.length} chars) elapsedMs=${Date.now() - startedAt} voiceSession=${this.params.entry.voiceSessionKey} supervisorSession=${this.params.entry.route.sessionKey} agent=${this.params.entry.route.agentId}: ${formatRealtimeLogPreview(text)}`,
729810);
730811if (text.trim()) {
731-this.clearOutputAudio();
812+this.clearOutputAudio("forced-agent-consult-answer");
732813this.bridge?.sendUserMessage(buildDiscordSpeakExactUserMessage(text));
733814}
734815} catch (error) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。