음성 AI 에이전트 — 듣고, 찾고, 말하는 파이프라인
받아 적기 — 스트리밍 트랜스듀서와 Whisper, 그리고 WER 이 숨기는 것
한 줄 요약
스트리밍 ASR 은 소리가 들어오는 동안 부분 결과를 계속 고쳐 내고, 말이 끝나면 짧은 마무리 뒤 확정 결과를 낸다. 오프라인 ASR(Whisper)은 발화를 통째로 받아야 시작한다. 정확도는 WER — (치환 + 삭제 + 삽입) ÷ 정답 단어 수 — 로 재는데, 이 숫자는 평가 자료가 모델의 학습 자료와 얼마나 닮았는가에 크게 흔들린다.
왜 이게 필요했나
음성 비서의 대답은 '말이 끝났다' 는 판정 뒤에 시작한다(모듈 2). 그 순간 인식 결과가 이미 거의 다 나와 있으면 곧바로 다음 단계로 넘어가고, 그때부터 인식을 시작하면 발화 길이에 비례해 기다린다. 이 차이가 스트리밍을 쓰는 이유다. 동시에 부분 결과는 '듣고 있다' 는 신호가 되고, 검색을 미리 시작하는 근거가 된다.
그런데 모델을 고를 때 처음 붙잡는 숫자인 WER 은 생각보다 많은 것을 숨긴다. 이 이미지에 넣을 모델을 고르던 중 실제로 겪은 일이 있다. 처음 고른 20M 짜리 스트리밍 모델은 공개 평가에서 멀쩡했는데, 이 코스의 질의를 넣자 스트림의 첫 1~2초를 통째로 잃었다 — "How late can I cancel without paying the fee?" 가 "LE WITH THAT PAIN THE FEET" 로 나왔다. 같은 파일을 두 번 이어 붙이면 두 번째는 정확했다. 평균 WER 로는 보이지 않는 실패였고, 모델을 바꾼 이유다(labs/voice/Dockerfile 에 기록).
어떻게 동작하나
트랜스듀서(RNN-T). 이 이미지의 스트리밍 모델은 icefall 의 zipformer 트랜스듀서다(LibriSpeech 로 학습, Apache-2.0). 인코더가 소리 조각을 받아 표현을 만들고, 조인너가 '이 시점에 무엇을 내보낼지(또는 아무것도 안 낼지)' 를 정한다. 인코더는 정해진 크기의 조각과 제한된 왼쪽 문맥만 보도록 학습돼서(모델 이름의 chunk-16 · left-128), 미래를 기다리지 않고 조각마다 결과를 낸다. 그래서 부분 결과가 나오고, 마지막 조각은 뒤에 짧은 무음(이 코스에서는 0.66초)을 넣어 마무리시킨다. 이 마무리는 수십 ms 면 끝난다 — 이 이미지에서 8개 발화의 '마지막 소리 뒤 확정까지' 중앙값은 44 ms 였다(맥의 도커, CPU 2개).
Whisper. OpenAI 의 Whisper(Radford et al., 2022)는 68만 시간의 웹 자료로 학습한 인코더-디코더 모델이다. 소리를 30초 창의 로그-멜 스펙트로그램으로 바꿔 인코더에 넣고, 디코더가 글자를 한 토큰씩 쓴다. 발화 전체를 본 뒤에 쓰므로 문장부호·대소문자까지 붙은 결과가 나오지만, 말이 끝나야 시작할 수 있다. 같은 조건에서 발화 하나에 734 ms(중앙값)가 걸렸다. 가장 작은 tiny.en(3,900만 매개변수)의 int8 판을 넣었다.
부분 결과는 흔들린다. 스트리밍 모델은 뒤 문맥을 보고 앞 낱말을 고치기도 한다. 화면에 부분 결과를 띄우면 사용자는 글자가 바뀌는 것을 본다. 그래서 흔히 '안정된 앞부분' 만 굵게 보이거나, 검색 같은 되돌리기 어려운 일은 확정 결과로만 한다.
WER 과 정규화. WER 은 단어 단위 편집 거리다. 비교 전에 반드시 정규화한다 — 대소문자, 문장부호, 아포스트로피. 정규화 없이 "Sunday?" 와 "SUNDAY" 를 비교하면 모델이 아니라 표기 규칙을 채점하게 된다. 말뭉치 WER 은 파일별 WER 의 평균이 아니라 오류 수 합 ÷ 단어 수 합이다. 짧은 발화 하나의 100% 오류가 평균을 망치지 않게 하려는 것이다.
WER 은 자료를 탄다. 이 이미지에서 잰 숫자 셋을 나란히 두면 분명해진다. LibriSpeech test-clean 8개 발화에서 스트리밍 zipformer 의 WER 은 0.95%, Whisper tiny.en 은 6.7% 였다 — zipformer 가 바로 그 코퍼스로 학습했기 때문이다. 그런데 합성 음성으로 만든 전화 질의 12개(모듈 9)에서는 같은 zipformer 가 20% 가까이 틀렸다("GUYS" → "GUISE", "refill" → "ORIFYL"). 평가 자료가 학습 자료와 닮을수록 숫자는 좋아 보인다. 모델을 고를 때는 여러분의 서비스와 닮은 자료로 재야 한다.
샘플레이트는 정직하게. sherpa-onnx 는 16 kHz 가 아닌 소리를 받으면 스스로 바꿔 넣는다. 8 kHz 전화 녹음을 8000 이라고 알려 주면 이 실습 자료에서 WER 0% 였지만, 16000 이라고 속이면 소리가 두 배 빨라져 100% 가 틀렸다. 소음은 다르다. 같은 8개 발화에 흰 소음을 섞으면 SNR 20 dB 에서 0%, 10 dB 에서 2.9%, 0 dB 에서 24.8% 로 올랐다.
현장에서 만나는 모습
두 단계 인식(two-pass). 스트리밍 모델로 부분 결과를 바로 보여 주고, 말이 끝나면 더 큰 오프라인 모델로 한 번 더 받아 적어 확정을 고치는 구성이 흔하다. 지연 예산이 허락하는 만큼만 두 번째 패스에 쓴다. LabHub 회화 연습의 ASR 은 GPU 에 올린 큰 모델을 녹음 끝에 한 번 부르는 방식이고, 가중치가 내려가 있으면 첫 요청이 78.7초, 올라가 있으면 0.4초였다 — 그래서 자리를 잡는 순간 무음을 보내 미리 올린다(backend/app/lang_talk.py 의 warm_asr). 큰 모델을 쓰면 '예열' 이 설계의 일부가 된다.
다음 실습에서 할 것
WER 계산기를 만들어 숨긴 사례로 시험받고, LibriSpeech 8개 발화를 100 ms 씩 스트리밍으로 받아 적어 부분 결과와 확정 지연을 기록한다. 같은 발화를 Whisper 로 받아 적어 WER 과 지연을 비교하고, 8 kHz 전화 대역과 샘플레이트를 속인 경우, 소음 세기에 따른 WER 을 잰 뒤 어떤 구성을 고를지 보고서에 적는다.