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 后,文本先切句、逐段合成再拼接:
- split_sentences_basic():在
. ! ? \n处硬切,并处理引号尾随(句号在引号内时不提前断); - split_long_segment():超长段先在
,;:切、仍超长按空白切; - split_text_for_segmented_synthesis() 组合两者;
- synthesize_segmented_to_sink():参考音频只解析一次复用给各段,逐段合成、裁掉尾静音,段间插入
sentence_pause_ms(默认 180 ms)的 PCM 停顿。
分段路径同时适用于最终化流和实时分块(: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 |