首页
/ Voicebox OpenAI 兼容 API 设计详解:把 /v1/audio 变成本地 OpenAI Audio 的直接替代

Voicebox OpenAI 兼容 API 设计详解:把 /v1/audio 变成本地 OpenAI Audio 的直接替代

2026-09-06 12:38:37作者:尤辰城Agatha

本文基于 Voicebox 仓库中的规划文档 OPENAI_SUPPORT.md 展开,完整解析“OpenAI API Compatibility”这一特性:它计划暴露 POST /v1/audio/speech(TTS)与 POST /v1/audio/transcriptions(Whisper 转写)两个 OpenAI 兼容端点,使任何使用 OpenAI SDK、LangChain/AutoGen 或 curl 的第三方工具都能把 Voicebox 作为本地 drop-in 替代。读完本文,你将理解这两个端点的请求/响应契约、voice 参数到语音档案(profile)的解析策略、与现有 TTS/Whisper 管线的复用关系、规划中的配置项与集成点,以及该特性在仓库中的最新落地状态(截至 PROJECT_STATUS.md 仍为“Not started”,社区 PR #656 待评审)。

特性定位与当前状态

该特性对应社区 issue #10(OpenAI API compatibility)。核心意图在 OPENAI_SUPPORT.md 的 Overview 中表述得很直接:

This feature exposes OpenAI-compatible endpoints from Voicebox, allowing any tool, library, or application that speaks the OpenAI Audio API to use Voicebox as a drop-in local replacement.

需要注意的版本状态:

  • 规划文档标注 Status: Planned for v0.2.0
  • 项目状态文档 PROJECT_STATUS.md(更新于 v0.5.0 周期)在其“Existing Plan Documents — Status”表格中把 OPENAI_SUPPORT.md 标记为 Not started
  • 同一文档指出社区贡献者 @neuron-tech-ai 提交的 PR #656(OpenAI-compatible /v1/audio/speech + /v1/models,正是解决 issue #10)仍在 88 个待评审 PR 队列中,尚未合入。

因此本文呈现的是“设计契约 + 源码级落点”:端点规范是规划文档中已定稿的对外契约,而实现细节则对照当前仓库真实代码结构说明每个规划模块应当接到哪里。

总体架构

规划文档给出的调用链路(外部客户端 → Voicebox 服务器 → 现有模型/档案)如下:

flowchart LR
    subgraph clients [External Clients]
        SDK[OpenAI SDK]
        Curl[curl / HTTP]
        Apps[Third-party Apps]
    end

    subgraph voicebox [Voicebox Server]
        OpenAI["/v1/audio/* endpoints"]
        TTS[TTSModel]
        Whisper[WhisperModel]
        Profiles[Voice Profiles]
    end

    SDK --> OpenAI
    Curl --> OpenAI
    Apps --> OpenAI
    OpenAI --> TTS
    OpenAI --> Whisper
    OpenAI --> Profiles

从源码结构看,这条链路在实现上是完全可行的:/v1/audio/* 端点不引入任何新模型,只是现有组件之上的薄适配层——TTS 侧复用多引擎后端注册表(backend/backends/init.py 中的 TTSBackend Protocol 与线程安全单例工厂),STT 侧复用 Whisper 后端(PyTorch 或 MLX 两种实现),profile 数据来自 SQLite 中的 VoiceProfile 表。

目标使用场景

规划文档列出的四类使用场景,也决定了兼容性的严格程度(必须严格对齐 OpenAI 的 wire format):

  • OpenAI SDK 用户openai.audio.speech.create() 直接指向 Voicebox 即可工作;
  • LLM 框架:LangChain、AutoGen 等框架的 TTS 组件可无改造指向本地 Voicebox;
  • Shell 脚本:从 OpenAI 文档复制的 curl 命令可原样使用;
  • 既有集成:任何按 OpenAI Audio API 编写的工具零代码改动。

端点一:POST /v1/audio/speech(TTS)

请求契约

对齐 OpenAI 官方 createSpeech 规范的请求体:

{
  "model": "tts-1",
  "input": "Hello world!",
  "voice": "alloy",
  "response_format": "mp3",
  "speed": 1.0
}
  • model:客户端一般填 tts-1,服务端不校验具体取值(OpenAI SDK 也允许任意模型名);
  • input:待合成的文本;
  • voice:OpenAI 侧是固定音色名(alloy/echo 等),在 Voicebox 中需映射到语音档案;
  • response_formatmp3 | wav | opus | aac | flac | pcm
  • speed:语速倍率。

响应为音频文件流(按 response_format 返回 mp3、wav、opus、aac、flac、pcm)。

Voice 映射策略

这是该端点与原生 /generate 最关键的差异点。规划文档定义的映射规则为:

  1. voice 参数映射到 Voicebox 的 profile 名称(大小写不敏感);
  2. 无匹配时使用可配置的默认 profile(对应配置项 OPENAI_COMPAT_DEFAULT_VOICE);
  3. 支持特殊语法 voice: "profile:uuid" 显式指定 profile ID。

这个策略并非凭空设计——仓库中已经存在高度同构的先例。MCP 工具与 REST 的 /speak 端点都通过 resolve_profile 做四级优先级的档案解析:

  1. 显式参数(profile 名称或 id);
  2. 按客户端绑定(MCPClientBinding,经 X-Voicebox-Client-Id 请求头关联);
  3. 全局默认(CaptureSettings.default_playback_voice_id);
  4. 均未命中则返回 None,由调用方抛出带引导信息的错误。

其底层查询函数 get_profile_orm_by_name_or_id 正是“名称或 ID 二选一”的档案查找,与 OpenAI 层规划的 resolve_voice_for_openai(先名称、后 profile:uuid、再默认值)语义一致。可以说,OpenAI 兼容层只需在现成的 resolve_profile 风格解析链上补一个“名称/ID 查找 + 显式默认值”分支即可,规划文档中的助手函数骨架如下:

async def resolve_voice_for_openai(voice: str, db: Session) -> Optional[VoiceProfile]:
    """
    Resolve OpenAI voice parameter to a Voicebox profile.

    Priority:
    1. Exact profile name match (case-insensitive)
    2. Profile ID match (if voice starts with "profile:")
    3. Default profile from config
    4. First available profile
    """
    ...

(该函数按规划应放入 backend/profiles.py;对应仓库重构后,落点为 backend/services/profiles.py。)

底层如何走到 TTS 模型

/v1/audio/speech 命中 profile 后,本质上是走一次现有的生成管线。PROJECT_STATUS.md 中记录的当前 POST /generate 完整流程为:

  1. 从数据库查 profile;
  2. 从请求解析 engine(qwen | qwen_custom_voice | luxtts | chatterbox | chatterbox_turbo | tada | kokoro);
  3. get_tts_backend_for_engine(engine) 取线程安全的引擎单例;
  4. 模型缓存缺失则触发后台下载并返回 HTTP 202;
  5. 懒加载模型 load_model(model_size)
  6. create_voice_prompt_for_profile() 构建语音提示;
  7. generate(text, voice_prompt, language, seed, instruct) 合成;
  8. 后处理(Chatterbox 系引擎做 trim_tts_output());
  9. 保存 WAV 到 data/generations/{id}.wav
  10. 写入 SQLite 历史记录;
  11. 返回 GenerationResponse

当前该管线的入口是 generate_speechPOST /generate),后台执行统一收敛到 run_generationservices.generation 模块,支持 generate/retry/regenerate 三种模式、分块合成 max_chunk_chars/crossfade_ms)。OpenAI 兼容层可以直接复用 /speak 的做法——speak 端点 就是“解析 profile → 构造 GenerationRequest → 直接调用 generate_speech”的薄包装,并顺带发布 speak-start 事件供前端显示说话 pill。/v1/audio/speech 采用同一模式即可,区别只在于:同步返回音频字节流而非异步生成记录(OpenAI 契约要求响应体就是音频)。

另外注意一个兼容层必须处理的现实细节:生成管线当前是异步任务队列enqueue_generation + 轮询状态)。若要严格对齐 OpenAI 的同步响应语义,兼容端点需要在内部等待任务完成(或走同步路径)再读回 data/generations/{id}.wav 做格式转换后输出——这是实现该端点时最核心的工程点。

端点二:POST /v1/audio/transcriptions(Whisper)

请求契约

multipart/form-data 表单,对齐 OpenAI createTranscription 规范:

字段 说明
file 音频文件
model 一般填 whisper-1
language 可选语言提示
response_format jsontextsrtverbose_jsonvtt

json 格式响应即 OpenAI 的标准形状:

{
  "text": "Hello world!"
}

与现有转写端点的对应关系

Voicebox 原生已有 POST /transcribefile + language + model 三个表单字段,返回 TranscriptionResponse)。它已经实现了 OpenAI 兼容层所需的几乎全部脏活:

  • 上传落盘:1MB 分块读取写入临时文件(UPLOAD_CHUNK_SIZE),临时文件保留原扩展名;
  • 容器兼容处理:由于 STT 后端(MLX 路径经 miniaudio)只解码 WAV/FLAC/MP3/Vorbis,非 WAV 上传会用 librosa 解码后重新编码为临时 WAV 再交给 Whisper(见 transcription.py 的注释);
  • 模型校验model 必须是 WHISPER_HF_REPOS 中注册的尺寸(base/small/medium/large/turbo,对应 openai/whisper-baseopenai/whisper-large-v3-turbo,见 backends/__init__.py),非法值返回 400;
  • 模型未缓存时返回 202,并触发后台下载任务(model_name: "whisper-{size}"),客户端可稍后重试。

也就是说 /v1/audio/transcriptions 的主要增量工作是:接受 model="whisper-1" 这类 OpenAI 风格模型名并做宽松归一化(或映射到默认 Whisper 尺寸)、按 response_format 输出 text/srt/vtt 等变体(当前原生端点只返回纯文本 text + duration)。

规划模块结构:backend/openai_compat.py

规划文档建议新建独立模块,用 APIRouter(prefix="/v1/audio") 挂载两个端点,并给出了带注释的设计骨架:

from fastapi import APIRouter, UploadFile, File, Form, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import Literal, Optional

router = APIRouter(prefix="/v1/audio", tags=["OpenAI Compatible"])

class SpeechRequest(BaseModel):
    model: str = "tts-1"
    input: str
    voice: str = "alloy"
    response_format: Literal["mp3", "wav", "opus", "aac", "flac", "pcm"] = "mp3"
    speed: float = 1.0

@router.post("/speech")
async def create_speech(request: SpeechRequest, db: Session = Depends(get_db)):
    # 1. Map voice name to profile
    # 2. Generate audio using existing TTSModel
    # 3. Convert to requested format
    # 4. Return audio stream
    ...

@router.post("/transcriptions")
async def create_transcription(
    file: UploadFile = File(...),
    model: str = Form("whisper-1"),
    language: Optional[str] = Form(None),
    response_format: str = Form("json"),
):
    # 1. Save uploaded file
    # 2. Transcribe using existing WhisperModel
    # 3. Return in requested format
    ...

四个步骤的注释恰好对应后文三节要讨论的落点:profile 解析、复用现有 TTS 管线、音频格式转换。

音频格式转换

OpenAI 契约要求支持 mp3/wav/opus/aac/flac/pcm 六种输出格式,而 Voicebox 生成管线目前的原生产物是 24 kHz 单声道 WAV。规划文档因此要求在 backend/utils/audio.py 中新增转换工具:

def convert_audio_format(
    audio: np.ndarray,
    sample_rate: int,
    target_format: str,  # mp3, wav, opus, aac, flac, pcm
) -> bytes:
    """Convert audio to target format using ffmpeg or pydub."""
    ...

对照现状可以确认这一增量是必要的:当前 utils/audio.pyload_audio() 基于 librosa(默认重采样到 24000 Hz、单声道),save_audio() 基于 soundfile 且采用“先写临时文件再原子重命名”的策略防止产生损坏/半截 WAV;库内并没有 mp3/opus 等压缩格式编码能力。依赖清单中的新增项也因此而来:pydubffmpeg-python 用于 mp3/opus 等格式转换。pcmwav 可由现有 numpy/soundfile 路径直接产出,压缩格式必须引入编码器。

配置项设计

规划文档建议在配置层增加四个开关(文档写作时期落在 backend/config.py;该文件目前主要承担数据目录与云端 URL 配置,新增兼容层配置项与其风格一致):

# OpenAI API Compatibility
OPENAI_COMPAT_ENABLED = True
OPENAI_COMPAT_DEFAULT_VOICE = None  # Profile ID or name for default voice
OPENAI_COMPAT_REQUIRE_AUTH = False  # Require API key validation
OPENAI_COMPAT_API_KEY = None        # If set, validate against this

各配置项语义:

配置项 默认值 作用
OPENAI_COMPAT_ENABLED True 总开关,控制 /v1/audio/* 路由是否挂载
OPENAI_COMPAT_DEFAULT_VOICE None voice 参数无匹配时的兜底 profile(ID 或名称)
OPENAI_COMPAT_REQUIRE_AUTH False 是否强制 API key 校验(面向共享部署)
OPENAI_COMPAT_API_KEY None 设置的 key,请求需与之匹配

默认不启用鉴权符合 Voicebox 的本地优先(local-first)定位:默认 CORS 白名单也只放行本机 Vite/Tauri 源(见 app.py 的 _configure_cors),而 VOICEBOX_CORS_ORIGINS 环境变量可追加来源。

应用集成点:从规划到当前仓库布局

规划文档写于 v0.2.0 之前,当时的集成指示是:

In backend/main.py, include the router: if config.OPENAI_COMPAT_ENABLED: app.include_router(openai_compat.router)

需要说明的是,仓库其后经历了模块化重构(backend refactor,PR #285),当前 FastAPI 应用工厂在 backend/app.pycreate_app() 中,所有领域路由统一经由 register_routers 注册(health、profiles、generations、transcription、speak 等 18 个 router)。因此按当前布局落地,OpenAI 兼容路由的正确接入方式是:

  1. 新建 backend/openai_compat.py(或放入 backend/routes/ 目录遵循现有拆分惯例);
  2. register_routers() 内按 OPENAI_COMPAT_ENABLED 条件执行 app.include_router(openai_compat_router)——注意 FastAPI 路由匹配顺序,/v1/audio/* 不会与现有 /generate/transcribe 冲突,但需先于前端 SPA 的 /{full_path:path} catch-all 注册(该 catch-all 仅在存在前端静态目录时挂载,见 app.py);
  3. 请求依赖注入沿用 Depends(get_db)database.session),与现有路由完全一致。

另外,社区 PR #656 在该端点之外还顺带补齐了 /v1/models(OpenAI 的模型列表端点),这与规划文档的端点清单略有出入,评审合入时值得留意契约完整性。

流式支持的演进路线

初始实现按规划返回完整音频文件;流式支持被明确列为后续增强。规划文档给出的方向是对话术端点按 stream 标志切换为 StreamingResponse 分块输出。

结合现状,这一路线有现成的基础设施参照:POST /generate/stream 已存在但仅限 MLX 引擎(见 PROJECT_STATUS.md 的 Known Limitations:“Streaming generation only works for Qwen on MLX”)。也就是说,OpenAI 层的流式能力天然受限于底层引擎的流式能力:MLX 路径可先行,PyTorch 各引擎需等 #804(Stream MLX TTS audio chunks)及非 MLX 流式工作(Tier 2 优先级)成熟后再补齐。

测试与验证方式

规划文档给出的三类验收方式(curl、OpenAI SDK、转写)在实现完成后均可直接复制使用:

1. curl 直接调用 TTS

# TTS with curl
curl http://localhost:8000/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{"model": "tts-1", "input": "Hello!", "voice": "MyProfile"}' \
  --output speech.mp3

2. OpenAI Python SDK 指向本地

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused")
response = client.audio.speech.create(
    model="tts-1",
    voice="MyProfile",
    input="Hello world!"
)
response.stream_to_file("output.mp3")

3. Whisper 转写

# Transcription
curl http://localhost:8000/v1/audio/transcriptions \
  -F file=@audio.mp3 \
  -F model="whisper-1"

适用前提提醒:示例中的 localhost:8000 是规划文档写作时的服务端口;当前仓库的桌面端本地服务实际监听 localhost:17493(CORS 白名单与 MCP 挂载地址均以此为准),Docker/远程部署端口则以部署配置为准,验证时应按实际服务端口调整。

安全考量

规划文档列出三条安全基线,逐条对照仓库现状说明其必要性:

  • 可选 API key 校验:对应 OPENAI_COMPAT_REQUIRE_AUTH / OPENAI_COMPAT_API_KEY。Voicebox 服务端还承载 MCP(/mcp 挂载 + ClientIdMiddleware 客户端识别),此前社区报告过本地 API+MCP 的 DNS-rebinding/Host 头暴露问题(issue #778,见 PROJECT_STATUS.md 的 trust/security 一节),因此“把端口暴露到局域网”的共享部署场景下,API key 校验应当默认建议开启;
  • 速率限制:对 /v1/audio/* 端点做 rate limiting。生成请求会触发真实 GPU/CPU 推理,是服务器资源消耗最高的操作;
  • 输入长度限制:与现有 /generate 端点保持一致。当前仓库已知长文本仍有 5 万字符上限问题(issue #464 等,分块合成 PR #266 已合入但边界仍需调优),兼容层不应绕过这一既有约束。

依赖与改动范围

规划文档明确了两点范围边界:

  • 新增依赖pydubffmpeg-python(用于 mp3、opus 等压缩格式转换)——见 requirements.txt 的现有依赖基线,二者均为轻量级新增;
  • 零模型层改动:No changes to existing TTS/Whisper model code。即 backend/backends/ 下的各引擎实现、Whisper 加载路径、语音提示缓存(utils/cache.py)均不触碰,兼容层纯增量。

小结:这份规划为什么“低努力、高价值”

从源码结构看,该特性被 PROJECT_STATUS.md 的 Tier 2 优先级评估为“Low effort once API is stable”,原因在仓库证据中一目了然:档案解析有现成的 resolve_profileget_profile_orm_by_name_or_id 两级工具,/speak 端点已示范了“薄包装复用 /generate 管线”的模式,/transcribe 端点已承担上传解码、模型校验与 202 下载兜底的全部复杂路径,音频工具层缺的只是压缩格式编码。OpenAI 兼容层的真正工作量集中在三处——同步等待异步生成完成、response_format 输出适配、voice 参数的四级解析链——而这三者都已在规划文档中给出了明确契约。对搜索该特性的读者而言,可据此核对实现进度:以 register_routers 中是否出现 /v1/audio 前缀路由、以及 requirements.txt 是否出现 pydub/ffmpeg-python 作为落地标志。

登录后查看全文
热门项目推荐
相关项目推荐