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