지금 내 유튜브 나레이션 음성은 Typecast API가 만든다. 대본 텍스트를 넣으면 TTS 음성이 만들어지고, 자막 타임스탬프까지 붙어서 CapCut 프로젝트가 자동으로 생성되는 파이프라인이다. 여기까지 오는 데 도구를 세 번 갈아탔다 — Supertone MCP, Supertone raw API, 그리고 지금의 Typecast.
이 글은 "Typecast가 최고다" 같은 광고가 아니다. 실제로 파이썬 파이프라인을 굴리면서 왜 Supertone을 접고 Typecast로 왔는지, 그 과정에서 코드가 어떻게 바뀌었는지를 정리한 기록이다. 특히 경제 콘텐츠처럼 티커·숫자·영문 약어가 많은 대본에서는 도구 선택이 생산성에 직접 영향을 준다. 그 이유를 실제 코드로 보여주겠다.
왜 나레이션을 자동화했나
나는 유튜브 롱폼 채널을 운영하는데, 나레이션 음성을 매일 다량으로 뽑아야 한다. 직접 녹음하면 톤이 들쑥날쑥하고, 조용한 환경을 확보하는 것 자체가 번거롭다. 무엇보다 시리즈 콘텐츠는 음성 일관성이 생명인데, 사람 목소리는 그날 컨디션 하나에도 흔들린다. AI 음성은 한 번 세팅해두면 몇 편을 뽑아도 편차 없이 같은 톤이 나온다.
그래서 대본만 넣으면 음성이 나오고, 자막 싱크까지 맞춰서 영상 편집 프로젝트가 자동으로 만들어지는 파이프라인을 파이썬으로 짰다. 핵심은 "손이 가는 단계를 최대한 없애는 것"이었다. 그 관점에서 TTS 도구를 고르다 보니, 음질만큼이나 API로 얼마나 깔끔하게 자동화되는지가 중요해졌다.
도구 세 번 갈아탄 여정 — Supertone MCP → raw API → Typecast
처음부터 Typecast는 아니었다. 순서대로 이랬다.
- Supertone MCP — 처음엔 Supertone을 MCP(Model Context Protocol) 연동으로 붙여서 썼다. 에디터 안에서 바로 음성을 뽑는 흐름이 편했다.
- Supertone raw cloud API — 자동화를 제대로 하려니 MCP로는 부족해서, Supertone의 클라우드 API를 직접 호출하는 방식으로 바꿨다. 파이프라인에 녹이려면 결국 raw API가 답이었다.
- Typecast — 그런데 대량으로 돌리다 보니 TTS 품질·일관성에서 아쉬움이 쌓였다. 그래서 Typecast와 ElevenLabs를 놓고 고민했고, 내 콘텐츠(한국어 경제)에는 Typecast가 더 맞아서 최종적으로 갈아탔다. Supertone에는 결제 환불을 요청했다 — 결과는 바로 아래에 적었다.
빈말이 아니라, 아래처럼 몇 달간 Supertone Play PRO를 실제로 결제하며 썼다. 그러다 이번에 Typecast로 정리하면서 부분 환불을 문의했고, 요청이 접수됐다는 자동 응답을 받았다(개인정보는 가렸다).


결과까지 솔직하게 적는다 — 환불은 거절됐다. 아흐레쯤 뒤에 아래처럼 답변이 왔는데, "크레딧 사용이 확인된 경우 정책상 환불이 어렵다"였고 실제로 내 계정에 33,194개의 크레딧 사용 내역이 있었다. 쓸 만큼 쓰고 나서 돌려달라는 건 무리수가 맞아서 수긍했고, 대신 결제 주기가 끝나는 날짜에 구독이 종료되도록 취소만 걸어뒀다. 갈아탈 때 이런 뒷정리 비용까지 감안하면, 구독형 도구는 결제 주기 초반에 갈아타는 게 그나마 손해가 적다.

