Skip to content

문장 경계 pause — Duration Predictor

다문장을 분할 합성하면 문장 사이의 쉼 (pause) 이 소실되는 부작용이 발생합니다.

HayaKoe 는 Duration Predictor 를 재활용하여 각 문장 경계의 자연스러운 pause 시간을 직접 예측합니다.

Flow · Decoder 를 건너뛰고 TextEncoder + Duration Predictor 만 실행 하므로, 추가 비용이 낮습니다.

왜 문제인가

문장 분할의 이점

아키텍처 한눈에 에서 설명한 것처럼, HayaKoe 는 다문장 입력을 문장 단위로 쪼개 개별 합성합니다.

긴 텍스트를 통째로 넣으면 억양이 뭉개지거나 불안정해지는 경향이 있습니다.

문장 단위로 끊으면 각 문장마다 안정적인 prosody (운율) 가 보장됩니다.

분할의 부작용 — pause 소실

하지만 분할에는 부작용이 있습니다.

원본 SBV2 는 통문 합성에서 ., !, ? 같은 구두점 뒤에 자연스러운 쉼을 넣어줍니다.

문장 단위 분할을 하면 각 문장이 구두점에서 끝나고 다음 문장이 처음부터 시작되므로, 구두점 뒤의 쉼이 함께 사라집니다.

초기 구현에서는 문장 사이에 고정 80 ms 무음을 삽입했습니다.

실제로 Duration Predictor 가 예측하는 문장 경계 pause 는 0.3 ~ 0.6 초 수준이므로, 80 ms 는 이에 비해 매우 짧습니다.

결과적으로 "숨 돌릴 틈이 없는" 부자연스러운 발화가 만들어졌습니다.

원리 분석

이 절을 읽기 전에 Synthesizer 내부 흐름을 먼저 짚고 가겠습니다 (상세는 아키텍처 한눈에 — Synthesizer 참고).

1
Text Encoder

Transformer 인코더가 음소 시퀀스를 192차원 벡터로 임베딩합니다. BERT 특징이 여기서 음소 레벨 임베딩과 결합되어, 문장 맥락이 처음으로 음소에 주입됩니다.

2
Duration Predictor

각 음소를 몇 프레임 동안 발음할지 예측합니다. 안정적이지만 단조로운 DDP (결정적) 와, 자연스럽지만 불안정한 SDP (확률적) 두 predictor 의 출력을 sdp_ratio 로 블렌딩하여 안정성과 자연스러움의 균형을 잡습니다. 이 단계에서 음소 시퀀스가 시간 축으로 늘어납니다.

3
Flow

Normalizing Flow (가역 신경망) 의 역변환을 거쳐, Text Encoder 가 만든 가우시안 분포 (평균·분산) 를 실제 음성의 복잡한 분포로 변형하여 latent z 벡터를 생성합니다. 학습 때는 정방향 (음성 → 텍스트 공간), 추론 때는 역방향 (텍스트 → 음성 공간) 으로 동작합니다.

4
Decoder

HiFi-GAN 계열의 보코더로, latent z 를 ConvTranspose 업샘플링과 잔차 블록 (ResBlock) 을 거쳐 시간 도메인의 실제 파형 (44.1 kHz) 으로 생성합니다. Synthesizer 서브 모듈 중 연산량이 가장 크며, CPU 추론 시간의 대부분이 여기서 소비됩니다.

이 문서에서 다루는 핵심은 1 · 2 단계 (Text Encoder + Duration Predictor) 까지만 따로 실행 하는 것입니다.

3 · 4 단계 (Flow + Decoder) 를 건너뛰므로 비용이 매우 낮습니다.

원본 모델은 pause 를 어떻게 만들었나

원본 SBV2 가 통문 합성에서 자연스러운 pause 를 생성하는 원리를 추적한 결과, Duration Predictor 가 구두점 음소의 frame 수를 예측하는 부수 효과 였습니다.

Duration Predictor 는 원래 "각 음소를 몇 프레임 동안 발음할지" 를 예측하는 모듈입니다.

"안" 은 5 프레임, "녕" 은 4 프레임 같은 식입니다.

그런데 ., !, ? 같은 구두점도 음소 시퀀스에 포함됩니다.

Duration Predictor 가 구두점에 대해 예측한 frame 수가 곧 해당 구두점 위치에서의 쉼 길이 가 됩니다.

