.NET용 F5-TTS v0.3.0: 스트리밍(Streaming), 올바르게 읽는 숫자, 그리고 내가 측정 과정에서 사라뜨린 기능
요약
.NET용 F5-TTS v0.3.0 업데이트를 통해 오디오 스트리밍 기능과 텍스트 정규화 기능이 추가되었습니다. 스트리밍을 통해 첫 오디오 도달 시간을 단축하고, 정규화기를 통해 숫자, 날짜, 통화 등을 정확하게 발음할 수 있도록 개선되었습니다.
핵심 포인트
- SynthesizeStreamAsync를 통한 오디오 스트리밍 지원으로 대기 시간 단축
- 스트리밍된 청크와 전체 렌더링 결과가 바이트 단위로 동일함 보장
- 독일어 및 영어 텍스트 정규화기로 숫자, 날짜, 통화 등의 정확한 발음 구현
- 정규화된 텍스트 사용 시 데이터 분포 외 문제 해결 및 발음 품질 향상
Horus.F5Tts.Onnx
v0.3.0에 반영된 내용은 다음과 같습니다:
스트리밍 (Streaming): 첫 문장을 더 빨리 듣기
단락(paragraph)의 경우, SynthesizeLongAsync는 모든 문장이 완료될 때까지 아무것도 제공하지 않습니다. 대화형 앱에서 실제로 고통을 주는 대기 시간은 바로 _첫 오디오 도달 시간(time-to-first-audio)_이며, v0.3.0은 바로 이 점을 해결합니다.
await foreach (var chunk in model.SynthesizeStreamAsync(reference, refText, paragraph))
player.Write(chunk.Samples); // sentence chunk.Index + 1 of chunk.Count
제가 중요하게 생각하는 부분은 이것이 두 번째 렌더링이 아니라는 점입니다. 청크(chunk)들은 동일한 문장 단위 조각들이며, 동일한 방식으로 크로스페이드(cross-faded)됩니다. 따라서 모든 스트리밍된 청크를 연결하는 것은 동일한 입력과 시드(seed)에 대해 SynthesizeLongAsync를 사용하는 것과 **바이트 단위로 완전히 동일(byte-for-byte identical)**합니다. 저는 이를 두 번 증명했습니다. 모델이 없는 순수한 크로스페이드 수학 연산으로서, 그리고 모델을 대상으로 한 엔드 투 엔드(end-to-end) 테스트로서 증명했으며, 이후 실시간으로 확인했습니다. 스트리밍된 WAV와 스트리밍되지 않은 WAV는 동일한 SHA-256 해시값을 공유합니다.
이는 프레임(frame) 단위가 아닌 청크(chunk) 단위이며 (F5는 각 청크를 하나의 전체로 렌더링합니다), 따라서 이점은 첫 번째 청크가 일찍 도착한다는 것이며, 텍스트 길이에 따라 그 이점이 커집니다.
텍스트 정규화기 (Text normalizers): 올바르게 읽는 숫자
체크포인트(Checkpoints)는 정규화된(normalized) 텍스트로 학습됩니다. 모델에 가공되지 않은 50 %, 1.000 €, z.B., 3.8.2026 또는 14:30을 입력하면 모델이 이를 삼켜버리거나 웅얼거립니다. 이는 분포 외 데이터(out-of-distribution)이기 때문입니다. v0.3.0은 독일어와 영어에 대한 준비된 정규화기(normalizers)를 제공합니다:
options.TextNormalizer = GermanTextNormalizer.Normalize;
// "am 3.8.2026 um 14:30 Uhr" -> "am dritten August zweitausendsechsundzwanzig um vierzehn Uhr dreißig"
...
이 정규화기들은 기수(cardinal numbers), 퍼센트(percent), 센트가 포함된 통화(currency), 소수 및 천 단위(두 언어 간에 ,/. 관습이 바뀜), 날짜, 시각, 서수(ordinals, 독일어는 앞 단어에 따라 굴절됨 — am → -ten, der → -te, 없음 → -ter), 약어 및 몇 가지 기호를 다룹니다. 인식된 패턴만 수정되며, 산문(prose)과
그 차이는 귀로 들을 수 있습니다. 동일한 독일어 문장이 원문 그대로(raw)는 5.2초, 정규화(normalized) 시에는 8.3초가 걸렸습니다. 추가된 초들은 숫자가 건너뛰어지는 대신 실제로 발음되었음을 의미합니다.
그리고 읽기 방식이 '추측'이 될 수 있는 부분은 의도적으로 그대로 두었습니다. 문장 경계에 있는 독일어 기수(bare ordinals)는 기수(cardinals)로 유지되며, 영어 숫자 날짜(3/8/2026 — 이것이 3월 8일인지 8월 3일인지?)는 건드리지 않았습니다. 잘못된 읽기는 그냥 읽는 것보다 더 나쁩니다.
PreparedVoice — 그리고 내가 측정 과정에서 없애버린 기능
한 목소리가 여러 줄을 말할 때, 이제 참조(reference)를 한 번만 바인딩할 수 있습니다:
var voice = model.PrepareVoiceFromWav("reference.wav", refText);
var a = await voice.SynthesizeAsync("First line.");
var b = await voice.SynthesizeLongAsync(wholeParagraph);
본래 저는 이것을 호출 간에 참조 처리를 캐싱(cache)하는 '성능(performance)' 기능으로 만들고 싶었습니다. 그래서 한 번의 합성(NFE 32)을 측정해 보았습니다:
| 단계 | 시간 | 점유율 |
|---|---|---|
| 전처리 (유일한 참조 의존 단계) | 50 ms | 0.3 % |
| ... |
참조를 캐싱하면 약 0.3%를 절약할 수 있지만, F5가 생성된 텍스트와 동일한 그래프 패스(graph pass)로 참조를 융합(fuse)하기 때문에 캐싱조차 할 수 없습니다. 따라서 PreparedVoice는 사용 편의성(ergonomics)만을 위해 제공되며, 문서에도 정확히 그렇게 명시되어 있습니다. 이번 릴리스의 진정한 지연 시간(latency) 이점은 처리량(throughput)이 아니라 스트리밍(streaming, 첫 오디오 출력 시간)입니다.
이것이 제가 이 라이브러리에 적용하는 규칙입니다: 구축하기 전에 측정하고, 가지고 있지 않은 수치를 팔지 마세요.
0.3.0의 다른 변경 사항
전체 요청에 걸쳐 적용되는 IProgress<F5TtsProgress> 진행 상황 보고와 238개의 테스트(스트리밍 보장 및 약 124개의 정규화 케이스 포함)가 추가되었습니다.
피드백, 이슈 및 PR(Pull Request)을 환영합니다. 🙌
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기