HTTP 服务:单端点、单并发、多种音频形态

s2_server.cpp(900 行)基于内嵌的 cpp-httplib 把 Pipeline 能力包装成一个 HTTP 服务,默认监听 127.0.0.1:3030。接口设计刻意收敛:只有一个 POST /generate 端点,靠表单字段和 params JSON 区分行为;另附 s2-openapi.yaml 作为机器可读规范。

1. 服务骨架

Server::serve() 负责全部装配:

httplib::Server svr
server_busy: shared<atomic<bool>>           单并发闸门
pipeline: init(params.pipeline)             失败直接返回
pre-routing handler → X-Request-Start 头    计时起点(:462)
logger → [END] ... (xxx ms)                 (:474)
POST /generate
listen(host, port)

两个 httplib 钩子配合实现请求计时:pre-routing 阶段把毫秒时间戳写入响应头,logger 阶段取出差值打印。合成线程被统一登记在 active_threads(:459-460),避免脱离管理。

2. /generate 的四种响应形态

同一个端点,按 stream / chunked(别名 realtime)/ output_format 参数组合出四种产物:

形态 触发条件 响应
一次性 WAV 默认 audio/wav,整段合成完一次性返回(:725-747)
最终化流式 WAV stream=true 先建图再由 BufferedAudioSink 收集,合成完返回完整 WAV(:758-788)
实时分块 chunked=true chunked transfer,边合成边推;WAV 头为占位形式
裸 PCM output_format=pcm_s16le audio/L16; rate=...; channels=1,无 WAV 包装

实时分块通过 StreamContext(chunk 双端队列 + 条件变量 + startup gating)+ HttpStreamSink 实现,合成在独立线程跑、内容 provider 在 httplib 线程拉,二者靠队列解耦。

3. params JSON:全部可调参数

字段通过 multipart 的 params 字段传入一个 JSON 对象(:518-666),非 object 或解析失败一律 400。键集合完整对应 Pipeline/Generate 的所有旋钮:

类别 键
生成 max_new_tokens、temperature、top_p、top_k、min_tokens_before_end、n_threads、verbose
codec codec_follow_backend、codec_auto_backend
声音 voice(别名 voice_id/voice_profile)、voice_dir
流式 stream_decode_stride_frames(别名 stride)、stream_holdback_frames、codec_decode_context_frames(别名 context_frames)、stream_start_buffer_ms
分句 segment_sentences、sentence_pause_ms、segment_max_chars
模式/格式 low_latency、output_format、stream、chunked、realtime(chunked 别名)

所有数值在写入前都过一遍 std::max(0/1, …) 钳制,避免负数等非法值进入流水线。表单主字段与别名通过 get_first_form_field 解析,例如参考文本接受 reference_text / ref_text / prompt_text(:511-512)。

4. 校验与状态码

服务端的错误处理遵守简单一致的约定:

状态 情况
400 缺 text(:501-507)、params 不是对象/解析失败、分句后无内容
400 上传了参考音频但没有参考转写——克隆必须同时有音频与文本(:697-701)
503 已有合成在进行(见第 5 节)
500 合成/流式失败

参考音频文件本身加载失败时不报错,而是打 warning 后不带参考继续(:751-755)——尽力服务,参考音频字段别名包括 reference / reference_audio / prompt_audio / ref_audio(:690)。

5. 503:单并发的明确拒绝

s2.cpp 的服务没有请求队列(README 已知限制,README.md:564)。原因直接:模型 + codec 占内存大、合成耗时长,排队会让请求堆积且不可预测。实现用 CAS:

bool expected_idle = false;
if (!server_busy->compare_exchange_strong(expected_idle, true)) {
    res.status = 503;                                         // :714-719
}
  • 占用在所有校验通过之后才进行,避免非法请求白占闸门;
  • 一次性和最终化流式路径在返回前同步释放;实时分块路径则在合成线程结束时释放(:882),覆盖整个音频产出周期;
  • 并发第二个请求立刻拿到 503,客户端可自行重试。

6. 启动缓冲门控

实时分块直接播 PCM 有个体验问题:若首包到达后立刻播放,后续块一旦跟不上就会卡顿断音。服务端提供 stream_start_buffer_ms 缓冲门控:

  • chunk provider 在推送前等待,直到队列累积的字节达到启动缓冲量(:821-834),用 queued_bytes 与 startup_buffer_bytes 比较;
  • 分块 PCM 默认 3000 ms(:680-683)——先攒 3 秒再开播,平滑后续抖动;
  • low_latency 模式下缓冲归零(:677-679),首包即播;
  • 客户端断连或 sink.is_writable() 为假时设置 cancelled,Pipeline 的帧回调检测到后中止合成(:814-816)。

7. 分句合成

长文本一次性合成会在 ~800 token 后出现音质/幅度退化(README.md:556)。开启 segment_sentences 后,文本先切句、逐段合成再拼接:

分段路径同时适用于最终化流和实时分块(:760-762、:874-876),保证整段音频时间线连贯。

8. 音频格式细节

格式相关辅助集中在文件开头:

  • 格式枚举 Wav / PcmS16LE 与字符串解析 parse_stream_audio_format();
  • PCM 的 content-type 按 RFC 风格构造:audio/L16; rate=<sr>; channels=1(stream_audio_content_type());
  • PCM 响应额外带 X-Audio-Sample-Rate / X-Audio-Channels / X-Audio-Encoding 头,让客户端无需 WAV 头也能获知参数(:780-782);
  • 实时 WAV 的头是占位形式(尺寸字段填 0x7FFFFFF0),客户端按流播放;最终化 WAV 则由 patch_streaming_wav_header() 回填真实尺寸。

9. 小结

HTTP 服务把一个 Pipeline 包装成”单端点、单并发”的紧凑接口:用 400/503/500 明确表达输入错误、忙、内部失败;用启动缓冲和分句改善长文本的播放体验;用 OpenAPI 文档化全部参数。对于需要把引擎嵌入非 C++ 环境的场景,第 12 章介绍另一条更底层的通道——C ABI 导出库。

关键文件

位置 职责
s2_server.cpp:447 serve 与全部路由
s2_server.cpp:518 params JSON 解析
s2_server.cpp:714 503 单并发 CAS
s2_server.cpp:809 chunked provider 与启动门控
s2_server.cpp:195 分句合成与拼接
openapi/s2-openapi.yaml OpenAPI 规范
include/s2_server.h ServerParams

This site uses Just the Docs, a documentation theme for Jekyll.