Python TTS API: 요청, 폴링, MP3 저장하기

Python에서 Requests로 텍스트 음성 변환 API를 호출하세요. 텍스트와 음성을 POST하고 Job을 폴링한 뒤 MP3를 저장하는 Sume TTS 1.0 전체 스크립트입니다.

읽는 시간 5분Sume
전체 글

Python에서 텍스트 음성 변환(TTS) API를 쓰려면 requests로 텍스트와 음성을 POST하고, Job이 끝날 때까지 기다린 뒤, 반환된 오디오 파일을 내려받으세요. Sume에서는 POST /v1/tts-1.0/generate를 보내고, Job이 종료 상태가 될 때까지 GET /v1/jobs/{id}/status를 호출한 다음, 완성된 MP3의 링크가 담긴 GET /v1/jobs/{id}/result를 읽습니다.

Sume가 문서화한 SDK는 TypeScript(@sume-com/sdk)이므로, Python에서는 평범한 HTTPS 호출을 합니다. Sume 관련 사실은 Sume API 레퍼런스의 TTS 1.0 스키마와 Job과 결과 (영문) 문서에서, Requests 동작은 Requests의 빠른 시작과 고급 사용법 페이지에서 가져왔습니다. Sume API 레퍼런스는 API 레퍼런스 문서의 바탕이 되는 OpenAPI 문서입니다. 모두 2026-09-27에 확인했습니다. 같은 패턴을 영상에 쓰는 방법은 Python Text-to-Video API에 있습니다.

텍스트는 어떻게 보내나요?

본문에는 1–20,000자의 transcript와 음성 하나가 필요합니다. 음성은 voice.id, 또는 목소리가 준비된 아바타의 avatar_id나 avatar_handle입니다. 영어가 아닌 텍스트에는 language를 지정하세요. 본문은 json=으로 넘기세요. Requests 문서는 JSON Content-Type 헤더를 알아서 설정하게 하려면 json=을 쓰라고 안내합니다.

  • mode를 생략하면 제출은 async로 처리됩니다. 즉시 응답하며, data 안에 Job, status_url, result_url이 담깁니다.
  • Idempotency-Key를 보내세요. POST가 타임아웃되면 같은 본문을 같은 키로 다시 보내세요. 재시도하면 두 번째 Job을 과금하는 대신 원래 Job이 반환됩니다.
  • 아직 음성 ID가 없나요? GET /v1/avatar-1.0/avatars는 음성 ID를 반환하지 않지만 내 아바타 목록을 보여 주며, voice.status가 ready인 아바타는 avatar_id나 avatar_handle로 쓸 수 있습니다. voi_ ID는 Sume 앱의 Voices 라이브러리에서 얻습니다. 내 목소리로 AI 보이스오버 만들기를 참고하세요.

전체 스크립트는 어떻게 생겼나요?

제출, 폴링, 저장을 25줄로 처리합니다. 재전송한 제출은 이미 끝났을 수도 있는 원래 Job을 반환하므로, 루프는 다른 것을 읽기 전에 먼저 폴링합니다. 현재 코드에서 audio_url은 Sume 미디어 호스트에 있는 오디오 아티팩트의 공개 URL이므로, 내려받을 때는 키를 보내지 않습니다. stream=True와 iter_content를 함께 쓰면 파일을 청크 단위로 디스크에 씁니다.

import os, time, requests

AUTH = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
body = {"transcript": "Hello from Python.", "voice": {"id": os.environ["SUME_VOICE_ID"]}}
r = requests.post("https://api.sume.com/v1/tts-1.0/generate", json=body, timeout=30,
                  headers={**AUTH, "Idempotency-Key": "hello-python-v1"})
r.raise_for_status()
job = r.json()["data"]

while True:
    s = requests.get(job["status_url"], headers=AUTH, timeout=30)
    s.raise_for_status()
    status = s.json()["data"]
    if status["terminal"]:
        break
    time.sleep(status["next_poll_after_seconds"] or 2)
if status["sume_status"] != "completed":
    raise RuntimeError(f"TTS job ended as {status['sume_status']}")
res = requests.get(job["result_url"], headers=AUTH, timeout=30)
res.raise_for_status()
with requests.get(res.json()["data"]["result"]["audio_url"], stream=True, timeout=30) as audio:
    audio.raise_for_status()
    with open("speech.mp3", "wb") as f:
        for chunk in audio.iter_content(chunk_size=65536):
            f.write(chunk)

폴링 루프는 얼마나 기다려야 하나요?

  • next_poll_after_seconds는 다음 폴링 전에 권장하는 대기 시간입니다. 문서는 이 값이 있으면 따르고, 없으면 백오프하라고 안내합니다.
  • /result는 result_ready가 true가 될 때까지 409 job_not_completed로 응답합니다. failed나 canceled라면 대신 GET /v1/jobs/{id}에서 이유를 읽으세요.
  • mode: "sync"는 제출 요청을 최대 30초 동안 붙잡습니다. 이 제한은 Job이 아니라 HTTP 대기에 대한 것입니다. Job이 아직 실행 중이면 위처럼 폴링하세요. 이 경로는 오디오를 스트리밍하지 않습니다.
  • 모든 호출에 timeout을 지정하세요. 지정하지 않으면 Requests는 타임아웃되지 않습니다.
  • 클라이언트 쪽에서 기다리기를 포기해도 Job은 취소되지 않습니다. Job은 계속 실행되고 과금도 그대로 됩니다.

대신 WAV나 단어별 타이밍을 받으려면 어떻게 하나요?

루프가 아니라 본문을 바꾸세요. 표의 각 행은 스크립트의 body 딕셔너리에 추가할 항목이며, json=이 이를 JSON으로 보냅니다(Python의 True는 true가 됩니다). 바꾼 본문에는 새 Idempotency-Key를 주세요. 다른 페이로드에 같은 키를 다시 쓰면 409 idempotency_conflict로 응답하기 때문입니다.

Sume API 레퍼런스의 TTS 1.0 스키마와 현재 코드 기준, 2026-09-27 확인.
필요한 것본문에 추가할 항목결과
WAV 파일"output_format": {"container": "wav", "encoding": "pcm_s16le", "sample_rate": 44100}WAV audio_url. .wav로 저장
단어별 타이밍"timestamps": {"words": True}words와 duration_seconds
문장마다 클립 하나"segmentation": {"mode": "sentence"}도 추가. 단어별 타이밍을 켜고 WAV로 출력각자 audio_url이 있는 segments[]
영어가 아닌 음성"language": "ko"(BCP-47 / ISO-639 코드)와 그 언어로 쓴 대본그 언어로 된 음성

Python에서 TTS를 쓰면 비용은 얼마인가요?

TTS 1.0 요금은 1,000자당 $0.0475이며, 기본적으로 5.5% 에이전트 수수료가 더해지고, 공백과 문장 부호도 글자 수에 포함됩니다. 같은 Idempotency-Key로 재전송하면 원래 Job이 반환되며 다시 과금되지 않습니다. 합성된 오디오가 1,200초를 넘으면 tts_duration_exceeded로 실패하고 크레딧은 확정되지 않습니다.

출처

관련 글

개발자 카테고리의 다른 글

개발자 글 전체 보기

작성자 Sume