feat: per-call LLM timing, speaker ID, cancel captures on leave
- llm.py: log each Ollama call's caller + total/load/prompt/gen durations so a slow voice turn is attributable to a specific internal call (router/enrichment/digest/main); a RELOAD marker flags cold reloads. - voice.ts: track in-flight Opus captures and abort them on session destroy(); drop any utterance that finishes after the user left, so no trailing post-leave VAD turns are reported. - userbot.ts: show the speaker's Discord user ID on each transcript line (answered and dropped) so it's clear whose audio produced the turn. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,13 @@ export class VoiceSession {
|
||||
private connection: VoiceConnection;
|
||||
private player: AudioPlayer;
|
||||
private listening = new Set<string>();
|
||||
/** Set once the session is torn down (user left / leave command). In-flight
|
||||
* captures check this so we don't run STT/reply or post a transcript for
|
||||
* audio that arrived after the user already left the channel. */
|
||||
private destroyed = false;
|
||||
/** Opus subscriptions currently capturing, so leave() can end them
|
||||
* immediately instead of waiting out the silence timeout. */
|
||||
private activeStreams = new Set<{ destroy: () => void }>();
|
||||
/** Pending reply clips. Played one at a time so concurrent speakers don't
|
||||
* cut each other's replies off. */
|
||||
private playQueue: Buffer[] = [];
|
||||
@@ -158,11 +165,14 @@ export class VoiceSession {
|
||||
}
|
||||
|
||||
private async captureUtterance(userId: string): Promise<void> {
|
||||
// Don't start a new capture once we're tearing down (user left).
|
||||
if (this.destroyed) return;
|
||||
// "듣기 시작": the moment we begin capturing this speaker's utterance.
|
||||
const listenStartMs = Date.now();
|
||||
const opusStream = this.connection.receiver.subscribe(userId, {
|
||||
end: { behavior: EndBehaviorType.AfterSilence, duration: config.silenceMs },
|
||||
});
|
||||
this.activeStreams.add(opusStream);
|
||||
const decoder = new prism.opus.Decoder({
|
||||
frameSize: 960,
|
||||
channels: DISCORD_CHANNELS,
|
||||
@@ -173,6 +183,11 @@ export class VoiceSession {
|
||||
pcmStream.on("data", (c: Buffer) => chunks.push(c));
|
||||
|
||||
await new Promise<void>((resolve) => pcmStream.once("end", () => resolve()));
|
||||
this.activeStreams.delete(opusStream);
|
||||
// If the user left while we were capturing, drop this utterance entirely —
|
||||
// don't run STT/reply or report a (usually empty/VAD-blocked) turn for
|
||||
// audio that trailed in after they left.
|
||||
if (this.destroyed) return;
|
||||
// "듣기 종료": end of speech (silence detected). Anything after this is
|
||||
// STT + reply + TTS on the brain side.
|
||||
const listenEndMs = Date.now();
|
||||
@@ -264,6 +279,18 @@ export class VoiceSession {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyed = true;
|
||||
// End any in-flight captures now so their pcmStream resolves immediately
|
||||
// and the post-capture `destroyed` check drops them (no trailing
|
||||
// post-leave VAD turns).
|
||||
for (const s of this.activeStreams) {
|
||||
try {
|
||||
s.destroy();
|
||||
} catch {
|
||||
/* already ended */
|
||||
}
|
||||
}
|
||||
this.activeStreams.clear();
|
||||
try {
|
||||
this.connection.destroy();
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user