예를 들어 . 에 20 프레임이 예측되면 Synthesizer 는 그 구간 동안 무음 또는 묵음에 가까운 파형을 생성합니다.

분할 합성에서는 구두점 위치에서 합성이 끊기므로 이 정보가 그대로 폐기되고 있었습니다.

Duration Predictor 의 내부 동작

Duration Predictor 의 예측 흐름을 더 자세히 보면, 두 개의 서브 모듈이 병렬로 동작합니다.

DDP (Deterministic Duration Predictor) 는 같은 입력에 대해 항상 같은 duration 을 출력합니다.

안정적이지만 발화가 기계적으로 단조롭게 들릴 수 있습니다.

SDP (Stochastic Duration Predictor) 는 같은 입력에 대해 매번 약간 다른 duration 을 출력합니다.

확률 샘플링 기반이라 자연스러운 변동이 생기지만, 그만큼 결과가 덜 안정적입니다.

두 predictor 의 출력은 sdp_ratio 파라미터로 블렌딩됩니다.

sdp_ratio=0.0 이면 DDP 만, 1.0 이면 SDP 만, 0.5 면 반반 섞은 결과를 사용합니다.

length_scale (= speed 파라미터) 은 예측된 duration 전체에 곱해져 말속도를 조절합니다.

최종적으로 ceil() 로 올림하면 각 음소의 정수 프레임 수 가 결정됩니다.

blank token 과 구두점

pause 계산 시 한 가지 주의할 점이 있습니다.

원본 SBV2 는 음소 시퀀스의 모든 음소 사이에 blank token (공백 토큰, ID = 0) 을 삽입하는 구조입니다. HayaKoe 도 이 동작을 그대로 따릅니다.

원본:  [は, い, .]
삽입 후: [0, は, 0, い, 0, ., 0]

blank token 에도 duration 이 예측되므로, 구두점 . 의 pause 를 구할 때는 구두점 자체 + 앞뒤 blank 의 duration 을 합산 해야 합니다.

예: . = 20 프레임, 앞 blank = 3 프레임, 뒤 blank = 5 프레임 → 총 28 프레임

구현

핵심 아이디어

핵심은 간단합니다.

전체 원문 텍스트를 TextEncoder + Duration Predictor 까지만 통과시켜, 구두점 위치의 frame 수를 얻는 것 입니다.

Flow 와 Decoder 는 건너뜁니다.

Synthesizer 전체 pass 에서 비용의 대부분은 Flow 와 Decoder 에서 발생하므로 (ONNX 최적화 참고), Duration Predictor 까지만 실행하는 비용은 상대적으로 낮습니다.

전체 텍스트 (분할 전 원문)

  ├─ TextEncoder (G2P → 음소열 → 임베딩)

  ├─ Duration Predictor (음소별 frame 수 예측)
  │     └─ 구두점 위치의 frame 수만 추출

  └─ pause 시간 계산
        frames × hop_length / sample_rate = 초

전체 합성에서는 이미 분할된 개별 문장을 각각 TextEncoder → Duration Predictor → Flow → Decoder 로 통과시킵니다.

pause 예측에서는 분할 전 원문을 통째로 TextEncoder → Duration Predictor 만 통과시킵니다.

분할 전 원문을 사용하는 이유는, 문장 경계의 구두점이 원문에서만 온전히 존재하기 때문입니다.

개별 문장으로 쪼갠 뒤에는 마지막 문장의 구두점 외에는 경계 구두점이 사라지거나 위치가 달라집니다.

pause 시간 계산

구두점 위치의 frame 수를 얻었으면 초 단위로 변환합니다.

pause (초) = frames × hop_length / sample_rate

HayaKoe 의 기본 설정에서 hop_length = 512, sample_rate = 44100 이므로, 1 프레임은 약 11.6 ms 에 해당합니다.

예를 들어 구두점 + 인접 blank 의 합산 frame 수가 35 라면:

35 × 512 / 44100 ≈ 0.41 초

실제 구현 (durations_to_boundary_pauses()) 에서는 다음 과정을 거칩니다.

  1. 전체 음소 시퀀스에서 문장 경계 구두점의 위치 를 찾습니다 (., !, ? 에 해당하는 음소 ID).
  2. 각 구두점 위치에서 해당 음소의 duration 을 가져옵니다.
  3. 앞쪽 인접 토큰이 blank (ID = 0) 이면 그 duration 도 더합니다.
  4. 뒤쪽 인접 토큰이 blank (ID = 0) 이면 그 duration 도 더합니다.
  5. 합산된 frame 수를 frames × hop_length / sample_rate 로 변환합니다.

