Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion app/r/[room]/meeting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -30,6 +35,7 @@ import {
PinIcon,
PinOffIcon,
SignalLowIcon,
UsersRoundIcon,
VideoIcon,
VideoOffIcon,
} from "lucide-react"
Expand Down Expand Up @@ -97,6 +103,7 @@ export function Meeting({
<RoomAudioRenderer />
<SummaryListener onSummary={setSummary} />
<Shortcuts />
<RestoreRoomMic />
<Reconnecting />
{/* A phone shows the open panel instead of the stage; wider screens show both. */}
<div
Expand Down Expand Up @@ -295,6 +302,7 @@ function Controls({
useLocalParticipant()
const mic = useTrackToggle({ source: Track.Source.Microphone })
const camera = useTrackToggle({ source: Track.Source.Camera })
const roomMic = useRoomMic()
const wasSharing = React.useRef(false)
const screen = useTrackToggle({
source: Track.Source.ScreenShare,
Expand Down Expand Up @@ -333,6 +341,13 @@ function Controls({
>
{isCameraEnabled ? <VideoIcon /> : <VideoOffIcon />}
</Control>
<Control
label={roomMic.on ? "關閉會議室收音" : "會議室收音"}
active={roomMic.on}
onClick={() => void roomMic.set(!roomMic.on)}
>
<UsersRoundIcon />
</Control>
<Control
label={isScreenShareEnabled ? "停止分享螢幕" : "分享螢幕"}
active={isScreenShareEnabled}
Expand Down Expand Up @@ -392,6 +407,63 @@ function Controls({
)
}

// Room microphone: several people in one room share this microphone, so the
// summary says "會議室" for what it hears instead of this member's name. It is
// a participant attribute the summary service reads, and this browser
// remembers it for the next meeting.
const ROOM_MIC_ATTRIBUTE = "meet.room_mic"
const ROOM_MIC_KEY = "meet.room-mic"

function useRoomMic() {
const { localParticipant } = useLocalParticipant()
const [on, setOn] = React.useState(false)
React.useEffect(() => {
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
Expand Down
2 changes: 2 additions & 0 deletions lib/livekit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
Expand Down
9 changes: 8 additions & 1 deletion summary/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion summary/src/summarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ JSON 欄位:
1. 保留舊內容,只用新的話補充或修正;問題有了答案就從 questions 拿掉,若成為定案則加進 decisions。
2. 同一件事只寫一次,合併重複的項目。questions、improvements、decisions 各最多 8 項,超過就合併或刪掉最不重要的。
3. 沒有根據的不寫,不猜,不補充逐字稿裡沒有的事。
4. 每個項目一句話,說清楚是誰、要做什麼、關於什麼。
4. 每個項目一句話,說清楚要做什麼、關於什麼。描述討論的事,不寫是誰提的、誰說的。只有逐字稿明講某人負責某件事時,才寫負責的人。
5. 逐字稿是語音辨識結果,可能有錯字或斷句錯誤,照意思理解。
6. 說話者後面標「(聊天)」的是會議聊天室打的字,不是語音;和口說內容一樣算進摘要與議程。
7. 只寫和會議主題有關的事;分享螢幕、連線、誰聽不到等會議操作不寫。
8. 說話者是「會議室」的,是同一個會議室裡多個人共用一支麥克風說的話,分不出是誰。

## 任務二:回答提問

Expand Down
Loading