Mizuoto

HTTP 接口

Mizuoto 的 REST 接口、访问控制、任务、事件推送、流式合成和兼容 OpenAI 的 /v1/audio/speech,附 curl 与 Python 示例。

mizuoto webui 启动的服务器同时提供网页界面和 HTTP 接口。网页界面能做的事都通过这套接口完成,所以脚本和其他程序也能做同样的事。

地址与文档

内容地址
REST 接口http://127.0.0.1:7869/api/v1/...
交互式文档(Swagger UI)http://127.0.0.1:7869/docs
OpenAPI 结构http://127.0.0.1:7869/openapi.json
事件(socket.io)http://127.0.0.1:7869/ws,socket.io 路径为 /ws/socket.io
流式合成(WebSocket)ws://127.0.0.1:7869/api/v1/tts/stream
兼容 OpenAI 的接口http://127.0.0.1:7869/v1/audio/speech、/v1/models

端口以实际启动的为准(默认 7869,被占用时会顺延,见 命令行概览)。用 mizuoto webui --api-prefix /voice 启动时,上表的所有地址(接口、文档、socket 和网页界面)都移到这个前缀下,例如 http://127.0.0.1:7869/voice/api/v1/...、http://127.0.0.1:7869/voice/v1/audio/speech。

OpenAPI 结构里也列出了 socket 推送的事件类型,可以用它生成客户端类型。

资源一览

所有 REST 路由都在 /api/v1 下:

分组主要路由用途
appGET /app/version、GET /app/health、GET /app/meta版本、健康检查(无需令牌)、首次运行等信息和 torch 版本
settingsGET /settings、PATCH /settings读取设置(令牌只显示是否已设置);按深度合并写入一部分设置
computeGET /compute/info、GET /compute/usage、POST /compute/release设备、显存占用,释放空闲的显存
enginesGET /engines、GET/PATCH /engines/{id}、GET/POST/DELETE /engines/{id}/licence、GET /engines/{id}/doctor引擎状态(可用,或缺什么)与配置、许可、单个引擎的检查
runtimeGET /runtime、POST /runtime/unload、GET /runtime/doctor已加载的模型、卸载模型、mizuoto doctor 的检查
modelsGET /models、POST /models/download、GET/DELETE /models/{engine}/{name}、POST .../verify、GET .../speakers模型目录、下载、校验、删除
assetsGET /assets、POST /assets/download、DELETE /assets/{engine}/{name}共享文件
voicesGET/POST /voices、GET/PATCH/DELETE /voices/{id}、.../references、.../fit、.../prepare、.../export、PUT /voices/import音色库
audioPUT /audio/files、GET /audio/files/{id}、.../content、POST /audio/transcribe上传音频、语音识别
ttsPOST /tts/plan、POST /tts/speak、POST /tts/convert、POST /tts/edit、GET /tts/models/{engine}/{name}/spec、WS /tts/stream合成、转换、编辑、模型能接受的参数
outputsGET /outputs、GET/PATCH /outputs/{id}、.../content、.../rerun、POST /outputs/delete、POST /outputs/zip合成结果
presetsGET/POST /presets、PATCH/DELETE /presets/{id}、POST .../default参数预设
datasetsGET/POST /datasets、.../files、.../import、.../prepare、.../report、.../rows训练数据集
trainGET /train/methods、GET/POST /train/experiments、POST .../{engine}/{name}/run、GET .../metrics实验与训练
projectsGET/POST /projects、.../script、.../render、.../export项目(长篇朗读)
remoteGET /remote/hosts、PUT/DELETE /remote/hosts/{name}、PUT /remote/engines/{id}远程主机,见 远程主机
jobsGET /jobs、GET /jobs/{id}、GET .../log、POST .../cancel、.../retry、.../run-anyway、DELETE任务

每个字段的说明见 /docs 或 /openapi.json。

与命令行一致

每个操作在核心里只有一个方法,接口和命令行都只是解析参数、调用它、再输出结果。所以列表和查询类命令的 mizuoto … --json 与对应接口返回的 JSON 结构相同,例如 mizuoto model list --json 与 GET /api/v1/models。例外是 mizuoto speak --json,它打印 {"job": …, "outputs": […]},比接口多附上了结果记录。

已加载的模型与检查