Supertone의 한국어 음질 자체가 나쁘다는 얘기는 아니다. 예전에 ElevenLabs와 Supertone을 비교한 글에서 Supertone의 한국어를 꽤 높게 평가했고, 그 판단은 그 시점 기준으로 진심이었다. 다만 이 자동화 파이프라인을 매일 굴리면서, 내 케이스에서는 Typecast 쪽이 손이 훨씬 덜 갔다. 매일 돌리는 파이프라인에선 음질 몇 %보다 손이 얼마나 가느냐가 결국 도구를 정하더라.
결정적 차이 — Typecast의 텍스트 정규화
가장 결정적이었던 차이는 음질이 아니라 텍스트 정규화였다. 이건 내 파이프라인 코드가 그대로 증명한다. 같은 파이프라인이 두 엔진을 다 지원하는데, Supertone 경로에만 있고 Typecast 경로에는 없는 단계가 두 개다.
1. 발음 사전 (Supertone은 필요, Typecast는 불필요)
경제 콘텐츠 대본에는 KODEX, ETF, KOSPI 같은 영문 약어와 티커가 잔뜩 나온다. Supertone에 이걸 그냥 넣으면 엉뚱하게 읽어서, 발음 사전을 따로 관리해야 했다.
# Supertone 경로 — TTS 호출 전에 발음 매핑을 텍스트에 적용
def apply_pronunciations(text, mapping):
# 긴 키부터 치환 (KOSPI 가 KO 보다 먼저 매칭되도록)
for key in sorted(mapping, key=len, reverse=True):
text = text.replace(key, mapping[key])
return text
# 예: KODEX → 코덱스, ETF → 이티에프, KOSPI → 코스피
그리고 새 대본을 넣을 때마다 "발음 사전에 없는 토큰"이 있는지 검사하는 단계(lex check)까지 있었다. 미등록 토큰이 나오면 파이프라인이 멈추고, 발음을 등록한 뒤에야 진행됐다. 안전하긴 한데 손이 많이 갔다.
Typecast로 바꾸니 이 단계가 통째로 사라졌다. Typecast는 자체 텍스트 정규화가 강해서, 약어·티커를 알아서 자연스럽게 읽는다. 그래서 파이프라인 코드에서 Typecast일 때는 발음 사전 검사를 아예 건너뛰도록 짰다.
# typecast는 자체 텍스트 정규화가 강해 발음 사전이 거의 불필요 → lex_check 스킵
if not args.merge_only and not args.skip_lex_check and provider != "typecast":
rc = _run_lex_check(project_path) # Supertone일 때만 실행
2. 숫자 읽기 검증 (Supertone은 필요, Typecast는 불필요)
두 번째는 검증 단계다. Supertone 경로에서는 합성이 끝난 뒤 faster-whisper로 음성을 다시 텍스트로 받아, 원본 대본과 일치하는지 대조했다. 발음이 뭉개지거나 빠진 청크를 잡아내기 위해서였다.
그런데 Typecast는 숫자를 native로 자연스럽게 읽는다(자막은 디지트로 표기). 여기에 whisper 검증을 걸면 숫자마다 표기 방식이 달라서 헛검출이 잔뜩 난다. 대신 Typecast는 응답에 words 메타데이터를 줘서, 실제로 발화한 단어를 스스로 확인해준다. 그래서 이 경로에서도 whisper 하드 검증을 뺐다.
정리하면, 파이프라인 관점에서 두 엔진의 차이는 이렇다.
| 항목 | Supertone | Typecast |
|---|---|---|
| API 엔드포인트 | /v1/text-to-speech/{voice_id} |
/v1/text-to-speech/with-timestamps |
| 인증 헤더 | x-sup-api-key |
X-API-KEY |
| 약어·티커 발음 | 발음 사전 필요 (KODEX 등 오독) | 자체 정규화로 거의 불필요 |
| 숫자 읽기 검증 | whisper 재대조 필요 | 응답 words 메타로 자체 확인 |
| 내 파이프라인 부담 | 발음 사전 + whisper 검증 | 둘 다 스킵 |
즉 Typecast로 오면서 파이프라인에서 손이 가장 많이 가던 두 단계가 사라졌다. 내가 "Typecast는 붙이는 데 딱히 까다롭지 않았다"고 느낀 이유가 이거다. 엔진이 텍스트를 알아서 잘 처리해주니, 내가 앞뒤로 감싸던 방어 코드가 필요 없어진 것이다.
전체 파이프라인 구조
대본 한 편이 음성이 되어 CapCut 프로젝트까지 만들어지는 흐름은 이렇다.
1. 대본(txt) → 문장/호흡 단위로 청크 분할 (chunks.json)
2. chunks.json → 청크별 Typecast API 호출 (병렬, 기본 4 워커)
3. 각 청크 응답 → C01.wav, C02.wav ... + 글자 타임스탬프 저장
4. 개별 wav → ffmpeg 로 청크 사이 0.5초 gap 두고 병합 → audio.mp3
5. 음성 + 타임스탬프 → CapCut 프로젝트 자동 생성 (자막 트랙 싱크 포함)
핵심 설계는 청크 단위 병렬 합성이다. 10분짜리 대본을 통째로 한 번에 보내지 않고, 문장 호흡 단위로 잘라서 여러 개를 동시에 요청한다. 그러면 긴 대본도 빠르게 처리되고, 특정 청크만 마음에 안 들 때 그 청크만 다시 뽑을 수 있다.
from concurrent.futures import ThreadPoolExecutor, as_completed
# 청크들을 병렬로 합성 (기본 4개 동시)
with ThreadPoolExecutor(max_workers=4) as ex:
futures = [ex.submit(synth_one_chunk, c) for c in chunks]
for fut in as_completed(futures):
cid, msg = fut.result()
print(msg) # OK C01.wav len=8.2s ... / FAIL C07: ...
실패한 청크는 ID만 모아서 --only C03 C07 처럼 그 청크만 재생성한다. 전체를 다시 돌릴 필요가 없어서, 대량 작업에서 시간을 크게 아낀다.
Typecast API 실제 호출 코드
Typecast API 호출 자체는 단순하다. with-timestamps 엔드포인트에 POST 하면, base64 오디오와 글자 단위 타임스탬프가 JSON으로 온다. 아래는 내 파이프라인에서 실제로 쓰는 호출을 정리한 것이다(API 키는 환경변수에서 읽는다 — 코드에 하드코딩 금지).
import base64
import requests
TYPECAST_API_BASE = "https://api.typecast.ai/v1"
def synth_typecast(api_key, voice_id, text, *,
model="ssfm-v30", language="kor", emotion=None):
url = f"{TYPECAST_API_BASE}/text-to-speech/with-timestamps"
body = {
"voice_id": voice_id,
"text": text,
"model": model,
"language": language,
}
# emotion이 normal(또는 미지정)이면 기본 보이스 — prompt 생략
if emotion and emotion.lower() != "normal":
body["prompt"] = {"preset": emotion}
r = requests.post(
url,
headers={"X-API-KEY": api_key, "Content-Type": "application/json"},
json=body,
timeout=120,
)
r.raise_for_status()
data = r.json()
audio_bytes = base64.b64decode(data["audio"]) # wav
chars = data["characters"] # [{text, start, end}] 글자 타임스탬프
words = data["words"] # 단어 단위 메타
duration = data["audio_duration"]
return audio_bytes, chars, words, duration
API 키는 .env에 TYPECAST_API_KEY=... 형태로 넣고 python-dotenv로 읽는다. 키를 코드나 깃 저장소에 넣는 건 절대 금물이다.
import os
from dotenv import load_dotenv
load_dotenv() # 프로젝트 루트의 .env 로드
api_key = os.getenv("TYPECAST_API_KEY")
if not api_key:
raise SystemExit("TYPECAST_API_KEY 설정 필요 (.env)")
네트워크 오류나 일시적 서버 오류(429, 5xx)는 지수 백오프로 재시도하고, 4xx 같은 요청 자체 오류는 바로 실패시키는 게 안전하다. 대량으로 돌리다 보면 간헐적 timeout은 반드시 생기니, 재시도 로직은 넣어두는 걸 권한다.
for attempt in range(1, max_retries + 1):
r = requests.post(url, headers=..., json=body, timeout=120)
if r.status_code == 200:
break
# 429/5xx 는 재시도, 그 외 4xx 는 바로 실패
if r.status_code in (429, 500, 502, 503, 504) and attempt < max_retries:
time.sleep(2 ** attempt) # 2s, 4s, 8s ...
continue
raise RuntimeError(f"HTTP {r.status_code}: {r.text[:300]}")
자막 싱크 — with-timestamps 활용법
Typecast API에서 그냥 음성만 주는 엔드포인트가 아니라 with-timestamps를 쓰는 이유가 있다. 응답의 characters 배열이 글자 하나하나의 시작·끝 시각을 알려주기 때문이다.
"characters": [
{"text": "오", "start": 0.06, "end": 0.19},
{"text": "늘", "start": 0.19, "end": 0.33},
{"text": " ", "start": 0.33, "end": 0.36},
{"text": "코", "start": 0.36, "end": 0.49},
...
]
이 타임스탬프가 있으면 자막 싱크를 추정할 필요가 없다. 합성 시점에 엔진이 알려준 ground-truth 타이밍이라, 자막 조각을 이 시각에 그대로 얹으면 음성과 정확히 맞는다. 내 파이프라인은 이 글자 타임스탬프를 CapCut 자막 트랙 생성에 그대로 넘긴다.
한 가지 실무 팁 — TTS 음성은 앞뒤에 약간의 무음 패딩이 붙어 나온다. 나는 이 패딩을 ffmpeg로 잘라내는데, 글자 타임스탬프를 기준으로 첫 글자 직전과 마지막 글자 직후만 남기고 자른다. 이렇게 하면 말소리는 하나도 안 잘리고, 잘라낸 만큼 모든 타임스탬프를 0으로 시프트하면 자막 싱크도 그대로 유지된다.
삽질 포인트와 주의할 점
솔직히 Typecast를 붙이는 것 자체는 까다롭지 않았다. 문서대로 하면 금방 붙는다. 굳이 꼽자면 두 가지가 걸렸다.
- API 키 발급 위치. 웹 콘솔에서 API 키를 발급받아야 하는데, 처음엔 이걸 어디서 하는지 찾는 데 시간이 좀 걸렸다. 발급 후
.env에 넣고X-API-KEY헤더로 보내면 된다. - 요금제 고민. 실제로 가장 오래 고민한 건 기술이 아니라 요금제였다. 매일 대량으로 음성을 뽑으니 크레딧 구조가 비용에 직결된다.

