
Kokoro-82M 文本转语音模型的 Docker 化 FastAPI 封装。几分钟即可生成数小时的高质量语音。
docker run -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-cpu:latest
**NVIDIA**(GTX 900 系列至 RTX 40;内置 cu126)
docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
**NVIDIA RTX 50 系列 / Blackwell**(内置 cu128)
docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest-cu128
**NVIDIA arm64**(Jetson、GH200;同一标签,内置 cu129)
docker run --gpus all -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-gpu:latest
**AMD GPU**(ROCm,实验性,仅 x86_64)
docker run --device=/dev/kfd --device=/dev/dri -p 8880:8880 ghcr.io/remsky/kokoro-fastapi-rocm:latest
**Apple Silicon**(原生 MPS 克隆;CPU 镜像同样可用)
./start-gpu_mac.sh
`gpu:latest` 与 `gpu:latest-cu126` 为同一镜像。通过环境变量进行配置,详见[配置指南](docs/configuration.md)。
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8880/v1", api_key="not-needed"
)
with client.audio.speech.with_streaming_response.create(
model="kokoro",
voice="af_sky+af_bella", #单个或多个语音包组合
input="Hello world!"
) as response:
response.stream_to_file("output.mp3")
- API 可通过 http://localhost:8880 访问
- API 文档:http://localhost:8880/docs
- Web 界面:http://localhost:8880/web
# 使用 OpenAI 的 Python 库
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8880/v1", api_key="not-needed")
response = client.audio.speech.create(
model="kokoro",
voice="af_bella+af_sky", # 参见 /api/src/core/openai_mappings.json 进行自定义
input="Hello world!",
response_format="mp3"
)
response.stream_to_file("output.mp3")
或通过 Requests:
import requests
response = requests.get("http://localhost:8880/v1/audio/voices")
voices = [v["id"] for v in response.json()["voices"]]
# 生成音频
response = requests.post(
"http://localhost:8880/v1/audio/speech",
json={
"model": "kokoro",
"input": "Hello world!",
"voice": "af_bella",
"response_format": "mp3", # 支持:mp3、wav、opus、flac、aac、pcm
"speed": 1.0
}
)
# 保存音频
with open("output.mp3", "wb") as f:
f.write(response.content)
快速测试(在另一个终端运行):
python examples/assorted_checks/test_openai/test_openai_tts.py # 测试 OpenAI 兼容性
python examples/assorted_checks/test_voices/test_all_voices.py # 测试所有可用音色
# 兼容 OpenAI 的流式
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8880/v1", api_key="not-needed")
# 流式写入文件
with client.audio.speech.with_streaming_response.create(
model="kokoro",
voice="af_bella",
input="Hello world!"
) as response:
response.stream_to_file("output.mp3")
# 流式播放到扬声器(需要 PyAudio)
import pyaudio
player = pyaudio.PyAudio().open(
format=pyaudio.paInt16,
channels=1,
rate=24000,
output=True
)
with client.audio.speech.with_streaming_response.create(
model="kokoro",
voice="af_bella",
response_format="pcm",
input="Hello world!"
) as response:
for chunk in response.iter_bytes(chunk_size=1024):
player.write(chunk)
或通过 requests:
import requests
response = requests.post(
"http://localhost:8880/v1/audio/speech",
json={
"input": "Hello world!",
"voice": "af_bella",
"response_format": "pcm"
},
stream=True
)
for chunk in response.iter_content(chunk_size=1024):
if chunk:
# 处理流式分块
pass
import requests
response = requests.get("http://localhost:8880/v1/audio/voices")
voices = [v["id"] for v in response.json()["voices"]]
# 加权音色组合(67%/33% 混合)
response = requests.post(
"http://localhost:8880/v1/audio/speech",
json={
"input": "Hello world!",
"voice": "af_bella(2)+af_sky(1)", # 2:1 比例 = 67%/33%
"response_format": "mp3"
}
)
# 将组合音色下载为 .pt 文件
response = requests.post(
"http://localhost:8880/v1/audio/voices/combine",
json="af_bella(2)+af_sky(1)" # 2:1 比例 = 67%/33%
)
# 保存 .pt 文件
with open("combined_voice.pt", "wb") as f:
f.write(response.content)
# 使用下载的音色文件
response = requests.post(
"http://localhost:8880/v1/audio/speech",
json={
"input": "Hello world!",
"voice": "combined_voice", # 使用保存的音色文件
"response_format": "mp3"
}
)
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8880/v1", api_key="not-needed")
client.audio.speech.create(
model="kokoro",
voice="narrator",
input="[voice:narrator] Once upon a time. [voice:villain] Never!",
extra_body={
"allow_voice_tags": True,
"voice_aliases": {"narrator": "af_bella(2)+af_sky", "villain": "am_michael"},
},
)
或
curl -X POST http://localhost:8880/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "kokoro",
"voice": "narrator",
"input": "[voice:narrator] Once upon a time. [voice:villain] Never!",
"allow_voice_tags": true,
"voice_aliases": {"narrator": "af_bella(2)+af_sky", "villain": "am_michael"},
"response_format": "mp3"
}' --output aliased.mp3
curl -s http://localhost:8880/dev/tune -F audio=@ref.wav -F 'request={"input":"Hello there."}' -o out.mp3
curl -s http://localhost:8880/dev/tune -F audio=@ref.wav -F return_voice_pack=true -o ref.pt
curl -s http://localhost:8880/dev/tune -F audio=@ref.wav -F save_voice=am_ref # 保存 am_ref_tuned,需要 ALLOW_LOCAL_VOICE_SAVING=true
- `request` 为除 `voice` 外的 `/v1/audio/speech` 请求体;语音包仅在响应期间存在,除非通过 `save_voice` 保留至 `VOICES_DIR`
- 名称沿用现有语言前缀模式(`am_`、`bf_`、`ax_` 等),保存的音色会加上 `_tuned` 后缀。内置调校音色以 `_inno` 结尾
- 可调参数:`prosody_head`(默认开启)、`fmax` 音高上限(Hz,默认自动)
- 默认关闭:
- `ENABLE_INNO_TUNER=true` 启用并带一个 Web 播放器标签页
- `ALLOW_LOCAL_VOICE_SAVING=true` 允许将其保存到运行中的服务器
- 完整参考见 [docs/inno-tune.md](docs/inno-tune.md)
curl -X POST http://localhost:8880/v1/audio/speech \
-H "Content-Type: application/json" \
-d '{
"model": "kokoro",
"voice": "af_heart",
"input": "The narrator opens. [voice:af_bella] Did it land? [pause:0.3s] [voice:am_michael] It did.",
"allow_voice_tags": true,
"response_format": "mp3"
}' --output dialogue.mp3
使用官方 OpenAI 客户端时,通过 `extra_body` 传入参数:
client.audio.speech.create(
model="kokoro",
voice="af_jadzia",
input="The narrator opens. [voice:af_bella] Did it land?",
extra_body={"allow_voice_tags": True},
)
`POST /dev/dialogue` 使用结构化轮次,并可通过参数控制 `pause_between_turns`。
curl -X POST http://localhost:8880/dev/dialogue \
-H "Content-Type: application/json" \
-d '{
"turns": [
{"voice": "af_bella", "text": "Did the multi speaker support land?"},
{"voice": "am_michael", "text": "It did. Turns switch voices inline."}
],
"pause_between_turns": 0.4,
"response_format": "mp3"
}' --output dialogue.mp3
说明:
- 首个标签之前的文本使用请求的 `voice`/默认值。
- 每位说话人根据音色前缀保留各自的语言处理流程。显式 `lang_code` 会覆盖所有说话人。
- 连续使用同一音色的轮次会自动合并。
- 标签可接受上文**音色别名**中的短名称,而非完整的加权混合表达式。
The city of [Worcester](/wˈʊstər/) is easy. [pause:1s] See?
{
"model": "kokoro",
"input": "<speak>Hi<break time=\"750ms\"/>there</speak>",
"voice": "af_bella",
"allow_voice_tags": true,
"ssml": true
}
当你希望返回标记文本而非音频,或在合成前检查时,`POST /dev/ssml` 可单独进行翻译。发送 `text`,如语音请求使用了 `voice` 也一并发送,然后用 `allow_voice_tags: true` 将结果传回。无 `voice` 时,``/`` 会被剥离但保留内容。
- `` 变为 `[pause:0.75s]`。使用 `strength=` 代替 `time=` 时:none/x-weak 0s、weak 0.25s、medium 0.5s、strong 1s、x-strong 1.5s
- `` 变为 `[voice:am_michael]`,在闭合标签处恢复
- `` 变为 `[rate:0.75]`,也接受 `80%` 或 `1.2`。在请求 `speed` 之上缩放说话人的语速,限制 0.25-4.0,随后恢复。`pitch`/`volume` 被忽略
- `Worcester` 变为 `[Worcester](/wˈʊstər/)`,仅支持 IPA 与英语
- `WWW` 朗读别名
- `import requests
def get_phonemes(text: str, language: str = "a"):
"""获取输入文本的音素与 token"""
response = requests.post(
"http://localhost:8880/dev/phonemize",
json={"text": text, "language": language} # "a" 表示美式英语
)
response.raise_for_status()
result = response.json()
return result["phonemes"], result["tokens"]
def generate_audio_from_phonemes(phonemes: str, voice: str = "af_bella"):
"""从音素生成音频"""
response = requests.post(
"http://localhost:8880/dev/generate_from_phonemes",
json={"phonemes": phonemes, "voice": voice},
headers={"Accept": "audio/wav"}
)
if response.status_code != 200:
print(f"Error: {response.text}")
return None
return response.content
# 示例用法
text = "Hello world!"
try:
# 文本转音素
phonemes, tokens = get_phonemes(text)
print(f"Phonemes: {phonemes}") # 例如 ðɪs ɪz ˈoʊnli ɐ tˈɛst
print(f"Tokens: {tokens}") # 包含起始/结束 token 的 token ID
# 生成并保存音频
if audio_bytes := generate_audio_from_phonemes(phonemes):
with open("speech.wav", "wb") as f:
f.write(audio_bytes)
print(f"Generated {len(audio_bytes)} bytes of audio")
except Exception as e:
print(f"Error: {e}")
示例脚本见 `examples/phoneme_examples/generate_phonemes.py`。
import requests
import base64
import json
response = requests.post(
"http://localhost:8880/dev/captioned_speech",
json={
"model": "kokoro",
"input": "Hello world!",
"voice": "af_bella",
"speed": 1.0,
"response_format": "mp3",
"stream": False,
},
stream=False
)
with open("output.mp3","wb") as f:
audio_json=json.loads(response.content)
chunk_audio=base64.b64decode(audio_json["audio"].encode("utf-8"))
f.write(chunk_audio)
print(audio_json["timestamps"])
流式生成带词级时间戳的音频:
import requests
import base64
import json
response = requests.post(
"http://localhost:8880/dev/captioned_speech",
json={
"model": "kokoro",
"input": "Hello world!",
"voice": "af_bella",
"speed": 1.0,
"response_format": "mp3",
"stream": True,
},
stream=True
)
f=open("output.mp3","wb")
for chunk in response.iter_lines(decode_unicode=True):
if chunk:
chunk_json=json.loads(chunk)
chunk_audio=base64.b64decode(chunk_json["audio"].encode("utf-8"))
f.write(chunk_audio)
print(chunk_json["timestamps"])
启用 `"allow_voice_tags": true` 后,每个时间戳还会携带朗读该词所用的 `voice`,多说话人字幕可在客户端无需重新推导切分即可标注。未启用该字段则不存在。
response = requests.post(
"http://localhost:8880/v1/audio/speech",
json={
"input": "Hello world! [pause:1s] Again.",
"voice": "af_bella",
"stream": True,
"return_download_link": True,
"return_timing": True,
},
stream=True,
)
audio = b"".join(response.iter_content(1024))
timings = requests.get(f"http://localhost:8880/v1{response.headers['x-timing-path']}").json()
# {"chunks": [{"text": "Hello world!", "start": 0.0, "end": 0.64}, ...]}
- 块级(一个句组,约 10-20 秒音频),非词级。需要词