路由返回
GET /api/v1/runtimeRuntimeStatus:torch(版本,或 null)、models(每个已加载模型的 model_id、engine、device、bytes、last_used、busy、remote)和 engines(每个引擎的 device、state、current_request、last_error)
POST /api/v1/runtime/unload请求体 {"model": "qwen3tts/1.7b-base"} 卸载这个模型(未加载时返回 404);{"model": null} 卸载所有未在使用的模型。返回新的 RuntimeStatus
GET /api/v1/runtime/doctorDoctorReport:torch、torch_build、recommended_build、install_command 以及 findings(level 为 ok / warning / error,engine、check、message、fix)

GET /api/v1/engines/{id}/doctor 给出单个引擎的检查结果。模型在服务器的进程中,这些路由描述的就是这个进程。

错误

出错时返回对应的 HTTP 状态码和统一的结构:

{"code": "not_found", "message": "Unknown model: nope/x", "detail": {"kind": "model", "id": "nope/x", "hint": "mizuoto model list"}}

detail 随错误而不同;detail.hint 存在时是一条能解决问题的命令或说明。请求体本身不合法(缺字段、类型不对)时是 FastAPI 的 422,结构为 {"detail": [...]}。

codeHTTP命令行退出码含义
internal_error5001未分类的内部错误
not_found4042模型、音色、任务、结果或引擎不存在
conflict、busy4093名称冲突,或与进行中的操作冲突
invalid_input、invalid_path4004输入不合法;路径越出允许的范围
capability_not_supported、parameter_not_supported、parameter_out_of_range、reference_invalid、language_not_supported4224模型做不到、不接受这个参数或取值超出范围、参考音频不可用、不支持这种语言
not_local4034只能在服务器本机的浏览器中使用
licence_not_accepted4035还没有接受引擎的许可
model_missing、engine_not_installed、asset_missing4097模型未下载、引擎需要的包未安装(torch、mizuoto-fishspeech、mizuoto-indextts;detail.missing 列出它们)、共享文件缺失
device_unavailable4098设备不存在或不可用
compute_unavailable5039没有可用的设备,或显存不足
worker_failed50210训练阶段的进程或远程代理退出或不再响应
engine_error50010引擎抛出了异常
timeout50411超时(例如 wait=true 等待超时)
download_failed50212所有来源都下载失败
remote_unavailable50214远程主机连不上、拒绝了令牌或协议版本不同
cancelled499130已取消

访问控制本身的拒绝另有三种:bad_host(400)、bad_origin(403)、auth_required(401)。

访问控制

每个请求依次经过三项检查(mizuoto/api/security.py):

  1. Host 头。 服务器绑定在本机地址(默认 127.0.0.1)时,Host 必须是 localhost、127.0.0.1、::1、绑定的地址,或嵌入方用 extra_hosts / allow_host() 加入的名称,否则返回 400 bad_host。这可以防止 DNS 重绑定。绑定到非本机地址(如 0.0.0.0)时不检查 Host。
  2. 来源(Origin)。 改变状态的请求(GET、HEAD、OPTIONS 以外)和 WebSocket,如果带 Origin,它必须是页面自己的来源或列在 server.allowed_origins 中,否则返回 403 bad_origin;没有 Origin 但 Sec-Fetch-Site 表明是跨站请求时同样拒绝。curl、Python 等非浏览器客户端不带这些头,不受影响。server.allowed_origins 非空时服务器还会为这些来源开启 CORS。
  3. 访问令牌。 设置了 server.access_token 时,/api/、/ws/、/v1/、/openapi.json 和 /docs 下的每个请求都要带上令牌,否则返回 401 auth_required。唯一的例外是 GET /api/v1/app/health。令牌可以用以下任一方式提供:
方式示例
Bearer 头Authorization: Bearer <令牌>
Cookiemizuoto_token=<令牌>
查询参数?token=<令牌>(浏览器里的 WebSocket 无法加请求头时使用)
socket.io 连接auth={"token": "<令牌>"}
mizuoto config set server.access_token "一个足够长的随机字符串"

网页界面遇到 401 时会弹出 访问令牌 对话框,要求输入一次。

开放到其他电脑时