문장이 N 개면 경계는 N − 1 개이므로, 결과는 N − 1 개의 pause 시간 리스트입니다.

trailing silence (꼬리 무음) 보상

한 가지 더 고려해야 할 점이 있습니다.

Synthesizer 가 각 문장을 합성할 때, 문장 끝부분에 이미 짧은 무음이 포함 될 수 있습니다.

이 trailing silence 를 무시하고 예측된 pause 를 그대로 삽입하면 실제 쉼이 과도하게 길어집니다.

HayaKoe 는 합성된 오디오의 뒤쪽에서 무음 구간을 직접 측정합니다.

측정 방식은 오디오 끝에서부터 10 ms 윈도우를 한 칸씩 앞으로 옮기며, 피크 진폭의 2 % 이하인 구간 을 무음으로 판정합니다.

이후 pause 삽입 시에는 예측된 목표 pause 시간에서 trailing silence 를 빼서, 부족분만 무음 샘플로 보충 합니다.

추가 무음 = max(0, 예측 pause − trailing silence)

모델이 이미 충분한 무음을 생성했다면 추가 삽입은 0 이 됩니다.

목표 pause 시간 자체를 최소 80 ms 로 하한을 두기 때문에, 예측값이 아무리 짧아도 문장 사이 총 무음은 항상 80 ms 이상이 됩니다.

ONNX 지원

PyTorch 경로에서는 모델 내부 모듈을 개별 호출할 수 있어 Duration Predictor 만 따로 실행하면 됩니다.

반면 synthesizer.onnx 는 Synthesizer 전체를 하나의 종단 간 그래프로 내보낸 형태라 중간 출력을 꺼낼 수 없습니다.

이를 해결하기 위해 TextEncoder + Duration Predictor 만 포함하는 별도 ONNX 모델 (duration_predictor.onnx, ~30 MB, FP32) 을 추가로 export 했습니다.

개선 효과

pause 시간 분포

동일 텍스트에 대해 자동 예측된 문장 경계 pause 입니다.

백엔드pause 범위
GPU (PyTorch)0.41 s ~ 0.55 s
CPU (ONNX)0.38 s ~ 0.57 s

두 백엔드의 차이는 SDP 의 stochastic sampling (확률적 샘플링) 특성상 발생하는 편차 수준입니다.

SDP 는 확률 샘플링 기반이라 같은 입력이라도 호출마다 결과가 약간 달라집니다.

GPU 와 CPU 의 차이가 이 자연 변동폭 안에 들어오므로, ONNX 변환으로 인한 품질 손실은 무시할 수 있습니다.

Before / After

旅の途中で不思議な街に辿り着きました。少し寄り道していきましょう。きっと楽しい発見がありますよ。

つくよみちゃんpause 방식 = After (DP 예측)Duration Predictor 가 문장 경계 pause 를 자동 예측
0:00 / 0:00

비용

추가 비용은 TextEncoder + Duration Predictor 1 회 실행입니다.

ONNX 최적화 — Synthesizer 비중 에서 확인할 수 있듯, Synthesizer 가 전체 CPU 추론 시간의 64 ~ 91 % 를 차지하며, 그 중 대부분은 Flow + Decoder 에서 소비됩니다.

Duration Predictor 까지만 실행하는 비용은 이에 비해 낮으므로, pause 예측으로 인한 체감 지연은 거의 없습니다.

관련 커밋

  • c57e0ad — Duration Predictor 기반 pause 예측으로 다문장 합성 자연스럽게 개선
  • 5522db1 — ONNX duration_predictor 추가로 CPU 백엔드도 자연스러운 문장 경계 무음 지원

향후 과제

  • 감정별 pause 길이 분화 — 기쁨은 짧게, 슬픔은 길게 등 감정 스타일에 따라 pause 분포를 다르게 적용
  • 쉼표 · 콜론 등 세분화 — 현재는 문장 종결 구두점 (. ! ?) 만 대상이지만, 쉼표 (,, ) 나 콜론 등 긴 호흡이 필요한 위치에 대한 추가 세분화
  • pause 직접 제어 API — 사용자가 특정 문장 경계의 pause 길이를 명시적으로 지정할 수 있는 인터페이스