TTS API Python: SDK를 사용하여 음성 생성용 텍스트 준비
이 API는 오디오를 반환하지 않습니다. OpenAI SDK를 사용하여 Python에서 호출하는 텍스트 모델이며, 음성 엔진 앞에 배치할 수 있습니다. 스크립트를 작성하고 정리하여 음성 단계가 읽을 수 있도록 문장으로 분할합니다.
업데이트

이 API의 기능과 한계
tts api python 튜토리얼을 검색하셨다면, 코드 작성 전 명확히 해야 할 점이 있습니다. 이 서비스는 음성을 합성하지 않습니다. 오픈 웨이트 텍스트 모델 하나를 위해 POST /v1/chat/completions 및 GET /v1/models을 제공하며, 모델 ID는 uncensored입니다. 오디오 엔드포인트, 음성 목록, 응답 내 오디오 파일이 없습니다. 이미지, 비디오, 임베딩 출력도 없습니다.
그렇다면 음성 프로젝트에서 어디에 적합합니까? 파이프라인의 텍스트 단계입니다. 음성 엔진이 스크립트를 읽기 전에 누군가가 이를 작성하고, 숫자 및 약어를 발음 가능한 단어로 확장하며, 마크업을 제거하고 합성 시 긴 정지가 발생하지 않도록 짧은 조각으로 나누어야 합니다. 언어 모델은 이 작업을 잘 수행하며, 이미 다른 OpenAI 호환 서비스용 SDK를 사용하고 있다면 동일한 SDK를 Python에서 호출할 수 있습니다.
이 모델은 합법적인 성인 사용에 대해 콘텐츠 거부 없이 답변하도록 조정되었습니다. 이는 엄격한 모델이 장면 중간에 중단할 수 있는 픽션, 캐릭터 대화, 내레이션에서 중요합니다. 하나의 엄격한 규칙이 적용됩니다: 미성년자가 포함된 성 콘텐츠 금지.
이 기사의 나머지 부분에서는 네 단계로 작은 Python 모듈을 구축합니다: 첫 번째 호출, 스트리밍 문장 분할기, JSON 출력이 있는 텍스트 정규화기 및 속도 제한을 준수하는 재시도 래퍼입니다.
SDK 설치 및 첫 번째 호출 수행
Python 3.8 이상과 공식 openai 패키지가 필요합니다. 먼저 API 키 페이지에서 키를 생성하세요. 새 계정은 한 번의 클릭으로 $0.50 체험 크레딧을 청구할 수 있으며, 카드가 필요하지 않습니다.
pip install openai
키는 소스 코드에 직접 넣지 마세요. 환경 변수로 내보내고 코드에서 읽으세요:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.xtts.cc/v1",
api_key=os.environ["XTTS_API_KEY"],
)
resp = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "You write short narration scripts. Plain sentences only."},
{"role": "user", "content": "Write a 60-word intro for a podcast about night trains."},
],
temperature=0.8,
)
script = resp.choices[0].message.content
print(script)
print(resp.usage.prompt_tokens, resp.usage.completion_tokens)
전체 통합 방법입니다. SDK는 Authorization: Bearer <key>을 사용자를 대신하여 전송합니다. 모든 응답에는 X-Balance-USD 헤더가 포함되어 있으므로, 로깅하려는 경우 원본 응답에서 남은 크레딧을 읽을 수 있습니다. 문서 페이지에는 다른 헤더와 매개변수가 나열되어 있습니다.
temperature, top_p, stop, seed 및 페널티와 같은 매개변수가 전달됩니다. 내레이션의 경우 온도를 0.6에서 0.9 사이로 설정하는 것이 적절한 시작 범위이며, 프롬프트를 조정하면서 동일한 결과를 재현하려면 고정 seed가 도움이 됩니다.
스크립트 스트리밍 및 문장 분할
음성 엔진은 일반적으로 문장 단위로 작동합니다. 900단어 분량의 스크립트가 완료되기를 기다리면 청취자도 기다려야 합니다. stream: true를 사용하면 API는 서버 전송 이벤트를 통해 토큰을 전송하며, 나머지 부분이 작성되는 동안 완료된 문장을 음성 단계로 전달할 수 있습니다.
import os
import re
from openai import OpenAI
client = OpenAI(base_url="https://api.xtts.cc/v1", api_key=os.environ["XTTS_API_KEY"])
SENTENCE_END = re.compile(r"(?<=[.?])\s+")
def stream_sentences(topic):
stream = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "Write spoken narration. One idea per sentence."},
{"role": "user", "content": topic},
],
stream=True,
)
buffer = ""
for chunk in stream:
if not chunk.choices:
continue # the final chunk can carry usage only
buffer += chunk.choices[0].delta.content or ""
parts = SENTENCE_END.split(buffer)
for sentence in parts[:-1]:
yield sentence.strip()
buffer = parts[-1]
if buffer.strip():
yield buffer.strip()
for line in stream_sentences("Explain how a tide table is read."):
print(line) # replace with: your_voice_engine.say(line)
두 가지 세부 사항은 놓치기 쉽습니다. 첫째, 사용량 숫자는 스트림 끝에 도착하며, 빈 choices 목록을 가진 청크일 수 있으므로 선택 사항이 없는 청크는 건너뜁니다. 둘째, 스트리밍된 요청은 스트림이 끝날 때까지 동시 요청 슬롯을 점유합니다. 음성 엔진이 느리고 스트림을 지연해서 읽는 경우 슬롯이 점유된 상태가 됩니다. 텍스트를 빠르게 추출하고 문장을 큐에 넣은 후, 오디오 쪽이 자신의 속도로 큐를 소비하도록 하세요.
위의 정규식은 뒤에 공백이 오는 마침표나 물음표에서 분할합니다. "Dr." 또는 "No."와 같은 약어 뒤에 잘못 분할할 수 있으므로 텍스트를 먼저 정규화하는 것이 좋습니다. 이는 다음 섹션에서 보여줍니다.
청취자를 위한 텍스트 정규화
글로 된 텍스트와 말로 된 텍스트는 다릅니다. "Lee 박사는 3/4에 2판 3부를 구입했다"는 읽기 쉽지만 말하기 어색합니다. 많은 음성 엔진은 이를 일부 처리하지만 엔진마다 결과가 다르므로, 음성 단계에 도달하기 전에 스크립트를 정리하면 예상치 못한 문제를 제거할 수 있습니다. 명시적인 지시문을 사용하여 모델에게 이를 수행하도록 요청하세요:
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.xtts.cc/v1", api_key=os.environ["XTTS_API_KEY"])
NORMALIZE = (
"Rewrite the user's text so it can be read aloud. "
"Spell out numbers, currency, dates and abbreviations as words. "
"Remove markdown, URLs and emoji. Keep the meaning and the order. "
"Return only the rewritten text."
)
def normalize(text):
r = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": NORMALIZE},
{"role": "user", "content": text},
],
temperature=0.2,
)
return r.choices[0].message.content.strip()
print(normalize("Dr. Lee bought 3 copies on 3/4 for the 2nd edition."))
이 단계에서는 낮은 온도를 사용하세요. 동일한 입력이 동일한 출력을 제공하기를 원하며, 창의성은 방해가 될 뿐입니다. 전체 책을 처리하기 전에 결과 샘플을 검토하고 모호한 항목을 어떻게 처리할지 결정하세요. "3/4" 날짜는 표준 예시입니다: 3월 4일 또는 4월 3일로 읽으시겠습니까? 모델은 로캘을 추측할 수 없으므로 시스템 프롬프트에 선택 사항을 입력하세요.
이 단계의 비용
정규화는 저렴합니다. 600토큰의 단락 입력과 약 650토큰 출력은 입력에 600 x $0.15 / 1,000,000 = $0.00009, 출력에 650 x $0.50 / 1,000,000 = $0.000325가 들어가고 총합은 $0.000415입니다. 이는 단락 1,000개당 약 $0.42입니다. 가격 책정 페이지에 전체 표와 충전 옵션이 있습니다.
JSON 모드로 구조화된 세그먼트 반환
일부 음성 엔진은 세그먼트별 설정을 허용합니다: 일시 정지 길이, 말하는 속도, 다른 캐릭터를 위한 다른 음성 등. 모델이 이러한 지시문을 JSON으로 생성하도록 하고 Python에서 읽을 수 있습니다. response_format을 {"type": "json_object"}로 설정하고 프롬프트에서 구조를 설명하세요. JSON 모드는 유효한 JSON을 보장하지만 스키마를 보장하지는 않기 때문입니다.
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.xtts.cc/v1", api_key=os.environ["XTTS_API_KEY"])
prompt = (
"Split this scene into segments. Return JSON like "
'{"segments": [{"speaker": "narrator", "text": "...", "pause_ms": 300}]}. '
"Speakers are narrator, mara, or tom."
)
r = client.chat.completions.create(
model="uncensored",
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": "Mara opens the door. Tom says he is late again. She sighs and lets him in."},
],
temperature=0.3,
)
data = json.loads(r.choices[0].message.content)
for seg in data["segments"]:
print(seg["speaker"], seg["pause_ms"], seg["text"])
반환된 내용을 검증하세요. segments이 존재하고, 모든 항목에 알려진 화자가 있으며, pause_ms이 허용하는 범위의 숫자인지 확인하세요. 검증에 실패하면 온도를 낮춰서 한 번 다시 시도한 후 포기하세요. 모델 출력을 신뢰할 수 없는 입력으로 취급하세요. 사용자 텍스트와 동일하게 취급합니다.
시간을 낭비하는 함정
언어 모델을 음성 엔진 앞에 연결할 때 실수가 반복적으로 나타납니다.
- 모델이 설명을 추가하도록 허용. 모델은 "물론입니다, 스크립트는 다음과 같습니다."와 같은 말을 좋아합니다. 이 문장은 읽히게 됩니다. 시스템 프롬프트를 "스크립트 텍스트만 반환하세요"로 끝내고, 콜론으로 끝나는 선행 줄이 있으면 제거하세요.
- 문장 길이를 신뢰. 일부 엔진은 매우 긴 문장에서 어려움을 겪습니다. 프롬프트에 "25단어보다 긴 문장은 쓰지 않는다"와 같은 규칙을 추가하고, 문장이 여전히 너무 길면 쉼표에서 분할하여 Python에서 강제하세요.
- 컨텍스트 창을 잊지 마세요. 프롬프트와 완료는 100,000 토큰을 공유합니다. 긴 책 장의 경우 전체 텍스트 대신 이전 섹션의 짧은 요약을 보내며 섹션별로 전송하세요. 이렇게 하면 입력 비용도 낮아집니다.
- 한계 없이 스레드 간에 하나의 전역 클라이언트 재사용. 클라이언트는 공유해도 안전하지만 서비스는 무한이 아닙니다. 키의 동시 요청 수에 따라 풀을 제한하세요.
- 키 하드코딩. 환경 변수를 사용하고 키가 저장소에 유출될 경우 회전하세요.
무언가 잘못 보이는 경우, 원인이 된 프롬프트와 함께 X-Request-Id 헤더를 로그에 남기세요. 도움이 필요할 때 이 ID는 특정 호출을 가장 빠르게 지시하는 방법이며, 문서에서 찾는 방법을 설명합니다.
속도 제한 및 재시도 처리
전체 키는 동시 요청 8개와 분당 300개의 요청을 허용합니다. 체험 키는 동시 2개, 분당 60개를 허용합니다. 이 한도를 초과하면 HTTP 429 오류가 발생하며, 분당 요청에는 rate_limited, 동시 요청 초과에는 concurrency 오류 코드가 반환됩니다. 일시적 과부하 시 API는 429를 응답하고 Retry-After: 5를 전송합니다.
SDK는 자체적으로 재시도하지만 명시적인 래퍼를 사용하면 제어할 수 있으며 로그에서 동작을 확인할 수 있습니다. 키에 맞는 스레드 풀과 결합하면 429를 거의 보지 못합니다:
import os
import time
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI, RateLimitError
client = OpenAI(
base_url="https://api.xtts.cc/v1",
api_key=os.environ["XTTS_API_KEY"],
max_retries=0,
)
def ask(prompt, tries=5):
for attempt in range(tries):
try:
r = client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": prompt}],
)
return r.choices[0].message.content
except RateLimitError as err:
wait = int(err.response.headers.get("Retry-After", 5))
time.sleep(wait * (attempt + 1))
raise RuntimeError("still rate limited after retries")
prompts = ["Narrate a sunrise in two sentences."] * 20
with ThreadPoolExecutor(max_workers=8) as pool: # use 2 on a trial key
results = list(pool.map(ask, prompts))
print(len(results))
실패하거나 거부된 요청은 청구되지 않으므로, 오류 후 재시행해도 실패한 시도에 대해서는 요금이 부과되지 않습니다. 완료된 요청은 정상적으로 청구되므로, 느리지만 성공적인 호출에 대해 무작위로 재시도하는 것을 피하세요.
텍스트를 음성 엔진에 전달
마지막 단계는 이 API 외부에서 수행됩니다. Python에는 텍스트에서 오디오를 생성하는 여러 방법이 있습니다: 직접 실행할 수 있는 오픈 소스 음성 모델, 운영 체제에 내장된 데스크톱 음성 서비스 및 다른 회사의 호스팅 음성 API. 각자 고유한 라이선스, 품질 및 가격이 있으므로 선택하기 전에 현재 문서와 약관을 확인하세요. 이 사이트는 해당 부분을 제공하지 않습니다.
깨끗한 경계는 코드를 쉽게 변경할 수 있게 합니다. speak(text: str) -> None 시그니처의 함수를 하나 작성하고 모든 엔진별 세부 사항을 그 안에 유지하세요. 위의 텍스트 파이프라인은 각 문장이나 세그먼트에 대해 해당 함수를 호출합니다. 엔진을 교체해도 다른 부분은 변경되지 않습니다.
아직 체험판과 유료 설정 중 고민 중이라면 숫자는 간단합니다: 선불 크레딧에서 소모되며 만료되지 않는 크레딧으로 매백만 입력 토큰당 $0.15, 매백만 출력 토큰당 $0.50을 지불하며 구독은 없습니다. 충전은 $20에서 $500까지 가능하며 암호화폐(TRC20의 USDT, Base의 USDC)로 결제할 수 있습니다.
더 많은 가이드
질문과 답변
이 API는 오디오 파일을 반환합니까?
아니요. 텍스트만 반환합니다. 이를 사용하여 스크립트를 작성, 정리 및 구조화한 후, 텍스트를 별도의 음성 엔진에 전달하세요.
어떤 Python 패키지를 사용해야 하나요?
공식 OpenAI 패키지입니다. base_url을 https://api.xtts.cc/v1로 설정하고 키를 전달한 후, 모델 ID uncensored를 사용하세요.
Python에서 스트리밍으로 응답을 받을 수 있나요?
네. stream=True를 전달하고 청크를 반복하세요. 사용량은 스트림의 끝에서 보고되며, 요청은 스트림이 완료될 때까지 동시성 슬롯을 유지합니다.
속도 제한에 도달하면 어떻게 되나요?
속도 제한(rate_limited) 또는 동시성(concurrency) 코드로 HTTP 429를 받습니다. Retry-After 값이 있으면 기다렸다가 재시도하고, 전체 키의 경우 8 이하, 체험 키의 경우 2 이하로 워커 수를 유지하세요.
키는 양식 하나만 작성하면 받을 수 있습니다
계정을 생성하고 키를 복사한 후 base URL을 변경하세요. 설정은 이것으로 끝입니다.