Files
meeting-minutes/src/__tests__/speakers.test.ts
T
csbaeandClaude Opus 5 d2cb5b0009 feat: dual-stream 캡처로 원격 회의 화자 분리
내 마이크와 시스템(탭) 오디오를 별도 트랙으로 잡고, 트랙마다 인식기를
붙인다. 트랙이 곧 화자이므로 추론도 모델 다운로드도 없이 화자가 갈린다.

Chrome이 SpeechRecognition.start(audioTrack)를 지원하는 덕분에 가능하다.
트랙을 명시적으로 넘기므로 기본 마이크를 두고 경합할 일도 없다.

- speakers.ts 신설 — 화자 식별자와 표시 이름 해석
- TranscriptChunk.speaker 추가, formatTranscriptChunks가 "화자: 발화"로 출력
- mergeSpeakerChunks — 트랙별 청크를 하나의 시간축으로 병합.
  여러 인식기가 같은 timeOrigin을 공유해야 순서가 맞는다
- mergeAdjacentChunks가 화자 경계를 넘어 합치지 않도록 수정
- useSpeechRecognition: audioTrack / speaker / timeOrigin / lang 옵션.
  인자 없이 호출하면 기존 동작 그대로다
- useDualStreamCapture 신설 — getUserMedia + getDisplayMedia.
  video는 즉시 끊고 오디오만 쓴다. 시스템 오디오 확보에 실패하면
  마이크 단독으로 폴백하고 화자 분리가 꺼졌음을 안내한다
- LiveRecorder: 화자 분리 토글, 화자 이름 입력, 전사 목록 화자 색상 구분

원격 다자 회의와 대면 회의는 원격 트랙에 diarization이 필요하며 후속 작업이다.

테스트 162 → 176.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:27:00 +09:00

46 lines
1.3 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
LOCAL_SPEAKER,
REMOTE_SPEAKER,
listAttendees,
resolveSpeakerName,
} from '@/lib/speakers'
describe('resolveSpeakerName', () => {
it('기본 화자에 한국어 표기를 붙인다', () => {
expect(resolveSpeakerName(LOCAL_SPEAKER)).toBe('나')
expect(resolveSpeakerName(REMOTE_SPEAKER)).toBe('상대')
})
it('사용자가 지정한 이름이 우선한다', () => {
const names = { [REMOTE_SPEAKER]: '김지훈' }
expect(resolveSpeakerName(REMOTE_SPEAKER, names)).toBe('김지훈')
expect(resolveSpeakerName(LOCAL_SPEAKER, names)).toBe('나')
})
it('공백뿐인 이름은 무시한다', () => {
expect(resolveSpeakerName(LOCAL_SPEAKER, { [LOCAL_SPEAKER]: ' ' })).toBe('나')
})
it('모르는 식별자는 그대로 돌려준다', () => {
expect(resolveSpeakerName('spk_3')).toBe('spk_3')
})
})
describe('listAttendees', () => {
it('중복을 제거하고 순서를 유지한다', () => {
expect(
listAttendees([REMOTE_SPEAKER, LOCAL_SPEAKER, REMOTE_SPEAKER]),
).toEqual(['상대', '나'])
})
it('지정된 이름을 반영한다', () => {
expect(
listAttendees([LOCAL_SPEAKER, REMOTE_SPEAKER], {
[LOCAL_SPEAKER]: '배철승',
[REMOTE_SPEAKER]: '김지훈',
}),
).toEqual(['배철승', '김지훈'])
})
})