LabHub
시작하기
배우기 러닝패스 코스

음성 AI 에이전트 — 듣고, 찾고, 말하는 파이프라인

부분 결과에서 확정까지 — 스트리밍 ASR 과 Whisper 를 WER 로 잰다

LabHub 에서 이어서 보기

목표

WER 계산기를 직접 만들고, 스트리밍 ASR 과 오프라인 ASR 을 같은 발화로 비교해 정확도와 '말이 끝난 뒤 확정까지' 의 지연을 잰다. 전화 대역·샘플레이트 오기·소음이 WER 을 어떻게 바꾸는지 확인한다.

왜 중요한가

음성 비서가 대답을 시작하는 시각은 '확정 결과가 언제 나오는가' 에 묶여 있다. 스트리밍은 말하는 동안 받아 적어 확정이 곧바로 나오고, 오프라인은 말이 끝나야 시작한다. 정확도 숫자(WER)는 정규화와 집계 방식, 평가 자료에 따라 크게 달라지므로 계산기를 직접 만들어 봐야 숫자를 믿을 수 있다. 채점기는 여러분의 wer.py 를 숨긴 사례로 시험하고, 같은 모델을 다시 돌려 여러분의 결과와 대조한다.

단계

  1. wer(ref, hyp) 가 {S, D, I, N, wer} 를 돌려주는 /root/voice/asr/wer.py 를 만드세요(소문자·문장부호 제거·아포스트로피 유지 정규화).
  2. /opt/lab/fixtures/voice/librispeech/ls01~ls08.wav 를 스트리밍 ASR 에 100 ms 씩 넣어 부분 결과·확정 결과·확정 지연을 /root/voice/asr/stream.jsonl 에 적으세요.
  3. 확정 결과의 말뭉치 WER 을 /root/voice/asr/wer_stream.json 에 적으세요.
  4. 같은 8개를 Whisper tiny.en 으로 받아 적어 /root/voice/asr/whisper.jsonl 과 /root/voice/asr/wer_whisper.json 을 만드세요.
  5. 두 방식의 '말이 끝난 뒤 확정까지' 중앙값을 /root/voice/asr/latency.json 에 적으세요.
  6. 8 kHz 전화 녹음(phone_8k.wav)의 WER 을 16 kHz 원본(ls04)과 비교하고, 8 kHz 를 16000 이라고 속였을 때도 재서 /root/voice/asr/phone.json 에 적으세요.
  7. SNR 20 · 10 · 0 dB 의 흰 소음을 섞었을 때의 말뭉치 WER 을 /root/voice/asr/noise.json 에 적으세요.
  8. 측정을 모아 어떤 구성을 고를지 /root/voice/asr/report.json 에 적으세요.

참고

WER 계산기를 만든다

/root/voice/asr/wer.py 에 wer(ref, hyp) 를 만드세요. 두 글을 소문자로 바꾸고 아포스트로피를 뺀 문장부호를 지운 뒤 단어 단위 최소 편집 거리를 구해 {"S": 치환, "D": 삭제, "I": 삽입, "N": 정답 단어 수, "wer": (S+D+I)/N} 을 돌려줍니다(정답이 비면 wer 0). 채점기가 숨긴 사례 8건으로 시험합니다.

레벤슈타인 거리를 글자 대신 단어로 합니다. 표 d[i][j] 를 채운 뒤 오른쪽 아래에서 거꾸로 따라가며 어떤 편집이었는지 셉니다. "don't" 의 아포스트로피는 단어의 일부라 남겨야 합니다.

100 ms 씩 넣으며 부분 결과를 기록한다

/opt/lab/fixtures/voice/librispeech/refs.tsv 의 ls01~ls08 을 스트리밍 ASR 에 1600 샘플(100 ms)씩 넣으며 결과가 바뀔 때마다 {"audio_s": 지금까지 넣은 소리 길이, "text": 부분 결과} 를 모으고, 마지막 소리를 넣은 순간부터 확정 결과가 나올 때까지의 ms 를 final_ms 로 재서 /root/voice/asr/stream.jsonl 에 한 줄씩(id·partials·final·final_ms) 적으세요.