用 --host 0.0.0.0 绑定后,服务器会在日志里提醒它可以被其他机器访问,但并不强制要求令牌。接口能删除文件、下载模型、读取服务器上的音频,请设置访问令牌,或放在防火墙、VPN、带身份验证的反向代理之后。

只能在本机使用的路由

下面这些路由会读写服务器磁盘上的任意位置,只接受服务器本机的浏览器(客户端地址和请求的主机名都是本机回环地址),否则返回 403 not_local。通过隧道或同机反向代理转发的请求不算本机。

路由用途
GET /audio/browse、POST /audio/files/paths浏览、登记服务器上的音频文件
POST /models/identify、POST /models/import识别、导入服务器上的模型文件夹
POST /voices/{id}/weights从服务器路径添加音色的微调权重
POST /datasets/{id}/import把服务器上的一个文件夹导入数据集
POST /outputs/{id}/reveal在文件管理器中显示结果
POST /engines/{id}/ops/{op}引擎专属操作(参数可能指向服务器文件)

其中浏览和登记的路径还必须位于 paths.browse_roots 之内(留空时为主文件夹、数据文件夹和所有驱动器/卷)。远程客户端请改用上传:PUT /audio/files,或按角色上传权重 PUT /voices/{id}/weights/{engine}/{role}。

任务

耗时的操作(合成、下载、训练、项目生成……)返回一个任务(Job),而不是等它完成:

{"id": "j1a13058b6acbeab2259", "kind": "speak", "state": "queued", "progress": null, "result": null, "error": null, ...}

state 依次经过 queued、waiting(等待 GPU、引擎或其他资源,waiting_for 说明在等什么)、running,最后是 completed、failed、cancelled 或 interrupted(服务器在任务运行中退出)。完成后 result 中有结果,失败时 error 有 code、message、detail。

操作路由
查询GET /api/v1/jobs/{id};列表 GET /api/v1/jobs?state=active
日志GET /api/v1/jobs/{id}/log?offset=0(返回 lines 和 next_offset,下次从这里继续读)
取消POST /api/v1/jobs/{id}/cancel
重试POST /api/v1/jobs/{id}/retry(失败、已取消或已中断的任务)
不排队直接运行POST /api/v1/jobs/{id}/run-anyway(在等 GPU 的任务;确认显存够用时)

合成可以直接等待结果:POST /api/v1/tts/speak?wait=true&timeout=600。任务结束后返回完成的任务,result.output_id(批量时为 result.output_ids)是结果的 id;超过 timeout 秒(默认 600,最多 3600)仍未结束时返回 504 timeout,detail.job_id 是仍在继续运行的任务,之后照常查询即可。

结果音频用 GET /api/v1/outputs/{id}/content 下载,GET /api/v1/outputs/{id} 返回它的参数、时长、采样率和生成耗时。

不想轮询的话,订阅 事件 中的 job_updated。

示例

合成并下载

BASE=http://127.0.0.1:7869
AUTH="Authorization: Bearer $MIZUOTO_TOKEN"   # 没有设置令牌时可省略

job=$(curl -s -H "$AUTH" -H "Content-Type: application/json" \
  "$BASE/api/v1/tts/speak?wait=true" \
  -d '{"model": "qwen3tts/1.7b-custom-voice", "text": "你好,欢迎使用 Mizuoto。", "voice_spec": {"preset": "vivian"}, "style": {"instruct": "语气轻快"}}')
id=$(echo "$job" | jq -r .result.output_id)
curl -s -H "$AUTH" "$BASE/api/v1/outputs/$id/content" -o hello.wav

请求体就是 SpeakRequest,主要字段如下(完整定义见 /docs):

字段说明
model<引擎>/<模型>,如 cosyvoice/cosyvoice3-0.5b
text / texts文本;texts 是一批文本,每条一个结果(模型需支持批量)
languagezh、en……或 auto
voice音色库中音色的 id 或短名
voice_spec临时给出的音色,只填一种方式:references([{"audio_id" 或 "path", "transcript"}])、preset、description / attributes(consent 字段会被忽略)
styleinstruct(指令),emotion(audio_id / path、vector、text、strength)
params通用参数:temperature、top_p、top_k、speed、seed、segment_silence_ms……
engine_options引擎专属选项
outputformat(wav、flac、mp3、ogg、m4a)、sample_rate、normalize、name
segmentationengine(默认)或 mizuoto:由谁切分长文本
strict是否拒绝模型不接受的参数;不填时用 tts.strict_params

