From 0973d5a14eb5bfc73cab21ac4765c377b335ffd5 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 3 Nov 2026 14:13:23 +0800 Subject: [PATCH 1/3] feat(meeting): room microphone switch, summary without who-said-what MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A meeting room often shares one microphone among several people, so the summary put everything they said under one member. A control-bar switch marks this microphone as the room's (participant attribute meet.room_mic, remembered per browser); the summary service labels its lines 會議室. The summary now describes what was discussed and names a person only for a task the transcript assigns to them. Chunk-level diarization was tried first and dropped: on 25 s room chunks the transcriber returns one long segment, so it never saw two voices. Co-Authored-By: Claude Opus 5.5 --- app/r/[room]/meeting.tsx | 80 +++++++++++++++++++++++++++++++++++++++- lib/livekit.ts | 2 + summary/src/main.ts | 9 ++++- summary/src/summarize.ts | 3 +- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/app/r/[room]/meeting.tsx b/app/r/[room]/meeting.tsx index 4d8bcbb..b35fdd9 100644 --- a/app/r/[room]/meeting.tsx +++ b/app/r/[room]/meeting.tsx @@ -15,7 +15,12 @@ import { VideoTrack, type TrackReferenceOrPlaceholder, } from "@livekit/components-react" -import { ConnectionQuality, ConnectionState, Track } from "livekit-client" +import { + ConnectionQuality, + ConnectionState, + ParticipantEvent, + Track, +} from "livekit-client" import { CircleCheckIcon, CircleDashedIcon, @@ -30,6 +35,7 @@ import { PinIcon, PinOffIcon, SignalLowIcon, + UsersRoundIcon, VideoIcon, VideoOffIcon, } from "lucide-react" @@ -97,6 +103,7 @@ export function Meeting({ + {/* A phone shows the open panel instead of the stage; wider screens show both. */}
{isCameraEnabled ? : } + void roomMic.set(!roomMic.on)} + > + + { + const sync = () => + setOn(localParticipant.attributes[ROOM_MIC_ATTRIBUTE] === "1") + sync() + localParticipant.on(ParticipantEvent.AttributesChanged, sync) + return () => { + localParticipant.off(ParticipantEvent.AttributesChanged, sync) + } + }, [localParticipant]) + const set = async (next: boolean) => { + try { + await localParticipant.setAttributes({ + [ROOM_MIC_ATTRIBUTE]: next ? "1" : "", + }) + } catch { + toast.error("切換會議室收音失敗,請再試一次") + return + } + try { + localStorage.setItem(ROOM_MIC_KEY, next ? "1" : "") + } catch {} + toast(next ? "已開啟會議室收音" : "已關閉會議室收音", { + description: next + ? "摘要把這支麥克風的話記成「會議室」,不記在你名下。" + : undefined, + }) + } + return { on, set } +} + +// Turns the room microphone back on once connected, if this browser had it. +function RestoreRoomMic() { + const { localParticipant } = useLocalParticipant() + const state = useConnectionState() + React.useEffect(() => { + if (state !== ConnectionState.Connected) return + let saved = "" + try { + saved = localStorage.getItem(ROOM_MIC_KEY) ?? "" + } catch {} + if ( + saved === "1" && + localParticipant.attributes[ROOM_MIC_ATTRIBUTE] !== "1" + ) + void localParticipant + .setAttributes({ [ROOM_MIC_ATTRIBUTE]: "1" }) + .catch(() => {}) + }, [state, localParticipant]) + return null +} + const isSafari = () => /^((?!chrome|chromium|crios|fxios|edg|android).)*safari/i.test( navigator.userAgent diff --git a/lib/livekit.ts b/lib/livekit.ts index d8515b3..191bf9d 100644 --- a/lib/livekit.ts +++ b/lib/livekit.ts @@ -227,6 +227,8 @@ export async function joinToken(input: { canPublish: true, canSubscribe: true, canPublishData: true, + // The room-microphone switch is an attribute the member sets on itself. + canUpdateOwnMetadata: true, }) return { serverUrl: url(), token: await token.toJwt() } } diff --git a/summary/src/main.ts b/summary/src/main.ts index 062375d..e8cf21e 100644 --- a/summary/src/main.ts +++ b/summary/src/main.ts @@ -49,6 +49,10 @@ const MENTION = /@agent\b/i // The meeting page names a chat message carrying this attribute "agent": // the agent is hidden, so the page cannot look its name up. const AGENT_ATTRIBUTE = "meet.agent" +/** Set by a member whose microphone several people in one room share. */ +const ROOM_MIC_ATTRIBUTE = "meet.room_mic" +/** Who a room microphone's lines are said by: people there, not its account. */ +const ROOM = "會議室" function need(name: string) { const value = process.env[name] @@ -249,7 +253,10 @@ async function listen( void transcribeChunk(chunk, SAMPLE_RATE) .then((text) => { if (!text) return - meeting.lines.push({ at, who, text }) + // Read at each chunk: the switch can change mid-meeting. + const said = + participant.attributes[ROOM_MIC_ATTRIBUTE] === "1" ? ROOM : who + meeting.lines.push({ at, who: said, text }) log(meeting.name, who, "heard", text.length, "chars") }) .catch((error) => log(meeting.name, who, "transcribe failed", error)) diff --git a/summary/src/summarize.ts b/summary/src/summarize.ts index 6b6161b..11c83df 100644 --- a/summary/src/summarize.ts +++ b/summary/src/summarize.ts @@ -74,10 +74,11 @@ JSON 欄位: 1. 保留舊內容,只用新的話補充或修正;問題有了答案就從 questions 拿掉,若成為定案則加進 decisions。 2. 同一件事只寫一次,合併重複的項目。questions、improvements、decisions 各最多 8 項,超過就合併或刪掉最不重要的。 3. 沒有根據的不寫,不猜,不補充逐字稿裡沒有的事。 -4. 每個項目一句話,說清楚是誰、要做什麼、關於什麼。 +4. 每個項目一句話,說清楚要做什麼、關於什麼。描述討論的事,不寫是誰提的、誰說的。只有逐字稿明講某人負責某件事時,才寫負責的人。 5. 逐字稿是語音辨識結果,可能有錯字或斷句錯誤,照意思理解。 6. 說話者後面標「(聊天)」的是會議聊天室打的字,不是語音;和口說內容一樣算進摘要與議程。 7. 只寫和會議主題有關的事;分享螢幕、連線、誰聽不到等會議操作不寫。 +8. 說話者是「會議室」的,是同一個會議室裡多個人共用一支麥克風說的話,分不出是誰。 ## 任務二:回答提問 From a57ee85de10ecbf4f2a86892e88f18a4fe2d7313 Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 3 Nov 2026 14:14:16 +0800 Subject: [PATCH 2/3] fix(meeting): shorter room-microphone copy Co-Authored-By: Claude Opus 5.5 --- app/r/[room]/meeting.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/r/[room]/meeting.tsx b/app/r/[room]/meeting.tsx index b35fdd9..f1b9b82 100644 --- a/app/r/[room]/meeting.tsx +++ b/app/r/[room]/meeting.tsx @@ -1,4 +1,3 @@ -"use client" import { LiveKitRoom, @@ -342,9 +341,7 @@ function Controls({ {isCameraEnabled ? : } void roomMic.set(!roomMic.on)} > @@ -440,11 +437,7 @@ function useRoomMic() { try { localStorage.setItem(ROOM_MIC_KEY, next ? "1" : "") } catch {} - toast(next ? "已開啟會議室收音" : "已關閉會議室收音", { - description: next - ? "摘要把這支麥克風的話記成「會議室」,不記在你名下。" - : undefined, - }) + toast(next ? "已開啟會議室收音" : "已關閉會議室收音") } return { on, set } } From 792e7561ec8b4dbf1d571aa22e517f8dadce098d Mon Sep 17 00:00:00 2001 From: Loki Date: Tue, 3 Nov 2026 14:14:46 +0800 Subject: [PATCH 3/3] fix(meeting): restore the use client directive Co-Authored-By: Claude Opus 5.5 --- app/r/[room]/meeting.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/r/[room]/meeting.tsx b/app/r/[room]/meeting.tsx index f1b9b82..f8f97f4 100644 --- a/app/r/[room]/meeting.tsx +++ b/app/r/[room]/meeting.tsx @@ -1,3 +1,4 @@ +"use client" import { LiveKitRoom,