MyShorts - Typecast 도입과 WAV 싱크 자동 보정 설계

MyShorts의 첫 번째 쇼츠는 개발자와 ChatGPT가 대화를 주고받는 짧은 상황극이다. 영상 길이도 길지 않아 처음에는 오픈소스 TTS로 충분할 거라고 생각했다. 그런데 실제 음성을 붙여보니 단순히 한국어를 읽는 것과 상황에 맞게 대화하는 것 사이에는 차이가 있었다.

 

상용 TTS를 검토하고 Typecast Smart Emotion으로 방향을 바꾼 뒤에는 음성 자체보다 더 중요한 문제가 남았다. ChatGPT가 예상한 대사·장면 시간과 실제 WAV 재생시간을 어떻게 맞출 것인가. 이번 작업에서는 그 문제에 대한 구조를 내가 제안하고, ChatGPT와 검토한 다음 Codex로 구현·검증하는 순서로 진행했다.

오픈소스 TTS의 한계

첫 에피소드에는 “야, ChatGPT! 너 코딩 잘해?”라는 질문에 “당근!!! 대박 잘하지!!!”라고 받아치는 장면이 나온다. 짧은 대사지만 질문하는 개발자의 태도와 자신 있게 대답하는 AI의 분위기가 살아야 상황극이 자연스럽다.

 

오픈소스 TTS도 음성 생성은 가능했지만, 짧은 대사를 연달아 재생하면 말투와 감정 표현이 기대에 미치지 못했다. 대사 수가 적은 쇼츠에서는 이런 어색함이 오히려 더 눈에 띄었다. 음성 품질을 개선하기 위해 상용 서비스로 방향을 바꾼 이유다.

ElevenLabs와 Typecast 비교

비교한 서비스는 ElevenLabs와 Typecast였다. 직접 테스트했을 때 감정 표현은 ElevenLabs가 더 만족스러웠지만, 대본이나 목소리를 바꿔가며 반복 합성해야 하는 개인 프로젝트에서 유료 결제 부담이 걸렸다.

 

Typecast는 당시 확인한 무료 계정으로도 Open API를 사용할 수 있었고, 무료 이용에 따른 출처 표기 조건이 있었다. 음성 품질과 반복 테스트 비용을 함께 고려했을 때 현재 개발 단계에서는 Typecast가 더 현실적인 선택이었다.

Typecast Preset과 Smart Emotion

먼저 Preset 방식으로 생성한 음성은 오픈소스보다 자연스러웠다. 다만 대사를 각각 만들고 이어 들으면 문장 사이 말투가 조금씩 분리돼 보이는 느낌이 남았다. 상황에 맞춰 감정 설정을 조정할 수도 있겠지만, 매번 대사별로 수동 설정을 관리하는 일도 부담이었다.

 

처음에는 Smart Emotion이 과한 기능이라고 생각했다. 30초 남짓한 상황극에 문맥 기반 감정 표현까지 필요할까 싶었기 때문이다. 그래도 실제로 들어보기로 했다. 전체 대본을 한 번에 WAV 하나로 합성한 것이 아니라 앞뒤 대화 문맥을 함께 제공하면서 발화 11개를 개별 요청으로 생성하는 방식이었다. 저작권 표시 문제로 생성한 WAV 파일은 블로그에는 올리지 못하겠네... ;;

결과는 예상을 뒤집었다. 개별 WAV를 이어 붙였는데도 개발자의 질문, ChatGPT의 자신감 있는 응답, 작업 요청 이후의 대사가 꽤 자연스럽게 연결됐다. Preset 방식에서 느꼈던 어색함이 크게 줄어 Smart Emotion으로 진행하기로 결정했다.

 

최종 테스트에서는 대사 사이의 기본 추가 쉼을 0.3초로 설정해 11개 발화를 연결했고, 전체 상황극은 30.200초였다. 다만 각 발화 뒤의 쉼은 모두 동일하지 않다. 장면의 의도에 따라 마지막 대사 이후 화면을 유지하거나 즉시 전환하는 예외를 따로 관리한다.

실제 WAV와 영상 시간표의 불일치

Smart Emotion 덕분에 음성 품질 문제는 예상보다 빨리 정리됐다. 그런데 다음 단계인 YPUB 패키지 제작 흐름을 살펴보니 새로운 문제가 드러났다. ChatGPT는 대본과 화면 연출을 만들면서 대사의 출력 시각과 장면 전환 시간을 대략적으로 정한다. 반면 Typecast가 생성하는 WAV는 문장 길이와 합성 결과에 따라 실제 재생시간이 달라진다. 예상 길이가 2.5초인 대사가 실제로는 2.95초라면, 다음 대사와 화면 전환을 기존 시간표대로 실행할 수 없다.

 