voice、voice_spec 都不给时使用模型自带的音色(模型需支持)。只想检查请求而不运行,用 POST /api/v1/tts/plan,它返回分段、生效的参数和警告。请求中没有的顶层字段会被忽略(这样发送新版本字段的客户端仍能工作),并在警告中列出(“Ignored fields the request does not have: …”):引擎选项应放在 engine_options 中,写成 options 会被忽略。音色转换和语音编辑会把这类字段记入日志。

上传一段音频并克隆

PUT /api/v1/audio/files 的请求体就是文件本身(不是 multipart),name 查询参数给出文件名。返回的 id 作为参考音频的 audio_id:

audio_id=$(curl -s -H "$AUTH" -X PUT --data-binary @ref.wav \
  "$BASE/api/v1/audio/files?name=ref.wav" | jq -r .id)

curl -s -H "$AUTH" -H "Content-Type: application/json" "$BASE/api/v1/tts/speak?wait=true" -d @- <<EOF
{"model": "qwen3tts/1.7b-base", "text": "这是克隆出来的声音。",
 "voice_spec": {"references": [{"audio_id": "$audio_id", "transcript": "参考音频里说的话。"}]}}
EOF

不给 transcript 时,若开启了 tts.auto_transcribe 且已下载语音识别模型,会自动识别;否则模型要支持无文本克隆。想长期使用这个音色,把它加进音色库(POST /api/v1/voices),之后用 "voice": "<id>" 引用。

事件

服务器通过 socket.io 推送事件,挂在 /ws(socket.io 路径 /ws/socket.io)。推送只有服务器到客户端一个方向;所有操作仍走 REST,REST 的结果是准的,事件只用来及时刷新。

事件内容
job_updatedjob:任务的最新状态、进度、下载速度
job_logjob_id、offset、lines(最多 50 行,其余用 REST 读取)
jobs_clearedids
outputs_added / outputs_removedids(新增时还有 job_id)
voices_changed、presets_changed、models_changed、datasets_changed、projects_changedids
experiments_changednames(<引擎>/<名称>)
train_metricsexperiment、stage、points(训练损失)
engines_changedengines
runtime_changedruntime:torch 版本、已加载的模型和各引擎的状态(与 GET /runtime 相同)
compute_changedusage:设备与显存占用
settings_changedkeys:改变的设置项
import socketio  # pip install "python-socketio[client]"

sio = socketio.Client()

@sio.on("job_updated")
def on_job(data):
    job = data["job"]
    print(job["id"], job["state"], job["progress"])

sio.connect("http://127.0.0.1:7869", socketio_path="/ws/socket.io", auth={"token": "<令牌>"})
sio.wait()

流式合成

WebSocket:/api/v1/tts/stream

边生成边收到音频。协议如下:

  1. 客户端发送一条 JSON 文本消息,内容是一个 SpeakRequest。
  2. 服务器回复 {"type": "queued", "job_id": …}。
  3. 开始生成时回复 {"type": "start", "job_id": …, "sample_rate": 24000, "granularity": "token" | "segment" | "emulated"}。
  4. 之后是二进制消息:单声道 16 位小端 PCM,采样率为 start 中的值;每一段开始前有一条 {"type": "segment", "index": n}。
  5. 最后是 {"type": "end", "job_id": …, "output_ids": […], "output_id": …},或 {"type": "error", "code", "message", "detail"}。

二进制消息一定是音频,其他消息都是 JSON 文本。整段结果仍然照常保存为一个结果(output_id)。客户端随时可以发送 {"type": "cancel"} 取消。

granularity 说明音频有多“细”:token 是模型逐 token 输出(CosyVoice、VoxCPM、Qwen3-TTS),segment 是每段一次,emulated 是模型本身不分段输出、由 Mizuoto 切分文本来模拟。

流式文本输入:模型支持 流式文本输入(CosyVoice 2 和 3)时,请求中设 "text_stream": true,text 只是开头;之后发送 {"type": "text", "text": "…"} 追加文本,最后发送 {"type": "flush"}。在 flush 之前断开连接会取消任务。