참고로 내가 쓰는 건 라이트 플랜($15/월, 월 20만 크레딧)이다. 여기서 개발자가 눈여겨볼 값이 동시 처리 한도 5다. 앞에서 청크를 병렬로 합성한다고 했는데, 병렬 워커를 이 한도 아래로(나는 4개) 두는 게 안전하다. 한도를 넘겨 동시 요청을 던지면 429가 나기 쉽다. 즉 플랜의 동시 처리 한도가 곧 병렬 처리량의 상한이 되는 셈이다.

실제 사용량은 위 대시보드처럼 이번 달 약 1만 5천 크레딧 정도다. 20만 크레딧 한도에 한참 못 미쳐서, 내 볼륨에는 라이트 플랜으로 충분했다. 한 가지 참고 — 대시보드 사용량은 API 호출 직후 바로 반영되지 않고 1~2분 지연이 있다. 실시간 정산이 아니라는 걸 알아두면 사용량을 확인할 때 헷갈리지 않는다. 사용량은 본인 제작량에 따라 다르고 요금제도 수시로 바뀌니, 결제 전 공식 페이지에서 현재 기준을 확인하자.
그 외에 코드에서 챙긴 것들 — 앞서 말한 앞뒤 패딩 트림, 429/5xx 재시도, 청크 병렬 합성 정도다. 엔진이 텍스트를 알아서 잘 처리해주는 덕분에, Supertone 때 필요했던 발음 사전·재검증 같은 방어 코드가 없어서 오히려 파이프라인이 단순해졌다.
보이스 선택 — 샘플로 고르기
보이스는 코드로 고르지 않았다. Typecast 콘솔에서 실제로 샘플을 여러 개 만들어서 귀로 듣고 골랐다. 이때 보이스뿐 아니라 속도와 감정(emotion) 프리셋도 같이 바꿔가며 샘플을 뽑아 비교했다. 나레이션은 결국 귀에 붙는 톤이 중요해서, 이 과정만큼은 자동화하지 않고 직접 듣는 게 맞다고 본다.
한 번 보이스와 파라미터를 정하면, 그 voice_id와 설정을 채널 설정 파일에 박아두고 이후로는 파이프라인이 그대로 쓴다. 감정을 바꾸고 싶으면 prompt.preset에 프리셋 이름을 넣으면 된다. 기본 톤이면 prompt를 아예 안 보내는 게 깔끔하다.
질문과 답
Typecast API로 만든 음성을 유튜브 수익 콘텐츠에 써도 되나
상업적 사용 가능 여부는 플랜과 약관에 달렸다. 유료 플랜이면 일반적으로 상업 콘텐츠에 쓸 수 있지만, "상업적 사용 가능"의 세부 조건(저작권 귀속, 재배포 범위 등)은 시점에 따라 다르니 결제 전에 공식 약관을 직접 확인하는 게 안전하다.
Supertone에서 Typecast로 갈아타면 코드를 다 새로 짜야 하나
아니다. 나는 파이프라인을 provider 값으로 분기하도록 짜서, 설정 파일에서 엔진만 바꾸면 되게 했다. 합성 함수만 엔진별로 두 개 두고(호출 URL·헤더·응답 파싱이 다름), 그 뒤 병합·자막 단계는 공통으로 쓴다. 처음부터 이렇게 추상화해두면 도구를 갈아탈 때 갈아엎지 않아도 된다.
왜 음성만 주는 엔드포인트가 아니라 with-timestamps를 쓰나
자막 싱크 때문이다. 일반 TTS 엔드포인트는 음성만 주는데, 그러면 자막을 음성에 맞추려고 별도로 정렬 작업을 해야 한다. with-timestamps는 글자 단위 시각을 같이 줘서, 그 값을 자막 트랙에 그대로 얹으면 싱크가 맞는다. 나레이션 영상에선 이게 시간을 가장 많이 아껴준다.
청크로 쪼개면 문장 사이가 부자연스럽지 않나
청크 사이에 짧은 무음(내 경우 0.5초)을 넣어서 자연스러운 호흡 간격을 만든다. 오히려 통짜로 뽑는 것보다 문단 전환이 또렷해진다. 다만 무음 길이는 콘텐츠 톤에 따라 조절하는 게 좋다.
그래서, 누구에게 Typecast가 맞나
내가 Supertone을 접고 Typecast로 온 이유를 한 줄로 줄이면 이렇다. 한국어 대본에 약어·티커·숫자가 많고, 그걸 API로 대량 자동화한다면 Typecast의 텍스트 정규화가 파이프라인을 눈에 띄게 단순하게 만든다.
잘 맞는 쪽부터 말하면 — 한국어 나레이션을 코드로 대량 생성하고, 대본에 영문 약어·티커·숫자가 자주 나오는 경제·시사·기술 콘텐츠를 만들고, with-timestamps로 자막 싱크까지 뽑아야 하는 사람이다. 반대로 영어 콘텐츠가 주력이면 여전히 ElevenLabs가 강하고, 가끔 몇 분 쓰는 정도라 자동화 파이프라인 자체가 필요 없다면 굳이 Typecast일 이유도 없다.
도구는 계속 바뀐다. 나도 Supertone MCP에서 시작해 raw API를 거쳐 Typecast까지 왔고, 반년 뒤엔 또 다른 걸 쓰고 있을지 모른다. 중요한 건 파이프라인을 엔진에 종속시키지 않고 provider로 갈아끼울 수 있게 짜두는 것이다. 그러면 다음에 더 나은 도구가 나와도, 갈아엎는 대신 한 줄만 바꾸면 된다.