feat: 대면 회의 화자 분리 (로컬 diarization)

마이크 한 트랙에 여러 사람이 섞여 들어오는 대면 회의에서 화자를 나눈다.
pyannote-segmentation-3.0 + 3D-Speaker CAM++ 를 ONNX Runtime으로 로컬
Next.js 프로세스에서 실행한다. GPU도 파이썬도 필요 없고 오디오가 외부로
나가지 않는다.

실측 (Apple Silicon, 2스레드, 4인 한국어 아닌 샘플 57초):
- 처리 1.5초 → RTF 0.03, 실시간의 33배
- 자동 추정은 화자 5명(과다 계수), 참석자 수를 주면 4명(정확)
  → 참석자 수 입력을 1급 기능으로 노출

구성:
- lib/diarization.ts — sherpa-onnx-node 래퍼, 모델 캐시, 참석자 수 힌트
- lib/audio-session.ts — 녹음 중 PCM 조각을 세션별로 누적 (메모리)
- api/diarize/chunk — 4초짜리 Int16 PCM 조각 업로드
- api/diarize — 누적 오디오 전체 분석, GET은 모델 설치 여부 확인
- public/pcm-worklet.js — 16kHz Int16 PCM 추출 AudioWorklet
- hooks/useDiarization.ts — 수집·분석 상태 관리
- transcript-formatter: assignSpeakers() — 시간 겹침으로 화자 배정.
  겹치는 구간이 없으면 라벨을 비운다 (틀린 라벨보다 없는 편이 낫다)
- LiveRecorder: 사용 안 함 / 원격 회의 / 대면 회의 3모드 + 참석자 수
- scripts/setup-diarization.mjs — 모델 34MB 내려받기 (npm run setup:diarization)

설계 변경: 롤링 재-diarization 대신 녹음 종료 시 1회 전체 분석.
구간을 잘라 반복하면 실행마다 화자 번호가 달라져 이어 붙일 수 없고,
누적분 재분석은 비용이 회의 길이에 비례해 커진다. 대신 실시간 화자 표시는 없다.

모델은 라이선스와 용량 때문에 저장소에 넣지 않고 gitignore 한다.

테스트 176 → 184.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
csbaeandClaude Opus 5 committed 2026-09-08 10:47:10 +09:00
1 parent a8cfab8a4a
commit 4af0a03de5
19 files changed
+1110 -75

No files matched your search

+94 -1
View File
@@ -16,7 +16,8 @@
"pg": "^8.13.1",
"prisma": "^7.7.0",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"sherpa-onnx-node": "^1.13.7"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -8422,6 +8423,98 @@
"node": ">=8"
}
},
"node_modules/sherpa-onnx-darwin-arm64": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-arm64/-/sherpa-onnx-darwin-arm64-1.13.7.tgz",
"integrity": "sha512-5NCE50hAvr3n2pdett0SgfPBJXaFZE0bqHwbHyiq+IKZ8Ids0l4M0VrG+ImGYIafCwie+oC3uAJ+pKj9xg/k+w==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/sherpa-onnx-darwin-x64": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-darwin-x64/-/sherpa-onnx-darwin-x64-1.13.7.tgz",
"integrity": "sha512-N3o+T+wn9WaQmsKV5DD8bTHdo+WN2+sXwmZcGJZiDjtOMR2zFz7uVCZnYCmEAMgvChC+oHcF5RvEEKcRCAu6Pw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/sherpa-onnx-linux-arm64": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-linux-arm64/-/sherpa-onnx-linux-arm64-1.13.7.tgz",
"integrity": "sha512-TFCVpXyTh69buhOtTS8KIfkRXOVKY4Y1qjAktSItrKS4A0chnnrlXO5bKWoNAPeI6fMxTF/uvMYbYgcvjEMfNg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
]
},
"node_modules/sherpa-onnx-linux-x64": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-linux-x64/-/sherpa-onnx-linux-x64-1.13.7.tgz",
"integrity": "sha512-npmxn5WwmAmlthgBhmbZ33t3i2j4mJwQt46dMEb3j7d41y1/uJrjrVAfa/DkvV+vn49ZWfcQ2UEWDipaZBVhuw==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
]
},
"node_modules/sherpa-onnx-node": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-node/-/sherpa-onnx-node-1.13.7.tgz",
"integrity": "sha512-0XGV7arGngBCnol0m8OLyqlnaUm19Q1KmetVj1DDBdymXa1upmAHZDwNdN47gjsEhqE5hXUEyc1vRQoXrNhNVg==",
"license": "Apache-2.0",
"optionalDependencies": {
"sherpa-onnx-darwin-arm64": "^1.13.7",
"sherpa-onnx-darwin-x64": "^1.13.7",
"sherpa-onnx-linux-arm64": "^1.13.7",
"sherpa-onnx-linux-x64": "^1.13.7",
"sherpa-onnx-win-ia32": "^1.13.7",
"sherpa-onnx-win-x64": "^1.13.7"
}
},
"node_modules/sherpa-onnx-win-ia32": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-win-ia32/-/sherpa-onnx-win-ia32-1.13.7.tgz",
"integrity": "sha512-sTwtpxPQ76XLn0giAbvknIDEDKD3XXi2mo2AVROEucf1pIK1DjQl+LjLkalTeFoQqbC4J3xGx/g+xgcHQD1dsw==",
"cpu": [
"ia32"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
]
},
"node_modules/sherpa-onnx-win-x64": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/sherpa-onnx-win-x64/-/sherpa-onnx-win-x64-1.13.7.tgz",
"integrity": "sha512-wBV1o+/zgsMrOjfCFIgGrH6S28xq6CqRCLSavCOjTZ6cqr80yGc07DUHxqsHFPZvfoJU+2JF5L2l3gyWFWoWdQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
]
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",