import asyncio, json
import websockets  # pip install websockets

async def main():
    url = "ws://127.0.0.1:7869/api/v1/tts/stream?token=<令牌>"
    req = {"model": "cosyvoice/cosyvoice2-0.5b", "text": "边生成边播放。", "voice": "my-voice"}
    pcm = bytearray()
    async with websockets.connect(url) as ws:
        await ws.send(json.dumps(req))
        async for message in ws:
            if isinstance(message, bytes):
                pcm += message           # 交给播放器,或累积起来
                continue
            event = json.loads(message)
            if event["type"] == "start":
                rate = event["sample_rate"]
            elif event["type"] in ("end", "error"):
                print(event)
                break
    print(len(pcm) // 2 / rate, "秒")

asyncio.run(main())

HTTP:POST /tts/speak?stream=true

不方便用 WebSocket 时,同一个请求加上 stream=true,响应就是边生成边发送的音频(分块传输):

参数响应
stream_format=wav(默认)长度未知的 WAV(头部之后直接是 PCM16 数据)
stream_format=pcm单声道 16 位小端 PCM,没有文件头

响应头 X-Mizuoto-Sample-Rate 是采样率,X-Mizuoto-Job 是任务 id(结果仍会保存)。这种方式不支持流式文本输入。

curl -sN -H "$AUTH" -H "Content-Type: application/json" \
  "$BASE/api/v1/tts/speak?stream=true" \
  -d '{"model": "qwen3tts/1.7b-custom-voice", "text": "边生成边播放。", "voice_spec": {"preset": "vivian"}}' \
  | ffplay -nodisp -autoexit -

兼容 OpenAI 的接口

为 OpenAI 语音接口编写的客户端可以直接使用 Mizuoto。这两个路由在 /v1 下(不在 /api 下;有 --api-prefix 时在 <前缀>/v1),同样需要访问令牌:

路由说明
POST /v1/audio/speech朗读 input,返回音频
GET /v1/models已安装的语音模型(不含语音识别),以及 tts.model_aliases 中的别名

请求字段的对应关系:

字段Mizuoto 中的处理
modelMizuoto 的模型 id;或 tts.model_aliases 中的别名;都不是时用 tts.default_model;仍没有则返回 404
voice音色库中的音色(id 或短名);或模型的预置说话人;或 OpenAI 的音色名(alloy、ash、ballad、cedar、coral、echo、fable、marin、nova、onyx、sage、shimmer、verse),这些名字会落到模型自带的音色,模型没有自带音色时用它的第一个预置说话人。其他名字返回 404
instructions只在模型支持 指令 时使用,否则忽略
speed0.25–4;模型不接受语速时丢弃
response_formatmp3(默认)、opus(生成 ogg)、aac(生成 m4a)、flac、wav、pcm(24 kHz 单声道 16 位小端,无文件头)
stream_formataudio 且格式为 wav 或 pcm 时边生成边发送;sse 不支持

这个接口按宽松模式处理参数:模型不接受的参数被丢弃,而不是报错。错误采用 OpenAI 的格式 {"error": {"message", "type", "param", "code"}},其中 code 是 Mizuoto 的错误代码。成功的响应带有 X-Mizuoto-Job 和 X-Mizuoto-Model 头。

先给常用的名字起别名:

mizuoto config set tts.model_aliases '{"tts-1": "qwen3tts/1.7b-custom-voice", "tts-1-hd": "cosyvoice/cosyvoice3-0.5b"}'
mizuoto config set tts.default_model qwen3tts/1.7b-custom-voice
from openai import OpenAI

# api_key:设置了访问令牌时填令牌,否则任意字符串
client = OpenAI(base_url="http://127.0.0.1:7869/v1", api_key="<令牌>")

with client.audio.speech.with_streaming_response.create(
    model="tts-1",
    voice="vivian",
    input="你好,这是兼容 OpenAI 的接口。",
    instructions="用平静的语气",
    response_format="mp3",
) as response:
    response.stream_to_file("speech.mp3")

下一步

  • 在 Python 中使用:不经过 HTTP,直接调用核心服务,或把服务器嵌入自己的程序。
  • 命令行:同样的操作在终端里完成。
  • 远程主机:让引擎在另一台机器上运行。

本页目录