데스크톱 녹음기에 누가 말했는지 알게 하던 과정: 전사(Transcription), 화자 식별(Speaker ID) 및 벡터 검색 이야기
요약
본 글은 회의 내용을 실시간으로 기록하고 요약하는 데스크톱 앱 개발 과정을 다룹니다. 핵심 기능으로는 전사(Transcription)와 화자 식별(Speaker ID), 그리고 의미 기반 검색을 구현하는 아키텍처가 소개됩니다. 이 과정에서 여러 기술적 난관과 버그를 해결하며 최종 시스템 구조를 완성했습니다.
핵심 포인트
- 회의 내용을 실시간으로 기록하고 요약하는 앱 개발 과정을 다룸.
- Deepgram의 diarization을 활용하여 음성을 화자별로 분리함.
- VoiceBio 같은 Speaker ID 엔진으로 '음성지문(voiceprint)'을 생성 및 비교함.
- 단순 전사본을 넘어 의미 기반 검색(벡터 검색) 기능을 추가하는 것이 목표임.
저는 또 다른 음성 녹음기를 만들고 있는 것이 아니었습니다.
제가 만들고 있던 것은 회의 동반자였습니다.
데스크톱에 놓이는 작은 앱이 통화 내용을 듣고 다음을 기록합니다:
- 모든 단어
- 누가 말했는지
- 무엇이 결정되었는지
그리고 나중에 팀원들이 찾을 수 있도록 대화를 어딘가에 저장합니다.
- 세 사람이 참여하는 통화 녹음하기.
- 정지(Stop) 버튼 누르기.
- 웹 앱 열기.
- 깔끔한 전사본, 요약본, 그리고 자신의 단어 옆에 자신의 이름까지 확인하기.
이것이 목표였습니다.
그리고 한동안...
완성된 것처럼 보였습니다.
그러다 유튜브 영상을 재생했습니다.
그러자 앱은 그 유튜브 영상이 저라고 판단했습니다.
목차
- 목표 (The Goal)
- 아키텍처 (The Architecture)
- 버그 #1 — 전사본이 멈추다 (Bug #1 — The Transcript Froze)
- 버그 #2 — 유튜브 영상이 나가 되다 (Bug #2 — The YouTube Video Became Me)
- 버그 #3 — 마이크 두 개, 하나의 진실 (Bug #3 — Two Microphones, One Truth)
- 버그 #4 — 컴퓨터 오디오가 나를 낯선 사람으로 만들다 (Bug #4 — Computer Audio Made Me a Stranger)
- 버그 #5 — "이 목소리를 기억해"는 아무것도 기억하지 못하다 (Bug #5 — "Remember This Voice" Remembered Nothing)
- 버그 #6 — "Okafor"가 "Aquifer"가 되다 (Bug #6 — "Okafor" Became "Aquifer")
- 버그 #7 — 저장되지 않은 벡터들 (Bug #7 — The Vectors That Were Never Stored)
- 최종 아키텍처 (The Final Architecture)
- 배운 교훈 (Lessons Learned)
- 마지막 생각 (Final Thoughts)
1. 목표 (The Goal)
아이디어는 간단했습니다.
사용자 마이크 + 컴퓨터 오디오
↓
실시간 전사(Live Transcription)
...
결과는 다음과 같아야 합니다:
- 정확한 단어들
- 모든 줄에 올바른 이름
- 단순 키워드가 아닌 의미로 나중에 찾을 수 있는 기능
들어보면 간단해 보이지만, 그렇지 않았습니다.
2. 아키텍처 (The Architecture)
시스템은 네 부분으로 구성되었습니다.
녹음기 (The recorder)
Electron으로 구축된 데스크톱 앱입니다. 사용자의 마이크를 캡처하고, 선택적으로 컴퓨터 사운드를 두 번째 채널로 추가하여 통화 상대방의 음성을 사용자 본인과 분리하여 녹음합니다.
실시간 전사 (Live transcription)
사용자가 말하는 동안 오디오 스트림이 Deepgram의 speech-to-text 모델로 전송됩니다. API 키가 앱 내부에 포함되지 않도록 하는 작은 로컬 릴레이를 거칩니다.
단어들은 이미 목소리별로 분할되어 돌아옵니다. Deepgram은 이를 diarization(화자 분리)이라고 부릅니다: Speaker 0, Speaker 1, Speaker 2.
하지만 diarization은 단순히 음성이 _다르다는 것_만 알 뿐입니다.
누구의 목소리인지는 모릅니다.
화자 검증 (Speaker verification)
이것이 바로 화자 식별(Speaker ID) 엔진인 VoiceBio의 역할입니다.
사용자가 짧은 단락을 한 번 읽으면, 시스템은 사용자의 목소리 수학적 지문인 **음성지문(voiceprint)**을 생성합니다. 이 음성지문은 암호화되어 사용자 자신의 컴퓨터에 저장됩니다.
녹음이 진행되는 동안, 각 목소리의 몇 초 분량이 이 음성지문과 비교됩니다. 엔진은 **유사도 점수(similarity score)**를 반환하는데, 점수가 높으면 '당신이다'라는 의미이고, 낮으면 '다른 사람이다'라는 의미입니다.
웹 앱은 사용자의 목소리를 절대 볼 수 없습니다. 오직 이름표만 받습니다.
기억 기능 (The memory)
데이터베이스가 기록을 저장합니다. 벡터 데이터베이스(Pinecone)는 색인 역할을 합니다.
각 전사본(transcript)은 청크로 잘려 임베딩으로 변환되어 저장되며, 이를 통해 단순히 '무엇이 말해졌는지'뿐만 아니라 '무슨 의도가 있었는지'를 나중에 검색할 수 있습니다.
3. 버그 #1 — 전사본이 멈춤 (The Transcript Froze)
녹음 중간에 실시간 텍스트가 멈췄습니다.
느려진 것이 아니었습니다. 완전히 멈췄습니다.
로그를 확인해보니, 하나의 음성 검사가 정확히 120초 동안 걸린 기록이 있었습니다.
원인 (The Cause)
해당 음성 검사는 음성 **등록(enrollment)**의 타임아웃을 상속받았습니다. 등록 과정은 긴 녹음을 업로드하고 이를 학습하는 과정이기 때문에 2분이라는 시간이 주어졌던 것입니다.
5초짜리 클립을 확인하는 것은 약 1초가 걸려야 합니다.
수정 (The Fix)
검사에 전용의 짧은 타임아웃을 적용했습니다. 이제 느린 검사는 빠르게 실패하며, 전사본은 계속 흐릅니다.
타임아웃은 단순한 세부 사항이 아닙니다. 그것은 사용자가 무엇을 기다리게 할지에 대한 결정입니다.
4. 버그 #2 — 유튜브 영상이 나로 인식됨 (The YouTube Video Became Me)
제가 녹음을 시작하고, 영상을 재생한 다음, 제가 말을 하기 시작했습니다.
영상은 저로 레이블링되었습니다.
저는 '화자 2(Speaker 2)'로 레이블링되었습니다.
그리고 제 이름이 마침내 나타났을 때, 약 15초가 걸렸습니다.
원인 (The Cause)
어떤 음성도 검증되기 전에, 앱은 추측했습니다. '들리는 첫 번째 목소리가 아마 사용자일 것이다.'라고요.
조용한 방에서는 합리적입니다.
하지만 다른 것이 먼저 말하는 순간에는 틀립니다.
그리고 실제 검사가 필요한 경우 몇 초의 음성 시간과 처리 시간이 필요했기 때문에, 이 추측이 답변처럼 보이도록 화면에 오래 머물렀던 것입니다.
수정 (The Fix)
추측을 멈춥니다.
음성 지문(voiceprint)이 있다면, 엔진이 그것을 확인하기 전까지는 아무도 '당신'이 될 수 없습니다. 그때까지 이름은 **"확인 중..."**으로 흐릿하게 표시됩니다.
솔직함이 빠름보다 낫습니다.
하지만 15초는 여전히 느리게 느껴졌습니다.
5. 버그 #3 — 두 개의 마이크, 하나의 진실
그러다가 전용 회의록 도구들이 작동하는 방식에서 영감을 받은 더 나은 아이디어가 나왔습니다.
'누구 목소리처럼 들리는지'를 묻지 마십시오.
'어떤 마이크에서 왔는지'를 물으십시오.
Channel 0 → 당신의 마이크 → 당신
Channel 1 → 당신의 컴퓨터 → 통화에 참여한 다른 모든 사람
Deepgram은 하나의 연결로 두 채널을 모두 전사하고, 각 문장에 해당 채널 태그를 붙입니다. 당신의 말은 당신의 마이크에서 나오기 때문에, 당신의 이름이 즉시 나타날 수 있습니다.
혹은 그렇게 생각했습니다.
헤드폰 대신 스피커로 듣자, 당신의 마이크가 통화의 다른 쪽 소리도 듣게 되었습니다. 같은 문장이 두 번 도착했습니다. 한 번은 컴퓨터에서, 한 번은 제 마이크에서 왔습니다.
저는 에코 필터(echo filter)를 추가했습니다: 컴퓨터 채널이 2초 이내에 같은 단어를 말하면 마이크 라인을 끊도록 했습니다. 하지만
전송된 클립은 다음과 같았습니다:
- 잘못된 순간의 영상
- 잘못된 속도의 영상
- 마이크와 컴퓨터 오디오가 샘플별로 섞인 영상
해결책
각 목소리를 개별 채널에서, 올바른 속도로 잘라내야 합니다.
수정 후에는:
나 +9.98 → 인식됨, 고정됨
컴퓨터 오디오 -57 → 다른 사람의 음성으로 정확하게 분류됨
보너스로는, 제 목소리 체크가 더 이상 통화 상대방의 소리를 함께 담아내지 않는다는 점이 확인되었습니다.
7. 버그 #5 — "이 목소리 기억하기" 아무것도 기억하지 못함
스피커에게 "밀라나(Milana)"와 같은 이름을 지정하고 이 목소리 기억하기를 체크하면, 향후 녹음에서 그녀의 목소리를 자동으로 인식하는 기능이 있습니다.
저는 그녀에게 이름을 붙였습니다. 그리고 다시 녹음을 했습니다.
아무도 밀라나라는 이름으로 불리지 않았습니다.
원인
두 가지 문제가 겹쳤습니다.
첫째, 이름이 즉시 스크립트에 적용되었지만, 그녀의 목소리 지문(voiceprint)을 학습시키려면 약 20초 분량의 발화가 필요합니다. 충분하지 않을 경우, 학습은 조용히 실패했습니다. 화면에는 어느 쪽이든 성공으로 표시되었습니다.
둘째, 흥미로운 문제입니다.
20초를 채우기 위해, 저는 그녀의 짧은 문장들을 간격 없이 하나로 이어 붙였습니다. 엔진은 40초 중 사용 가능한 발화가 약 6초밖에 되지 않는다고 계속해서 감지했습니다.
같은 오디오라도, 일시 정지 구간을 남긴 채 하나의 연속된 흐름으로 업로드하면 즉시 등록되었습니다.
문장 사이의 급격한 끊김(hard cuts)이 엔진의 음성 감지를 혼란스럽게 만들었습니다.
해결책
- 가장 밀도가 높은 연속적인 발화 구간으로 학습시키기
- 학습에 실패할 경우, 그 사실을 알려주기
- 그녀의 발화가 더 많이 도착하면 자동으로 재시도하기
8. 버그 #6 — "Okafor"가 "Aquifer"로 변함
이제 단어 자체에 대한 이야기입니다.
추측하는 대신, 저는 측정했습니다. 알려진 스크립트를 정확한 제작 파이프라인을 통해 스트리밍하고 단어별로 비교했습니다.
9.8%의 단어 오류율(word error rate).
대부분의 오류는 이름에서 발생했습니다:
- Okafor → "Akhafar", "Aquifer"
- Priya Raghunathan → "Priyuragunathan"
해결책
모델에게 예상되는 이름을 알려주세요. Deepgram은 이를 키터ม 프롬프팅(keyterm prompting)이라고 부릅니다.
이 목록은 사용자의 이름, 저장된 목소리, 그리고 직접 편집할 수 있는 짧은 목록에서 가져옵니다.
3.3% 단어 오류율(word error rate). 모든 이름이 정확함.
문장 끝을 맺기 전에 더 오래 기다려보는 방법도 시도해 보았습니다. 그 결과가 더 나빠졌기 때문에 (12%), 이 방법은 사용하지 않았습니다. 솔직히 말씀드리자면, 저는 합성 음성(synthetic speech)으로 측정했습니다. 실제 방에 있는 실제 노트북 마이크로는 성능 향상이 덜할 것입니다. 하지만 이름은 사람들이 가장 먼저 확인하는 단어입니다.
9. 버그 #7 — 저장되지 않은 벡터들 (The Vectors That Were Never Stored)
이제 검색 측면을 다루겠습니다.
녹음이 저장된 후:
데이터베이스에 저장 (기록)
↓
앱에 즉시 응답
...
스크립트를 청크(chunking)하는 것은 기사를 청크(chunking)하는 것과는 다릅니다. 저는 **화자 전환(speaker turns)**을 기준으로, 약 1,200자 단위로 자르고, 각 줄은 "이름: 단어" 형식으로 작성했으며, 질문과 그 답변이 함께 유지되도록 이전 줄의 내용을 다음 청크에 포함시켰습니다.
모든 청크는 메타데이터를 저장합니다: 누구의 녹음인지, 어떤 작업 공간(workspace)인지, 어느 줄들인지, 그리고 정확한 순간으로 돌아가는 링크가 포함됩니다.
여기서는 개인 정보 보호가 중요했습니다. 각 사용자에게 인덱스 내에 자신만의 공간이 할당되므로, 한 사람의 사적인 대화가 다른 사람의 검색 결과로 나타날 수 없습니다. 게다가 모든 검색 결과는 표시되기 전에 데이터베이스를 통해 재확인됩니다.
배포됨. 테스트됨. 검색됨.
아무것도.
백그라운드 작업(background job) 상태에는 실패함(failed)이라고 나와 있었습니다:
이 인덱스에서 허용된 최대 네임스페이스에 도달했습니다 (100).
인덱스가 가득 찬 것입니다. 100개의 공간. 저희의 것은 101번째였습니다.
그리고 그것은 우리 기능만의 문제는 아니었습니다. 앱 전체의 모든 새 프로젝트와 작업 공간이 같은 방식으로 실패할 것이었습니다. 그 100개 중 18개는 이미 삭제된 항목에 속한 것이었습니다.
제가 그 안에 있는 동안, 또 다른 것을 발견했습니다.
삭제 호출(delete call)은 라이브러리가 객체로 감싸기를 기대했던 곳에 단순 ID 목록을 전달하고 있었습니다. 따라서 문서의 벡터를 삭제하는 것은 조용히 아무것도 삭제하지 않았습니다. 삭제된 콘텐츠는 여전히 검색 가능했습니다.
수정 사항 (The Fix)
- 삭제 기능이 수정되었습니다.
- 네임스페이스 제한은 계획 및 설계 결정 사항입니다 (업그레이드, 정리 또는 더 엄격한 필터를 가진 하나의 공간 공유). 이 글을 쓰는 동안 논의되고 있습니다.
그리고 제가 기쁜 부분은 바로 이것입니다.
데이터베이스는 기록이고 벡터는 단지 인덱스일 뿐이므로, 실패하더라도 아무것도 손실하지 않습니다. 모든 녹음은 저장되어 있으며, 공간만 생기면 즉시 인덱싱될 준비를 합니다.
10. 최종 아키텍처
Microphone ──┐
├──► Two-Channel Stream ──► Local Relay ──► Speech-to-Text
Computer ────┘ (이름 언급)
...
11. 배운 점들
1. 빠르기보다 정직함이 중요하다 (Honest beats fast)
자신만만한 오답 이름보다는 흐릿한 '식별 중...'이라는 메시지가 더 낫습니다.
2. 누가 말했는지 결정하기 전에 오디오가 어디서 왔는지 알아야 한다
채널은 저렴하고 강력한 증거입니다. 하지만 그것 자체가 증거는 아닙니다.
3. 모델이 이상한 점수를 줄 때는 무엇을 입력했는지 확인하라
-11이라는 점수는 음성 엔진의 잘못이 아니었습니다. 제 오디오 슬라이싱 때문이었습니다.
4. 튜닝하기 전에 측정하라 (Measure before you tune)
알려진 스크립트 하나와 단어 오류율(Word Error Rate)을 통해 문제가 모델이 아니라 이름에 있다는 것을 알게 되었습니다.
5. 기록과 인덱스를 분리 유지하라
데이터베이스가 진실이고 벡터가 그저 인덱스일 때, 벡터 서비스 중단은 불편함일 뿐입니다. 데이터 손실이 아닙니다.
6. 여러분의 벡터 데이터베이스에는 프로덕션에서나 만날 수 있는 한계가 있다
네임스페이스 제한. 배치(batch) 제한: 임베딩 모델은 호출당 96개의 입력을 허용하는데, 저희는 120개를 보내고 있었습니다.
7. 침묵하는 실패(Silent failures)가 최악의 실패다
'이 목소리를 기억해줘'와 삭제 기능 모두 아무것도 하지 않으면서 성공했다고 보고했습니다.
12. 최종 생각들
이 프로젝트를 시작하기 전까지, 누가 무엇을 말했는지 아는 것이 모델의 문제라고 생각했습니다.
좋은 음성 모델을 고르고. 좋은 음성 엔진을 고르면 끝이라고 생각했습니다.
저는 틀렸습니다.
문제는 다음과 같았습니다:
- 타임아웃(timeout) 문제
- 추측(guessing) 문제
- 오디오 형식 문제
- 채널 레이아웃 문제
- 슬라이싱 문제
- 벡터 데이터베이스 제한
- 침묵하는 실패
모든 수정 과정마다 또 다른 숨겨진 약점을 발견했습니다.
오늘날 이 앱은 통화 내용을 실시간으로 전사하고, 말에 적절한 이름을 붙이며, 통화의 다른 쪽을 분리하여, 팀이 찾을 수 있는 곳 어디든 모든 것을 저장할 수 있습니다.
가장 큰 교훈은 더 나은 모델을 고르는 것이 아니었습니다.
누가 무엇을 말했는지 아는 것은 단순히 모델의 문제가 아니라 파이프라인(pipeline)의 문제라는 것을 깨달았습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기