2-호스트 신경망 TTS 파이프라인 CI 구축 중 겪은 네 가지 edge-tts 동작 특성
요약
GitHub Actions CI 환경에서 edge-tts를 활용한 신경망 TTS 파이프라인 구축 중 발생한 문제와 해결책을 다룹니다. 무음 오디오 파일 생성 방지를 위한 파일 크기 검사 및 폴백(fallback) 음성 설정의 중요성을 설명합니다.
핵심 포인트
- edge-tts 사용 시 네트워크나 로캘 문제로 무음 파일이 생성될 수 있음
- CI 환경에서는 생성된 오디오 파일의 크기를 검사하여 유효성을 확인해야 함
- 음성 합성 실패 시 폴백(fallback) 음성을 사용하여 안정성을 확보할 것
- 오디오와 이미지 클립의 싱크를 맞추기 위해 ffprobe를 통한 길이 측정이 필요함
이 2-호스트 YouTube 롱폼 파이프라인은 edge-tts를 사용하여 교사-학생 간의 대화를 합성하고, ffmpeg를 사용하여 클립들을 16:9 비디오로 연결합니다. 이 모든 과정은 사운드카드나 디스플레이 서버 없이 GitHub Actions 내부에서 이루어집니다. 사양 형식과 호스트 음성 설계는 이전 포스트에서 다루었습니다. 이 글은 로컬 테스트 실행에서 CI 전용 렌더링으로 전환한 후 제가 맞닥뜨린 네 가지 edge-tts 동작에 관한 것입니다.
1. 성공적인 종료 코드, 하지만 무음 오디오 파일
edge-tts는 종료 코드 0을 반환하며 기술적으로는 유효한 오디오인 mp3 파일을 생성할 수 있습니다. 다만, 그 파일은 거의 무음에 가까운 22바이트일 뿐입니다. edge-tts는 Microsoft Edge의 온라인 TTS 서비스를 래핑(wrap)한 것으로, 브라우저의 SpeechSynthesis API와 동일한 합성 백엔드를 사용합니다. 이런 현상은 음성 이름(voice name)은 확인되지만, Azure TTS 백엔드가 빈 합성 결과(typically a transient network condition or a locale mismatch, 일반적으로 일시적인 네트워크 상태 또는 로캘 불일치)를 반환할 때 발생합니다.
build_longform.py에서의 해결책은 서브프로세스(subprocess) 호출 후 명시적인 크기 검사를 수행하는 것입니다:
def tts(text: str, speaker: str, out_mp3: str):
for voice in (VOICE.get(speaker, VOICE["A"]), VOICE_FALLBACK.get(speaker, "en-US-GuyNeural")):
r = subprocess.run(
...
크기 검사가 없다면, 빌드는 성공하지만 무음 비디오가 생성될 것입니다. 이는 CI를 통과하고 깨진 파일을 업로드하게 되므로 최악의 결과입니다. 1000바이트 임계값은 보수적인 설정입니다. 48kbps 속도의 1초짜리 신경망 TTS mp3는 약 6000바이트입니다.
VOICE = {
"A": os.environ.get("LF_VOICE_A", "en-US-GuyNeural"),
"B": os.environ.get("LF_VOICE_B", "en-US-AvaNeural"),
...
tts() 함수는 먼저 기본 음성 (primary voice)을 시도하고, 그다음 폴백 (fallback) 음성을 시도한 뒤, 실패하면 특정 에러 메시지와 함께 종료됩니다. 호스트 B의 경우, 폴백 음성이 AvaNeural에서 AriaNeural로 전환되는데, 두 음성 모두 유사한 운율 (prosody)을 가진 미국식 영어 여성 음성입니다. 20개 세그먼트로 구성된 영상에서 이 차이는 의도된 음성을 알고 듣는 사람에게는 들릴 수 있지만, 의도된 음성을 모르는 시청자에게는 방해가 되지 않습니다.
두 실패 상황 모두에서 sys.exit()를 호출하는 것은 의도된 설계입니다. 세그먼트를 조용히 건너뛰면 대사가 누락된 영상이 생성되는데, 이는 빌드 실패보다 더 나쁜 상황입니다. CI 실패는 복구가 가능하지만, 대사가 누락된 채 게시된 영상은 복구가 불가능하기 때문입니다.
3. 세그먼트 길이는 단어 수가 아니라 ffprobe에서 가져와야 함
각 세그먼트는 정지 이미지 클립과 해당 세그먼트의 오디오로 구성됩니다. 이미지 클립은 오디오의 길이와 정확히 일치해야 합니다. 그렇지 않으면 클립들을 연결 (concatenation)하는 과정에서 싱크가 어긋나게 됩니다.
저는 단어 수(분당 180단어를 근사치로 사용)를 통해 길이를 추정하려고 시도했습니다. 하지만 결과는 지속적으로 15~25% 정도 오차가 발생했습니다. 특히 edge-tts가 음절 사이에 일시 정지 (pause)를 두는 기술 용어들의 경우 오차가 심했습니다. 숫자, 약어, 그리고 제품명 ("ffprobe", "SPDX", "GuyNeural")은 특히 예측하기 어려웠습니다.
올바른 접근 방식은 합성 (synthesis) 후 ffprobe를 사용하는 것입니다:
def duration(path: str) -> float:
r = run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", path])
...
-show_entries format=duration은 오디오를 디코딩 (decoding)하지 않고 컨테이너 헤더 (container header)를 읽습니다. 이는 CI 러너 (runner)에서 세그먼트당 약 30ms가 소요됩니다. 20개 세그먼트로 구성된 영상의 경우 600ms의 오버헤드 (overhead)가 발생하지만, 이는 TTS 합성 시간(줄당 1~3초)에 비하면 무시할 수 있는 수준입니다.
출력값에 직접 float()를 사용하는 것은 ffprobe가 깨끗한 숫자 문자열을 생성해야 함을 의미합니다. -of default=noprint_wrappers=1:nokey=1 플래그는 레이블 접두사가 붙지 않도록 보장합니다. 첫 실행 시 noprint_wrappers=1을 잊어버려 출력이 3.824000 대신 duration=3.824000으로 나오는 바람에 ValueError가 발생했습니다.
4. 음성 실험에는 코드 변경이 아닌 환경 변수 (env var) 재정의가 필요함
음성 쌍(GuyNeural, AvaNeural)은 TTS 품질 지표를 개별적으로 평가하는 대신, 전체 테스트 렌더링을 들어보고 결정되었습니다. 코드 주석에는 이 결정이 내려진 시점(# CEO選定 2026-05-25)이 기록되어 있어, 향후 변경 사항이 발생하더라도 커밋 메시지를 뒤질 필요 없이 git blame을 통해 추적할 수 있습니다.
음성이 환경 변수(environment variables)를 통해 설정 가능하기 때문에, 새로운 음성을 실험할 때 코드 변경이 전혀 필요하지 않습니다:
VOICE = {
"A": os.environ.get("LF_VOICE_A", "en-US-GuyNeural"),
"B": os.environ.get("LF_VOICE_B", "en-US-AvaNeural"),
...
GitHub Actions 워크플로에서 workflow_dispatch 입력을 통해 LF_VOICE_A=en-US-BrianNeural을 전달하면, 특정 음성을 확정하기 전에 하나의 영상에 새로운 음성을 테스트해 볼 수 있습니다. 환경 변수 재정의(env override) 지원이 없다면, 모든 음성 변경은 코드 커밋과 전체 CI 사이클을 거쳐야 합니다. 이는 궁극적으로 미적 판단(aesthetic judgment)의 영역인 사항에 대해 너무 느린 피드백 루프를 만듭니다.
이와 동일한 패턴이 두 음성 각각에 독립적으로 적용됩니다. 호스트 B는 유지한 채 호스트 A만 교체함으로써, 두 명을 모두 바꾸기 전에 대비(contrast)를 평가할 수 있습니다.
네 가지 동작을 종합하면 다음과 같습니다: 합성 후 출력 크기 확인, 이름이 지정된 폴백(fallback) 음성 제공, ffprobe를 이용한 재생 시간 측정, 그리고 코드 변경 없이 음성을 설정 가능하게 만들기입니다. 이 중 어느 것도 edge-tts 문서에는 나와 있지 않으며, 모두 로컬 실행에서는 드러나지 않았던 CI 실패를 통해 얻은 결과물입니다.
CI 파이프라인을 위한 무료 신경망 TTS 옵션 비교에서는 왜 다른 대안들 대신 edge-tts가 선택되었는지를 다룹니다. ffmpeg 슬라이드 렌더러와의 통합 방식은 이전 파이프라인 포스트에 설명되어 있습니다.
세 개의 AI 큐레이션 디렉토리 사이트를 운영하는 6개월간의 지속적인 실험 중 일부입니다. 여기에 기술된 주장들은 사실이며, 이 글은 AI의 도움을 받아 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기