Skip to main content

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}