그때마다 ChatGPT가 만든 JavaScript의 시간을 다시 고치거나, Python에서 JS를 직접 수정하는 방식은 불합리하다고 봤다. 음성을 다시 합성할 때마다 연출 코드까지 수정해야 하는 구조가 되기 때문이다. 이 문제는 개별 시간값을 잘 맞추는 일보다, 누가 연출을 결정하고 누가 시간을 계산할 것인지의 문제에 가까웠다.

JSON 메타데이터 기반 타임라인 제안

그래서 내가 먼저 제안한 것은 단순했다. “YPUB에는 대사 ID와 대략적인 시작·종료 시간, 장면 전환 조건을 JSON으로 기록하고, 실제 WAV를 만들고 나면 Python이 그 길이를 측정해 시간표만 다시 계산하면 되지 않을까?” 예를 들어 마지막 대사 뒤 화면을 1초 더 유지해야 한다면, 제작자는 그 의도만 명시하면 된다. Python은 해당 대사의 실제 WAV 종료 시각을 계산한 뒤 1초를 더해 Scene 종료 시점을 정한다. WAV 길이가 바뀌어도 같은 규칙으로 새 시간표를 생성할 수 있다.

 

하나의 Scene에 대사가 여러 개 있거나, 대사 도중 본문 연출이 바뀌거나, 아무 말 없이 화면만 유지되는 경우도 있었다. 이런 상황을 다루기 위해 발화별 ID, 화면 표현 구간인 Segment, 이벤트 앵커, Scene 종료 경계를 구분하는 방향으로 구체화했다. 중요한 것은 원래 제작한 JSON과 JS를 보존하고, 실측 결과를 별도의 시간표로 만든다는 점이었다.

ChatGPT와의 설계 크로스체크

구조를 논의하는 과정이 처음부터 매끄럽지는 않았다. ChatGPT가 Python 쪽에 하단 캐릭터 PNG나 대사 표시 내용까지 넘기는 방향으로 설명하기도 했다. 하지만 그건 내가 의도한 역할 분리가 아니었다. 그 방식대로라면 Python이 화면 연출을 다시 결정하거나 JS를 수정하는 셈이었다.

 

나는 “화면에 무엇을 보여줄지는 YPUB 제작자인 ChatGPT가 정하고, Python은 언제 보여줄지만 계산한다”는 기준을 다시 분명히 했다. 이후 여러 발화가 한 Scene에 있는 경우와 Scene 종료 후 유지 시간, 대사 중간의 화면 변화 같은 조건을 ChatGPT와 크로스체크하면서 계약을 다듬었다.

 

이 과정에서 내 역할은 실제 제작 흐름의 문제를 발견하고 해결 방향과 책임 경계를 정하는 것이었고, ChatGPT는 제안한 구조의 허점과 예외 상황을 검토하는 역할을 맡았다. 설계가 다른 방향으로 흘렀을 때 기준을 다시 세운 것도 이 검토 과정의 일부였다.

Codex 구현과 WAV 실측 검증

설계 방향을 정리한 뒤에는 Codex에 Timeline Resolver 구현을 맡겼다. Python은 발화 ID에 연결된 WAV를 측정하고, 발화 사이의 쉼과 Scene 종료 조건을 적용해 보정된 JSON 시간표를 생성한다. 화면의 GIF·PNG 선택이나 대사 내용은 건드리지 않는다.

 

기존 Smart Emotion WAV 11개를 이용한 검증에서는 전체 길이 30.200초를 재현했다. Scene 03의 마지막 음성 종료 후 1초 유지와 Scene 04-1에서 04-2로 넘어가는 즉시 전환 조건도 확인했다. 이때 검증한 것은 WAV 실측을 반영한 시간표 계산이지, 최종 영상과 음성이 결합된 EP.01 MP4 완성 결과는 아니다.

 

처음에는 TTS 음성의 자연스러움을 개선하려던 작업이었지만, 결과적으로는 YPUB 제작과 Python 시간 보정의 책임을 분리하는 설계로 이어졌다. 이제 남은 것은 동일한 시간표를 JavaScript 렌더러와 FFmpeg이 사용하도록 연결하고, 실제 MP4에서 화면 전환과 음성 싱크를 확인하는 일이다.