audio_cpp/options.rs
1//! 常用选项键常量与会话选项构造器。
2//!
3//! 上游选项是无类型的字符串键值(`options: { "key": "value" }`),配错键名
4//! 不会报错、只会被静默忽略(教训:`vad_threshold` 在上游从未存在,
5//! silero/marblenet 的阈值键都是 [`request::THRESHOLD`];
6//! 更隐蔽的是形状:会话选项必须包在 `"options"` 对象里传,扁平键同样被
7//! 静默丢弃)。本模块把源码级核实过的键收敛为常量,拼写错误在编译期暴露;
8//! 族名前缀别名(如 `sortformer_diar.session_len_sec`)与裸键等价
9//! (shim 内的归一化表负责映射,前提是包对了形状)。
10//!
11//! 键分两类,传参位置不同不可混用:
12//! - [`request`]:随请求走([`crate::Request::option`]),如 `language`;
13//! - [`session`]:随会话走([`SessionOptions`] →
14//! [`crate::Model::create_task_session`] 的 `session_options`),如
15//! `session_len_sec`。
16//!
17//! # 示例
18//!
19//! ```
20//! use audio_cpp::options::{SessionOptions, request, session};
21//! use audio_cpp::Request;
22//!
23//! // 请求选项:直接用常量当键
24//! let req = Request::asr("./speech.wav").option(request::LANGUAGE, "zh");
25//! // 会话选项:builder 拼出 create_task_session 要的 JSON 字符串
26//! //(注意是 {"options": {...}} 嵌套形状,与 Request::to_json 同款;
27//! // shim 只读嵌套 "options" 对象,扁平键会被静默丢弃)
28//! let opts = SessionOptions::new()
29//! .session_len_sec(100.0)
30//! .graph_capacity_mode(session::graph_capacity::GROW);
31//! assert_eq!(
32//! opts.to_json(),
33//! r#"{"options":{"graph_capacity_mode":"grow","session_len_sec":100.0}}"#
34//! );
35//! ```
36
37use std::collections::BTreeMap;
38
39/// 请求选项键(随 [`crate::Request::option`] 走)。
40pub mod request {
41 /// 转写/合成语言(如 `"zh"` / `"en"`)。
42 ///
43 /// 注意:部分族只读顶层 `language`(对齐器要求 `text_input.language`
44 /// 非空),部分只读本选项(如 Qwen3 TTS 的声音克隆参考);
45 /// [`crate::TtsRequest::language`] 与 [`crate::Request::align`] 写的是
46 /// 顶层 `language`。parakeet_tdt 会严格校验请求选项——它未声明的键
47 /// (含本键)会直接拒绝,此时只用顶层 `language`。
48 pub const LANGUAGE: &str = "language";
49 /// Qwen3 ASR 是否回填词级时间戳(`"true"` 开启)。
50 ///
51 /// 开启后还需会话选项配好对齐模型(见
52 /// [`super::session::QWEN3_ASR_FORCED_ALIGNER_MODEL_PATH`]),否则上游
53 /// 抛错;流式模式不支持本选项(直接抛错)。无词戳输出时先检查这两处,
54 /// 而非假设上游未填充。
55 pub const RETURN_TIMESTAMPS: &str = "return_timestamps";
56 /// 词级时间戳钳制到音频范围内(`"true"` 开启)。
57 ///
58 /// Qwen3 ASR 与 Qwen3 对齐器均读取本键(后者在
59 /// `model_specs/qwen3_forced_aligner.json` 声明,默认 `false`)。
60 pub const CLAMP_TIMESTAMPS_TO_AUDIO: &str = "clamp_timestamps_to_audio";
61 /// 音频分块时长(秒,浮点),如 Qwen3 ASR 流式的窗口/离线分块。
62 ///
63 /// 别名(等价):`audio_chunk_duration_sec` / `audio_chunk_duration_seconds`
64 /// / `audio_chunk_duration`。
65 pub const AUDIO_CHUNK_SECONDS: &str = "audio_chunk_seconds";
66 /// 音频分块模式:`auto` / `fixed` / `quiet_energy` / `vad` / `none`
67 /// (见 [`super::session::audio_chunk_mode`])。
68 pub const AUDIO_CHUNK_MODE: &str = "audio_chunk_mode";
69 /// VAD 阈值(silero_vad 与 marblenet_vad 均用本键)。
70 ///
71 /// 注意:`vad_threshold` 在上游从未存在,传它会被静默忽略。
72 pub const THRESHOLD: &str = "threshold";
73 /// Qwen3 TTS 声音克隆:参考音频的文本转写(越贴合内容音色越准)。
74 ///
75 /// [`crate::TtsRequest::reference_text`] 已封装本键,无需手写。
76 pub const REFERENCE_TEXT: &str = "reference_text";
77 /// 流式 TTS 坏样本重试开关(`"false"` 关闭;VoxCPM2 流式硬性要求关闭)。
78 pub const RETRY_BADCASE: &str = "retry_badcase";
79 /// YuE2 歌曲风格/标签(必填,非空)。
80 pub const STYLE: &str = "style";
81 /// YuE2 歌词(必填;与请求顶层 `text` 等价,见 `Request::tts`)。
82 pub const LYRICS: &str = "lyrics";
83 /// 采样种子(YuE2 等生成族)。
84 pub const SEED: &str = "seed";
85 /// 解码最大长度(如 SheetSage2、Moonshine)。
86 pub const MAX_TOKENS: &str = "max_tokens";
87 /// MMS 对齐器文本归一化:`latin`(默认,31 符号拉丁 CTC)/
88 /// `pre_romanized`(调用方自备 ASCII 罗马化文本)。
89 pub const TEXT_NORMALIZATION: &str = "text_normalization";
90}
91
92/// 会话选项键(随 [`SessionOptions`] 走,建会话时一次传入)。
93pub mod session {
94 /// 长音频会话图长度(秒),如 SortFormer 说话人分离默认仅 ~20s/90s,
95 /// 超长音频需调大(如 `100.0`)配合 [`GRAPH_CAPACITY_MODE`] 使用。
96 ///
97 /// 族名前缀别名(等价):`sortformer_diar.session_len_sec`。
98 pub const SESSION_LEN_SEC: &str = "session_len_sec";
99 /// 会话图容量模式(见 [`graph_capacity`] 取值)。
100 pub const GRAPH_CAPACITY_MODE: &str = "graph_capacity_mode";
101 /// Qwen3 ASR 词戳回填所需的对齐模型路径(Qwen3-ForcedAligner 权重)。
102 ///
103 /// 不配而请求开 [`super::request::RETURN_TIMESTAMPS`] 时上游抛错。
104 /// 别名(等价):`qwen3_asr.aligner_model_path`。
105 pub const QWEN3_ASR_FORCED_ALIGNER_MODEL_PATH: &str = "qwen3_asr.forced_aligner_model_path";
106
107 /// [`GRAPH_CAPACITY_MODE`](self::GRAPH_CAPACITY_MODE) 的合法取值。
108 pub mod graph_capacity {
109 /// 固定容量,超限即报 `request exceeds fixed graph capacity`。
110 pub const FIXED: &str = "fixed";
111 /// 分档容量。
112 pub const TIERED: &str = "tiered";
113 /// 按需增长(长音频 diar 常用)。
114 pub const GROW: &str = "grow";
115 /// 翻倍增长。
116 pub const DOUBLE: &str = "double";
117 }
118
119 /// [`super::request::AUDIO_CHUNK_MODE`] 的合法取值。
120 pub mod audio_chunk_mode {
121 /// 自动选择(默认)。
122 pub const AUTO: &str = "auto";
123 /// 固定窗口切分。
124 pub const FIXED: &str = "fixed";
125 /// 静音能量切分。
126 pub const QUIET_ENERGY: &str = "quiet_energy";
127 /// VAD 切分。
128 pub const VAD: &str = "vad";
129 /// 不切分。
130 pub const NONE: &str = "none";
131 }
132}
133
134/// 会话选项构造器:拼出 [`crate::Model::create_task_session`] 的
135/// `session_options` 参数要的 JSON 字符串。
136///
137/// JSON 形如 `{"options":{"session_len_sec":100.0,"graph_capacity_mode":"grow"}}`
138/// (嵌套形状,与 [`crate::Request::to_json`] 同款——shim 只读嵌套 `"options"`
139/// 对象,扁平键会被静默丢弃,见 `audio-cpp-sys/capi.cpp`);
140/// 空选项序列化为 `{}`。未知键用 [`SessionOptions::option`] 自由扩展
141/// (shim 对未知键同样静默忽略,键名请核对 [`session`] 常量)。
142///
143/// # 示例
144///
145/// ```no_run
146/// use audio_cpp::{Backend, Registry, RunMode, TaskKind, options::SessionOptions};
147///
148/// let registry = Registry::new()?;
149/// let model = registry.load("./sortformer-diar-4spk-q8_0.gguf", None, None)?;
150/// let opts = SessionOptions::new()
151/// .session_len_sec(100.0)
152/// .graph_capacity_mode(audio_cpp::options::session::graph_capacity::GROW);
153/// let opts_json = opts.to_json();
154/// let session = model.create_task_session(
155/// TaskKind::Diar, RunMode::Offline, Backend::Cpu, 0, 4, Some(&opts_json),
156/// )?;
157/// # Ok::<(), audio_cpp::Error>(())
158/// ```
159#[derive(Debug, Clone, Default)]
160pub struct SessionOptions {
161 options: BTreeMap<String, String>,
162}
163
164impl SessionOptions {
165 /// 空选项。
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 /// 设置一个选项键值(值为任意可转字符串类型)。
171 pub fn option(mut self, key: impl Into<String>, value: impl ToString) -> Self {
172 self.options.insert(key.into(), value.to_string());
173 self
174 }
175
176 /// 会话图长度(秒),见 [`session::SESSION_LEN_SEC`]。
177 pub fn session_len_sec(mut self, seconds: f64) -> Self {
178 self.options
179 .insert(session::SESSION_LEN_SEC.to_owned(), format!("{seconds:?}"));
180 self
181 }
182
183 /// 会话图容量模式(取值见 [`session::graph_capacity`]),见
184 /// [`session::GRAPH_CAPACITY_MODE`]。
185 pub fn graph_capacity_mode(mut self, mode: impl Into<String>) -> Self {
186 self.options
187 .insert(session::GRAPH_CAPACITY_MODE.to_owned(), mode.into());
188 self
189 }
190
191 /// Qwen3 ASR 词戳回填的对齐模型路径,见
192 /// [`session::QWEN3_ASR_FORCED_ALIGNER_MODEL_PATH`]。
193 pub fn qwen3_aligner_model_path(mut self, path: impl Into<String>) -> Self {
194 self.options.insert(
195 session::QWEN3_ASR_FORCED_ALIGNER_MODEL_PATH.to_owned(),
196 path.into(),
197 );
198 self
199 }
200
201 /// 序列化为 JSON 字符串(传给 `create_task_session`)。
202 ///
203 /// 嵌套在 `"options"` 键下(shim 约定,同 Request 形状);
204 /// 数值型值([`SessionOptions::session_len_sec`] 写入的)按 JSON 数字
205 /// 还原,其余按字符串;空选项为 `{}`。
206 pub fn to_json(&self) -> String {
207 if self.options.is_empty() {
208 return "{}".to_string();
209 }
210 let mut inner = serde_json::Map::new();
211 for (k, v) in &self.options {
212 // session_len_sec 写入的是 Rust float Debug 格式,还原为数字。
213 let value = if k == session::SESSION_LEN_SEC {
214 v.parse::<f64>().map_or_else(
215 |_| serde_json::Value::String(v.clone()),
216 serde_json::Value::from,
217 )
218 } else {
219 serde_json::Value::String(v.clone())
220 };
221 inner.insert(k.clone(), value);
222 }
223 let mut obj = serde_json::Map::new();
224 obj.insert("options".to_string(), serde_json::Value::Object(inner));
225 serde_json::Value::Object(obj).to_string()
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 // 测试断言中的 unwrap/expect 是惯用法:失败即测试失败,展开错误链无意义。
232 #![allow(clippy::unwrap_used, clippy::expect_used)]
233 use super::*;
234
235 #[test]
236 fn session_options_json_shape() {
237 let opts = SessionOptions::new()
238 .session_len_sec(100.0)
239 .graph_capacity_mode(session::graph_capacity::GROW);
240 let v: serde_json::Value = serde_json::from_str(&opts.to_json()).unwrap();
241 assert_eq!(
242 v,
243 serde_json::json!({
244 "options": {
245 "session_len_sec": 100.0,
246 "graph_capacity_mode": "grow",
247 }
248 })
249 );
250 }
251
252 #[test]
253 fn session_options_empty_and_custom() {
254 assert_eq!(SessionOptions::new().to_json(), "{}");
255 let opts = SessionOptions::new()
256 .qwen3_aligner_model_path("./align.gguf")
257 .option("custom_key", "v");
258 let v: serde_json::Value = serde_json::from_str(&opts.to_json()).unwrap();
259 assert_eq!(
260 v,
261 serde_json::json!({
262 "options": {
263 "qwen3_asr.forced_aligner_model_path": "./align.gguf",
264 "custom_key": "v",
265 }
266 })
267 );
268 }
269}