diff --git a/app/r/[room]/meeting.tsx b/app/r/[room]/meeting.tsx index 4d8bcbb..f8f97f4 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 ? "已開啟會議室收音" : "已關閉會議室收音") + } + 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. 說話者是「會議室」的,是同一個會議室裡多個人共用一支麥克風說的話,分不出是誰。 ## 任務二:回答提問