마지막 조각 뒤에 0.66초 무음을 넣고 input_finished() 를 불러야 마지막 낱말까지 나옵니다. final_ms 는 그 마무리에 걸린 시간 — 말이 끝난 뒤 사용자가 기다리는 몫입니다.

말뭉치 WER

여러분의 wer.py 로 8개 발화의 오류 수와 단어 수를 모두 더해 말뭉치 WER 을 구하고 /root/voice/asr/wer_stream.json 에 S·D·I·N·wer 로 적으세요.

파일별 WER 의 평균이 아닙니다. 긴 발화가 더 무겁게 반영되도록 오류 합 ÷ 단어 수 합으로 냅니다.

Whisper tiny.en 과 비교한다

같은 8개 발화를 voicekit.models.whisper() 로 한 번에 받아 적어 /root/voice/asr/whisper.jsonl(id·text·compute_ms·audio_s)을 만들고, 말뭉치 WER 을 /root/voice/asr/wer_whisper.json 에 적으세요.

오프라인 인식기는 s = rec.create_stream(); s.accept_waveform(sr, x); rec.decode_stream(s); s.result.text 입니다. Whisper 는 문장부호와 대소문자를 붙이므로 여러분의 정규화가 여기서 일합니다.

말이 끝난 뒤 확정까지

stream.jsonl 의 final_ms 와 whisper.jsonl 의 compute_ms 각각의 중앙값(가까운 순위: 정렬 후 ⌈0.5·n⌉번째 값)을 /root/voice/asr/latency.json 에 stream_final_ms_p50·whisper_ms_p50 으로 적으세요.

Whisper 는 말이 끝나야 시작하므로 compute_ms 전체가 사용자의 대기입니다. 스트리밍은 말하는 동안 대부분을 끝냈으니 final_ms 만 기다립니다.

8 kHz 전화 대역 — 그리고 샘플레이트를 속이면

ls04(16 kHz)와 같은 문장의 /opt/lab/fixtures/voice/librispeech/phone_8k.wav(8 kHz)를 스트리밍 ASR 로 받아 적어 ls04 정답 대비 WER 을 wer_16k·wer_8k 로, 8 kHz 샘플을 accept_waveform(16000, …) 으로 넣은 결과의 WER 을 wer_8k_as_16k 로 /root/voice/asr/phone.json 에 적으세요.

accept_waveform 의 첫 인자가 소리의 샘플레이트입니다. 8000 이라고 알려 주면 sherpa-onnx 가 안에서 16 kHz 로 바꿉니다. 16000 이라고 속이면 8,000개가 0.5초로 읽혀 소리가 두 배 빨라집니다.

소음이 셀수록

ls01~ls08(정렬 순서의 k 번째 파일)에 np.random.default_rng(100 + k).standard_normal(n) 소음을 SNR 20 · 10 · 0 dB(전력비)로 섞어 스트리밍 ASR(100 ms 조각)로 받아 적고, 말뭉치 WER 을 /root/voice/asr/noise.json 에 {"20": …, "10": …, "0": …} 로 적으세요.

배율은 모듈 1 과 같습니다: sqrt(P신호 / 10^(SNR/10) / P소음). 시드를 파일마다 고정하면 채점기가 같은 소음을 다시 만들어 대조할 수 있습니다.

무엇을 고를까

/root/voice/asr/report.json 에 wer_stream·wer_whisper·stream_final_ms_p50·whisper_ms_p50·wer_8k·wer_0db 를 앞 단계 파일에서 옮기고, choice 에 stream·whisper·two-pass 가운데 하나를 적으세요.

지연·정확도·자료의 닮음을 함께 봅니다. 이 자료(LibriSpeech)가 스트리밍 모델의 학습 자료와 같다는 점도 잊지 마세요.