diff --git a/bot/src/userbot.ts b/bot/src/userbot.ts index 9ff176c..aef6df5 100644 --- a/bot/src/userbot.ts +++ b/bot/src/userbot.ts @@ -38,6 +38,9 @@ export interface TurnInfo { /** Discord user ID of the speaker, so the transcript shows whose audio * produced each turn (and which user a dropped/VAD turn belongs to). */ user?: string; + /** Resolved display name (server nickname / global name); shown instead of + * the raw user ID when available. */ + userName?: string; transcript: string; reply: string; note?: string; @@ -72,7 +75,7 @@ function durSec(a?: number, b?: number): string | null { * timing breakdown (listening / LLM / TTS) with startβ†’end wall-clock times and * durations, so it's obvious what took long. Pure + exported for testing. */ export function formatTurnMessage(info: TurnInfo): string { - const who = info.user ? `πŸ‘€ ${info.user} ` : ""; + const who = info.userName || info.user ? `πŸ‘€ ${info.userName || info.user} ` : ""; const head = info.transcript ? `${who}🎀 λ“€μŒ β†’ πŸ—£οΈ "${info.transcript}"\nπŸ€– λ‹΅λ³€: ${(info.reply || "").trim() || "(무응닡)"}` : `${who}🎀 λ“€μŒ β†’ ❌ ${info.note || "λ¬΄μ‹œλ¨"}`; @@ -124,7 +127,7 @@ async function joinAndListen(client: AnyClient, channelId: string): Promise { - console.log(`πŸ‘€ ${info.user || "?"} πŸ—£οΈ ${info.transcript || "(" + (info.note || "empty") + ")"}\nπŸ€– ${info.reply}`); + console.log(`πŸ‘€ ${info.userName || info.user || "?"} πŸ—£οΈ ${info.transcript || "(" + (info.note || "empty") + ")"}\nπŸ€– ${info.reply}`); // Mirror every heard utterance (and the reply / drop reason) to a text // channel so you can see what the bot understood even when it doesn't answer. void postTranscript(client, info); diff --git a/bot/src/voice.ts b/bot/src/voice.ts index 91fc5db..1fedf08 100644 --- a/bot/src/voice.ts +++ b/bot/src/voice.ts @@ -81,6 +81,9 @@ export class VoiceSession { * diagnosable. `note` says why (e.g. "μŒμ„± μ•„λ‹˜(VAD 차단)", "λ„ˆλ¬΄ 짧음", "ok"). */ onTurn?: (info: { user: string; + /** Resolved display name (server nickname / global name) for the speaker, + * so logs show a human name instead of the raw Discord user ID. */ + userName?: string; transcript: string; reply: string; note?: string; @@ -164,6 +167,31 @@ export class VoiceSession { }); } + /** Resolve a speaker's Discord user ID to a human display name (server + * nickname, else global name / username), cached so we don't refetch every + * utterance. Falls back to the ID if lookup fails. */ + private nameCache = new Map(); + private async displayName(userId: string): Promise { + const cached = this.nameCache.get(userId); + if (cached) return cached; + let name = userId; + try { + const guild: any = this.client?.guilds?.cache?.get(this.guildId); + let member: any = guild?.members?.cache?.get(userId); + if (!member && guild?.members?.fetch) member = await guild.members.fetch(userId).catch(() => null); + if (member) { + name = member.displayName || member.nickname || member.user?.globalName || member.user?.username || userId; + } else { + const u: any = this.client?.users?.cache?.get(userId) || (await this.client?.users?.fetch?.(userId).catch(() => null)); + name = u?.globalName || u?.username || userId; + } + } catch { + /* fall back to id */ + } + this.nameCache.set(userId, name); + return name; + } + private async captureUtterance(userId: string): Promise { // Don't start a new capture once we're tearing down (user left). if (this.destroyed) return; @@ -199,6 +227,7 @@ export class VoiceSession { if (mono.length < DISCORD_RATE * 0.3 * 2) { this.onTurn?.({ user: userId, + userName: await this.displayName(userId), transcript: "", reply: "", note: "λ„ˆλ¬΄ 짧음(<300ms)", @@ -247,6 +276,7 @@ export class VoiceSession { // explains why a turn did or didn't answer, with full stage timing. this.onTurn?.({ user: userId, + userName: await this.displayName(userId), transcript: metaSeen?.transcript ?? "", reply: metaSeen?.reply ?? "", note: metaSeen?.note,