Skip to main content

audio_cpp/
request.rs

1//! 类型化任务请求([`Request`])构造器。
2//!
3//! 上层 API 的所有请求入口(离线 [`crate::Session::run_offline`]、流式
4//! [`crate::Session::start`] / [`crate::Session::prepare`])都接受
5//! [`IntoRequest`]:既可以是本模块的类型化 [`Request`] 枚举,也可以是任意
6//! JSON 字符串(透传给 C 边界)。用 [`Request`] 构造时无需手工拼接 / 转义
7//! JSON,Windows 路径也无需手动转义反斜杠。
8//!
9//! 每个任务一种变体,携带各自参数:
10//! ```rust
11//! use audio_cpp::Request;
12//!
13//! // 离线 VAD:音频 + 阈值选项(silero/marblenet 均用 `threshold`)
14//! let r1 = Request::vad("./speech.wav").option("threshold", 0.5);
15//! // 离线 / 流式 ASR
16//! let r2 = Request::asr("./speech.wav");
17//! let r3 = Request::asr("./speech.wav").option("audio_chunk_seconds", 3.0);
18//! // TTS:文本(可选说话人参考,如 Qwen3 TTS 声音克隆)
19//! let r4 = Request::tts("Hello!");
20//! let r5 = Request::tts("Hello!").reference("./ref.wav").reference_text("参考文本");
21//! // 原始 JSON 透传
22//! let r6 = Request::json(r#"{"audio_path":"./speech.wav"}"#);
23//! ```
24
25use std::collections::BTreeMap;
26use std::path::{Path, PathBuf};
27
28use serde_json::Value;
29
30use crate::audio::WavAudio;
31use crate::error::Error;
32
33/// 请求里的音频输入:本地文件路径,或内嵌采样数据。
34#[derive(Debug, Clone)]
35pub enum AudioInput {
36    /// 从本地 WAV 文件读取(等价于 JSON 顶层 `audio_path`)。
37    Path(String),
38    /// 内嵌音频数据(等价于 JSON 顶层 `audio` 对象)。
39    Buffer(WavAudio),
40}
41
42impl From<&str> for AudioInput {
43    fn from(s: &str) -> Self {
44        AudioInput::Path(s.to_owned())
45    }
46}
47
48impl From<String> for AudioInput {
49    fn from(s: String) -> Self {
50        AudioInput::Path(s)
51    }
52}
53
54impl From<&String> for AudioInput {
55    fn from(s: &String) -> Self {
56        AudioInput::Path(s.clone())
57    }
58}
59
60impl From<WavAudio> for AudioInput {
61    fn from(a: WavAudio) -> Self {
62        AudioInput::Buffer(a)
63    }
64}
65
66impl From<&Path> for AudioInput {
67    fn from(p: &Path) -> Self {
68        AudioInput::Path(p.to_string_lossy().into_owned())
69    }
70}
71
72impl From<PathBuf> for AudioInput {
73    fn from(p: PathBuf) -> Self {
74        AudioInput::Path(p.to_string_lossy().into_owned())
75    }
76}
77
78/// 音频类任务(VAD / ASR / 说话人分离 / 源分离)的请求参数。
79#[derive(Debug, Clone, Default)]
80pub struct AudioRequest {
81    /// 音频输入(文件路径或内嵌数据)。
82    pub audio: Option<AudioInput>,
83    /// 附加选项键值(如 VAD 的 `threshold`、流式 ASR 的
84    /// `audio_chunk_seconds`)。非字符串值由 shim 字符串化后交给上游。
85    pub options: BTreeMap<String, Value>,
86}
87
88impl AudioRequest {
89    /// 以音频输入构造请求。
90    pub fn new(audio: impl Into<AudioInput>) -> Self {
91        Self {
92            audio: Some(audio.into()),
93            options: BTreeMap::new(),
94        }
95    }
96
97    /// 构造仅含选项的请求(无音频输入)。
98    ///
99    /// 用于流式 `start` / `prepare` 阶段:此时音频经
100    /// [`crate::Session::process_audio`] 逐块送入,请求本身只需携带
101    /// `options`(如 `language`、`audio_chunk_seconds`)。
102    pub fn options_only() -> Self {
103        Self {
104            audio: None,
105            options: BTreeMap::new(),
106        }
107    }
108
109    /// 设置一个选项键值(等价于 JSON `options` 里的一个字段)。
110    pub fn option<V: Into<Value>>(mut self, key: impl Into<String>, value: V) -> Self {
111        self.options.insert(key.into(), value.into());
112        self
113    }
114
115    /// 批量设置选项。
116    pub fn options<K, V>(mut self, opts: impl IntoIterator<Item = (K, V)>) -> Self
117    where
118        K: Into<String>,
119        V: Into<Value>,
120    {
121        self.options
122            .extend(opts.into_iter().map(|(k, v)| (k.into(), v.into())));
123        self
124    }
125}
126
127/// 说话人参考 / 风格条件(映射到上游请求 JSON 的 `voice` 字段)。
128///
129/// 比把参考音频塞进顶层 `audio` 更语义化:支持「用参考音频克隆音色」、
130/// 「用 `cached_voice_id` 复用已注册音色」、以及风格/语速/音高控制。
131/// 通过 [`TtsRequest::voice`] 设置。
132#[derive(Debug, Clone, Default)]
133pub struct VoiceCondition {
134    /// 说话人参考音频(声音克隆)。
135    pub speaker_audio: Option<AudioInput>,
136    /// 已缓存的音色 id(复用之前克隆得到的音色,免去再次上传参考音频)。
137    pub cached_voice_id: Option<String>,
138    /// 风格:语言。
139    pub language: Option<String>,
140    /// 风格:情绪。
141    pub emotion: Option<String>,
142    /// 风格:语速缩放。
143    pub speaking_rate: Option<f32>,
144    /// 风格:音高偏移。
145    pub pitch_shift: Option<f32>,
146    /// 风格:能量缩放。
147    pub energy_scale: Option<f32>,
148    /// 风格:自由标签。
149    pub tags: BTreeMap<String, String>,
150}
151
152impl VoiceCondition {
153    /// 以说话人参考音频(声音克隆)构造。
154    pub fn speaker(audio: impl Into<AudioInput>) -> Self {
155        Self {
156            speaker_audio: Some(audio.into()),
157            ..Default::default()
158        }
159    }
160
161    /// 以已缓存音色 id 构造(复用音色)。
162    pub fn cached(id: impl Into<String>) -> Self {
163        Self {
164            cached_voice_id: Some(id.into()),
165            ..Default::default()
166        }
167    }
168
169    /// 设置语言。
170    pub fn language(mut self, language: impl Into<String>) -> Self {
171        self.language = Some(language.into());
172        self
173    }
174
175    /// 设置情绪。
176    pub fn emotion(mut self, emotion: impl Into<String>) -> Self {
177        self.emotion = Some(emotion.into());
178        self
179    }
180
181    /// 设置语速缩放。
182    pub fn speaking_rate(mut self, rate: f32) -> Self {
183        self.speaking_rate = Some(rate);
184        self
185    }
186
187    /// 设置音高偏移。
188    pub fn pitch_shift(mut self, shift: f32) -> Self {
189        self.pitch_shift = Some(shift);
190        self
191    }
192
193    /// 设置能量缩放。
194    pub fn energy_scale(mut self, scale: f32) -> Self {
195        self.energy_scale = Some(scale);
196        self
197    }
198
199    /// 设置一个风格标签。
200    pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
201        self.tags.insert(key.into(), value.into());
202        self
203    }
204}
205
206/// 语音合成(TTS)的请求参数。
207#[derive(Debug, Clone)]
208pub struct TtsRequest {
209    /// 待合成文本。
210    pub text: String,
211    /// 文本语言(可选)。
212    pub language: Option<String>,
213    /// 说话人参考音频(声音克隆,如 Qwen3 TTS base 变体)。
214    pub reference_audio: Option<AudioInput>,
215    /// 参考音频的文本转写(经 `options.reference_text` 交给上游)。
216    pub reference_text: Option<String>,
217    /// 说话人参考 / 风格条件(映射到上游 `voice` 字段)。
218    pub voice: Option<VoiceCondition>,
219    /// 附加选项键值(如流式 TTS 的 `retry_badcase`)。
220    pub options: BTreeMap<String, Value>,
221}
222
223impl TtsRequest {
224    /// 以待合成文本构造请求。
225    pub fn new(text: impl Into<String>) -> Self {
226        Self {
227            text: text.into(),
228            language: None,
229            reference_audio: None,
230            reference_text: None,
231            voice: None,
232            options: BTreeMap::new(),
233        }
234    }
235
236    /// 设置文本语言。
237    pub fn language(mut self, language: impl Into<String>) -> Self {
238        self.language = Some(language.into());
239        self
240    }
241
242    /// 设置说话人参考音频(声音克隆)。
243    pub fn reference(mut self, audio: impl Into<AudioInput>) -> Self {
244        self.reference_audio = Some(audio.into());
245        self
246    }
247
248    /// 设置参考音频的文本转写。
249    pub fn reference_text(mut self, text: impl Into<String>) -> Self {
250        self.reference_text = Some(text.into());
251        self
252    }
253
254    /// 设置说话人参考 / 风格条件(映射到上游 `voice` 字段)。
255    ///
256    /// 例:`Request::tts("Hi").voice(VoiceCondition::speaker("./ref.wav").emotion("happy"))`。
257    pub fn voice(mut self, voice: VoiceCondition) -> Self {
258        self.voice = Some(voice);
259        self
260    }
261
262    /// 设置一个选项键值(等价于 JSON `options` 里的一个字段)。
263    pub fn option<V: Into<Value>>(mut self, key: impl Into<String>, value: V) -> Self {
264        self.options.insert(key.into(), value.into());
265        self
266    }
267
268    /// 批量设置选项。
269    pub fn options<K, V>(mut self, opts: impl IntoIterator<Item = (K, V)>) -> Self
270    where
271        K: Into<String>,
272        V: Into<Value>,
273    {
274        self.options
275            .extend(opts.into_iter().map(|(k, v)| (k.into(), v.into())));
276        self
277    }
278}
279
280/// 强制对齐(Alignment)的请求参数:音频 + 待对齐文本 + 文本语言。
281///
282/// 序列化形状为顶层 `audio`(或 `audio_path`)+ `text` + `language`(+ 可选
283/// `options`),对应 shim `parse_task_request` 填充的上游 `TaskRequest` 的
284/// `audio_input` + `text_input{ text, language }`。两对齐族(`qwen3_forced_aligner` /
285/// `mms_forced_aligner`)均要求文本与语言非空,否则上游抛错。
286#[derive(Debug, Clone)]
287pub struct AlignRequest {
288    /// 待对齐音频(文件路径或内嵌数据)。
289    pub audio: Option<AudioInput>,
290    /// 待对齐的文本转写(需与音频内容一致,越贴合时间戳越准)。
291    pub text: String,
292    /// 文本语言(必填,上游不支持 `"auto"`,见 [`Request::align`])。
293    pub language: String,
294    /// 附加选项键值(如 `clamp_timestamps_to_audio`)。
295    pub options: BTreeMap<String, Value>,
296}
297
298impl AlignRequest {
299    /// 以音频、待对齐文本、文本语言构造请求。
300    pub fn new(
301        audio: impl Into<AudioInput>,
302        text: impl Into<String>,
303        language: impl Into<String>,
304    ) -> Self {
305        Self {
306            audio: Some(audio.into()),
307            text: text.into(),
308            language: language.into(),
309            options: BTreeMap::new(),
310        }
311    }
312
313    /// 设置一个选项键值(等价于 JSON `options` 里的一个字段)。
314    pub fn option<V: Into<Value>>(mut self, key: impl Into<String>, value: V) -> Self {
315        self.options.insert(key.into(), value.into());
316        self
317    }
318
319    /// 批量设置选项。
320    pub fn options<K, V>(mut self, opts: impl IntoIterator<Item = (K, V)>) -> Self
321    where
322        K: Into<String>,
323        V: Into<Value>,
324    {
325        self.options
326            .extend(opts.into_iter().map(|(k, v)| (k.into(), v.into())));
327        self
328    }
329}
330
331/// 一次任务请求。
332///
333/// 对应 capi.cpp `parse_task_request` 读取的顶层 JSON 字段(`text` /
334/// `language` / `audio` / `audio_path` / `options`)。按任务种类区分变体,
335/// 每个变体携带自己的参数;序列化时统一展开为上述 JSON 形状。
336///
337/// 任务种类本身由 [`crate::Model::create_task_session`] 的 `task` 参数决定,
338/// 变体只决定请求形状(音频型 / 文本型 / 原始 JSON)。`TaskKind` 到构造器的
339/// 推荐映射:
340///
341/// | 任务 | 构造器 |
342/// |---|---|
343/// | `Vad` / `Asr` / `Diar` / `SourceSeparation` | `vad` / `asr` / `diar` / `source_separation`(音频型) |
344/// | `Tts`(含 `VoiceCloning`/`VoiceConversion`/`VoiceDesign` 等文本型任务) | `tts`(文本型,可带参考音频/风格) |
345/// | `AudioGeneration`(音乐生成,如 YuE2:歌词 + `style` 选项) | `tts`(文本型)或 `json` |
346/// | `Alignment`(强制对齐:音频 + 文本 + 语言) | `align` |
347/// | `Midi`(音频转谱,如 SheetSage2:音频型,结果在产物字段) | 形状相同的任一音频型变体,或 `json` |
348/// | `SpeechToSpeech` / `Svc` / `SpeakerRecognition`(音频型) | 形状相同的任一音频型变体,或 `json` |
349/// | 其他 / 形状特殊 | `json`(原始 JSON 透传) |
350///
351/// 音频型四个变体(`Vad`/`Asr`/`Diar`/`SourceSeparation`)序列化形状完全
352/// 相同,选用时以语义就近为准。
353#[derive(Debug, Clone)]
354pub enum Request {
355    /// 语音活动检测:需音频输入,可配 `threshold` 等。
356    Vad(AudioRequest),
357    /// 语音识别:需音频输入,流式时用 `audio_chunk_seconds` 控制窗口。
358    Asr(AudioRequest),
359    /// 说话人分离:需音频输入。
360    Diar(AudioRequest),
361    /// 音乐源分离(Demucs 等):需音频输入。
362    SourceSeparation(AudioRequest),
363    /// 语音合成:需文本,可带说话人参考音频(声音克隆)。
364    ///
365    /// 用 [`Box`] 包裹以缩小枚举整体体积(`TtsRequest` 远大于其他变体)。
366    Tts(Box<TtsRequest>),
367    /// 强制对齐:需音频 + 待对齐文本 + 文本语言。
368    ///
369    /// 用 [`Box`] 包裹以缩小枚举整体体积。
370    Align(Box<AlignRequest>),
371    /// 原始 JSON 字符串透传(不经任何序列化改动)。
372    Json(String),
373}
374
375impl Request {
376    /// VAD 请求:以音频输入构造。
377    pub fn vad(audio: impl Into<AudioInput>) -> Self {
378        Request::Vad(AudioRequest::new(audio))
379    }
380
381    /// ASR 请求:以音频输入构造。
382    pub fn asr(audio: impl Into<AudioInput>) -> Self {
383        Request::Asr(AudioRequest::new(audio))
384    }
385
386    /// 流式 ASR 请求:仅含选项,无音频输入。
387    ///
388    /// 用于流式会话的 `start` / `prepare`(音频经
389    /// [`crate::Session::process_audio`] 逐块送入,无需 `audio_path`)。
390    /// 例如:
391    /// ```
392    /// use audio_cpp::Request;
393    /// let req = Request::stream_asr().option("language", "auto").option("audio_chunk_seconds", 3.0);
394    /// assert_eq!(req.to_json().unwrap(), r#"{"options":{"audio_chunk_seconds":3.0,"language":"auto"}}"#);
395    /// ```
396    pub fn stream_asr() -> Self {
397        Request::Asr(AudioRequest::options_only())
398    }
399
400    /// [`Request::stream_asr`] 的旧名(保留兼容,请用 `stream_asr`)。
401    #[deprecated(
402        since = "0.4.0",
403        note = "请改用 `Request::stream_asr`(原名易被误用于 TTS 流式)"
404    )]
405    pub fn stream() -> Self {
406        Self::stream_asr()
407    }
408
409    /// 说话人分离请求:以音频输入构造。
410    pub fn diar(audio: impl Into<AudioInput>) -> Self {
411        Request::Diar(AudioRequest::new(audio))
412    }
413
414    /// 音乐源分离请求:以音频输入构造。
415    pub fn source_separation(audio: impl Into<AudioInput>) -> Self {
416        Request::SourceSeparation(AudioRequest::new(audio))
417    }
418
419    /// TTS 请求:以待合成文本构造。
420    pub fn tts(text: impl Into<String>) -> Self {
421        Request::Tts(Box::new(TtsRequest::new(text)))
422    }
423
424    /// 强制对齐请求:以音频、待对齐文本、文本语言构造。
425    ///
426    /// 序列化为 `{"audio_path"|"audio", "text", "language", "options?"}`,
427    /// 配 [`TaskKind::Alignment`] 会话使用,结果的逐词时间戳在
428    /// [`crate::TaskResult::word_timestamps`]。例如:
429    /// ```
430    /// use audio_cpp::Request;
431    /// let req = Request::align("./speech.wav", "你好世界", "zh")
432    ///     .option("clamp_timestamps_to_audio", true);
433    /// let v: serde_json::Value = serde_json::from_str(&req.to_json().unwrap()).unwrap();
434    /// assert_eq!(v["text"], "你好世界");
435    /// assert_eq!(v["language"], "zh");
436    /// ```
437    ///
438    /// # 语言代码(上游要求非空,不支持 `"auto"`)
439    ///
440    /// - `qwen3_forced_aligner`:`zh` / `en` / `yue` / `fr` / `de` / `it` /
441    ///   `ja` / `ko` / `pt` / `ru` / `es`(11 语种,见上游
442    ///   `model_specs/qwen3_forced_aligner.json`);错配不报错,只产出劣质时间戳。
443    /// - `mms_forced_aligner`:`nl`(`nld`)/ `en`(`eng`)拉丁文本;非拉丁输入
444    ///   直接拒绝(可用 `text_normalization=pre_romanized` 传调用方自备的 ASCII
445    ///   罗马化文本)。权重为 CC-BY-NC-4.0,禁止分发。
446    pub fn align(
447        audio: impl Into<AudioInput>,
448        text: impl Into<String>,
449        language: impl Into<String>,
450    ) -> Self {
451        Request::Align(Box::new(AlignRequest::new(audio, text, language)))
452    }
453
454    /// 原始 JSON 字符串透传(不经序列化改动,直接交给 C 边界)。
455    pub fn json(s: impl Into<String>) -> Self {
456        Request::Json(s.into())
457    }
458
459    /// 设置一个选项键值(等价于 JSON `options` 里的一个字段)。
460    ///
461    /// **注意**:对 [`Request::Json`] 调用将被**静默忽略**(原始 JSON
462    /// 不经序列化改动)。如需给透传 JSON 加选项,请直接改 JSON 字符串。
463    pub fn option<V: Into<Value>>(self, key: impl Into<String>, value: V) -> Self {
464        let key = key.into();
465        let value = value.into();
466        match self {
467            Request::Vad(r) => Request::Vad(r.option(key, value)),
468            Request::Asr(r) => Request::Asr(r.option(key, value)),
469            Request::Diar(r) => Request::Diar(r.option(key, value)),
470            Request::SourceSeparation(r) => Request::SourceSeparation(r.option(key, value)),
471            Request::Tts(r) => Request::Tts(Box::new(r.option(key, value))),
472            Request::Align(r) => Request::Align(Box::new(r.option(key, value))),
473            Request::Json(_) => self,
474        }
475    }
476
477    /// 批量设置选项。对 [`Request::Json`] 调用将被**静默忽略**(见 [`Request::option`])。
478    pub fn options<K, V>(self, opts: impl IntoIterator<Item = (K, V)>) -> Self
479    where
480        K: Into<String>,
481        V: Into<Value>,
482    {
483        let opts: Vec<(String, Value)> = opts
484            .into_iter()
485            .map(|(k, v)| (k.into(), v.into()))
486            .collect();
487        match self {
488            Request::Vad(r) => Request::Vad(r.options(opts)),
489            Request::Asr(r) => Request::Asr(r.options(opts)),
490            Request::Diar(r) => Request::Diar(r.options(opts)),
491            Request::SourceSeparation(r) => Request::SourceSeparation(r.options(opts)),
492            Request::Tts(r) => Request::Tts(Box::new(r.options(opts))),
493            Request::Align(r) => Request::Align(Box::new(r.options(opts))),
494            Request::Json(_) => self,
495        }
496    }
497
498    /// 设置说话人参考音频(仅对 [`Request::Tts`] 有效,其余变体**静默忽略**)。
499    pub fn reference(self, audio: impl Into<AudioInput>) -> Self {
500        match self {
501            Request::Tts(r) => Request::Tts(Box::new(r.reference(audio))),
502            other => other,
503        }
504    }
505
506    /// 设置参考音频的文本转写(仅对 [`Request::Tts`] 有效,其余变体**静默忽略**)。
507    pub fn reference_text(self, text: impl Into<String>) -> Self {
508        match self {
509            Request::Tts(r) => Request::Tts(Box::new(r.reference_text(text))),
510            other => other,
511        }
512    }
513
514    /// 设置说话人参考 / 风格条件(仅对 [`Request::Tts`] 有效,其余变体**静默忽略**)。
515    pub fn voice(self, voice: VoiceCondition) -> Self {
516        match self {
517            Request::Tts(r) => Request::Tts(Box::new(r.voice(voice))),
518            other => other,
519        }
520    }
521
522    /// 设置文本语言(仅对 [`Request::Tts`] 有效,其余变体**静默忽略**)。
523    pub fn language(self, language: impl Into<String>) -> Self {
524        match self {
525            Request::Tts(r) => Request::Tts(Box::new(r.language(language))),
526            other => other,
527        }
528    }
529
530    /// 序列化为 JSON 字符串(传给底层 C ABI)。[`Request::Json`] 原样返回。
531    ///
532    /// # Errors
533    ///
534    /// 当内部对象无法序列化为合法 JSON 时返回 [`Error::Json`]。
535    pub fn to_json(&self) -> Result<String, Error> {
536        let mut obj = serde_json::Map::new();
537        match self {
538            Request::Json(s) => return Ok(s.clone()),
539            Request::Vad(r) | Request::Asr(r) | Request::Diar(r) | Request::SourceSeparation(r) => {
540                if let Some(audio) = &r.audio {
541                    write_audio(&mut obj, audio);
542                }
543                if !r.options.is_empty() {
544                    obj.insert(
545                        "options".into(),
546                        Value::Object(r.options.clone().into_iter().collect()),
547                    );
548                }
549            }
550            Request::Tts(r) => {
551                obj.insert("text".into(), Value::String(r.text.clone()));
552                if let Some(lang) = &r.language {
553                    obj.insert("language".into(), Value::String(lang.clone()));
554                }
555                if let Some(audio) = &r.reference_audio {
556                    write_audio(&mut obj, audio);
557                }
558                let mut options = r.options.clone();
559                if let Some(rt) = &r.reference_text {
560                    options.insert("reference_text".into(), Value::String(rt.clone()));
561                }
562                if !options.is_empty() {
563                    obj.insert(
564                        "options".into(),
565                        Value::Object(options.into_iter().collect()),
566                    );
567                }
568                if let Some(voice) = &r.voice {
569                    obj.insert("voice".into(), serialize_voice(voice));
570                }
571            }
572            Request::Align(r) => {
573                // 对齐形状:音频 + 顶层 text/language(shim 填 text_input)。
574                obj.insert("text".into(), Value::String(r.text.clone()));
575                obj.insert("language".into(), Value::String(r.language.clone()));
576                if let Some(audio) = &r.audio {
577                    write_audio(&mut obj, audio);
578                }
579                if !r.options.is_empty() {
580                    obj.insert(
581                        "options".into(),
582                        Value::Object(r.options.clone().into_iter().collect()),
583                    );
584                }
585            }
586        }
587        Ok(serde_json::to_string(&Value::Object(obj))?)
588    }
589}
590
591/// 把音频输入写入请求根对象(`audio_path` 或 `audio` 对象)。
592fn write_audio(obj: &mut serde_json::Map<String, Value>, input: &AudioInput) {
593    match input {
594        AudioInput::Path(p) => {
595            obj.insert("audio_path".into(), Value::String(p.clone()));
596        }
597        AudioInput::Buffer(buf) => {
598            obj.insert(
599                "audio".into(),
600                serde_json::json!({
601                    "sample_rate": buf.sample_rate,
602                    "channels": buf.channels,
603                    "samples": buf.samples,
604                }),
605            );
606        }
607    }
608}
609
610/// 把 [`VoiceCondition`] 序列化为上游请求的 `voice` 对象。
611fn serialize_voice(voice: &VoiceCondition) -> Value {
612    let mut speaker = serde_json::Map::new();
613    if let Some(id) = &voice.cached_voice_id {
614        speaker.insert("cached_voice_id".into(), Value::String(id.clone()));
615    }
616    if let Some(audio) = &voice.speaker_audio {
617        match audio {
618            AudioInput::Path(p) => {
619                speaker.insert("audio_path".into(), Value::String(p.clone()));
620            }
621            AudioInput::Buffer(buf) => {
622                speaker.insert(
623                    "audio".into(),
624                    serde_json::json!({
625                        "sample_rate": buf.sample_rate,
626                        "channels": buf.channels,
627                        "samples": buf.samples,
628                    }),
629                );
630            }
631        }
632    }
633    let mut style = serde_json::Map::new();
634    if let Some(v) = &voice.language {
635        style.insert("language".into(), Value::String(v.clone()));
636    }
637    if let Some(v) = &voice.emotion {
638        style.insert("emotion".into(), Value::String(v.clone()));
639    }
640    if let Some(v) = &voice.speaking_rate {
641        style.insert("speaking_rate".into(), serde_json::json!(v));
642    }
643    if let Some(v) = &voice.pitch_shift {
644        style.insert("pitch_shift".into(), serde_json::json!(v));
645    }
646    if let Some(v) = &voice.energy_scale {
647        style.insert("energy_scale".into(), serde_json::json!(v));
648    }
649    if !voice.tags.is_empty() {
650        let mut tags = serde_json::Map::new();
651        for (k, v) in &voice.tags {
652            tags.insert(k.clone(), Value::String(v.clone()));
653        }
654        style.insert("tags".into(), Value::Object(tags));
655    }
656
657    let mut voice_obj = serde_json::Map::new();
658    // 上游 `parse_task_request` 对缺失的 speaker/style 对象同样容忍(find
659    // 为空即跳过),故空对象直接省略,减少噪音。
660    if !speaker.is_empty() {
661        voice_obj.insert("speaker".into(), Value::Object(speaker));
662    }
663    if !style.is_empty() {
664        voice_obj.insert("style".into(), Value::Object(style));
665    }
666    Value::Object(voice_obj)
667}
668
669/// 可转换为一次任务请求的参数。
670///
671/// 已为以下类型实现:
672/// - [`Request`] / `&Request`:类型化请求;
673/// - `&str` / `String`:任意 JSON 字符串(透传给 C 边界);
674/// - `()`:空请求(等价于 `{}`)。
675pub trait IntoRequest {
676    /// 转换为请求对象。
677    ///
678    /// # Errors
679    ///
680    /// 永远成功(各实现均为 infallible),保留 `Result` 以便后续扩展。
681    fn into_request(self) -> Result<Request, Error>;
682}
683
684impl IntoRequest for Request {
685    fn into_request(self) -> Result<Request, Error> {
686        Ok(self)
687    }
688}
689
690impl IntoRequest for &Request {
691    fn into_request(self) -> Result<Request, Error> {
692        Ok(self.clone())
693    }
694}
695
696impl IntoRequest for () {
697    fn into_request(self) -> Result<Request, Error> {
698        Ok(Request::Json("{}".into()))
699    }
700}
701
702impl IntoRequest for &str {
703    fn into_request(self) -> Result<Request, Error> {
704        Ok(Request::Json(self.to_string()))
705    }
706}
707
708impl IntoRequest for String {
709    fn into_request(self) -> Result<Request, Error> {
710        Ok(Request::Json(self))
711    }
712}
713
714#[cfg(test)]
715mod tests {
716    // 测试断言中的 unwrap/expect 是惯用法:失败即测试失败,展开错误链无意义。
717    #![allow(clippy::unwrap_used, clippy::expect_used)]
718    use super::*;
719
720    fn json(s: &str) -> serde_json::Value {
721        serde_json::from_str(s).expect("合法 JSON")
722    }
723
724    #[test]
725    fn stream_start_options_only() {
726        let req = Request::stream_asr()
727            .option("language", "auto")
728            .option("audio_chunk_seconds", 3.0);
729        assert_eq!(
730            json(&req.to_json().unwrap()),
731            json(r#"{"options":{"audio_chunk_seconds":3.0,"language":"auto"}}"#)
732        );
733    }
734
735    #[test]
736    fn stream_start_empty() {
737        // 无音频、无选项的流式 start 请求等价于空对象。
738        let req = Request::stream_asr();
739        assert_eq!(json(&req.to_json().unwrap()), json(r#"{}"#));
740    }
741
742    #[test]
743    #[allow(deprecated)]
744    fn stream_deprecated_alias() {
745        // 旧名 stream() 与 stream_asr() 等价(保留兼容)。
746        let req = Request::stream().option("language", "auto");
747        assert_eq!(
748            json(&req.to_json().unwrap()),
749            json(r#"{"options":{"language":"auto"}}"#)
750        );
751    }
752
753    #[test]
754    fn vad_offline() {
755        let req = Request::vad("./a.wav").option("threshold", 0.5);
756        assert_eq!(
757            json(&req.to_json().unwrap()),
758            json(r#"{"audio_path":"./a.wav","options":{"threshold":0.5}}"#)
759        );
760    }
761
762    #[test]
763    fn asr_streaming_window() {
764        let req = Request::asr("./a.wav").option("audio_chunk_seconds", 3.0);
765        assert_eq!(
766            json(&req.to_json().unwrap()),
767            json(r#"{"audio_path":"./a.wav","options":{"audio_chunk_seconds":3.0}}"#)
768        );
769    }
770
771    #[test]
772    fn tts_text() {
773        let req = Request::tts("Hello!");
774        assert_eq!(json(&req.to_json().unwrap()), json(r#"{"text":"Hello!"}"#));
775    }
776
777    #[test]
778    fn tts_voice_clone() {
779        let req = Request::tts("Hi")
780            .reference("./ref.wav")
781            .reference_text("参考文本")
782            .language("zh");
783        assert_eq!(
784            json(&req.to_json().unwrap()),
785            json(
786                r#"{"text":"Hi","language":"zh","audio_path":"./ref.wav","options":{"reference_text":"参考文本"}}"#
787            )
788        );
789    }
790
791    #[test]
792    fn windows_path_no_manual_escaping() {
793        // 反斜杠与引号由序列化自动转义。
794        let req = Request::asr("C:\\dir\\spe\"ch.wav");
795        assert_eq!(
796            json(&req.to_json().unwrap()),
797            json(r#"{"audio_path":"C:\\dir\\spe\"ch.wav"}"#)
798        );
799    }
800
801    #[test]
802    fn diar_offline() {
803        let req = Request::diar("./meeting.wav").option("num_speakers", 4);
804        assert_eq!(
805            json(&req.to_json().unwrap()),
806            json(r#"{"audio_path":"./meeting.wav","options":{"num_speakers":4}}"#)
807        );
808    }
809
810    #[test]
811    fn source_separation_offline() {
812        let req = Request::source_separation("./song.wav").options([("stem", "vocals")]);
813        assert_eq!(
814            json(&req.to_json().unwrap()),
815            json(r#"{"audio_path":"./song.wav","options":{"stem":"vocals"}}"#)
816        );
817    }
818
819    #[test]
820    fn align_offline() {
821        // 对齐形状:音频 + 顶层 text/language + 可选 options。
822        let req = Request::align("./speech.wav", "你好世界", "zh")
823            .option("clamp_timestamps_to_audio", true);
824        assert_eq!(
825            json(&req.to_json().unwrap()),
826            json(
827                r#"{"audio_path":"./speech.wav","text":"你好世界","language":"zh","options":{"clamp_timestamps_to_audio":true}}"#
828            )
829        );
830    }
831
832    #[test]
833    fn align_embedded_audio() {
834        let buf = WavAudio {
835            sample_rate: 16000,
836            channels: 1,
837            samples: vec![0.0, 0.5],
838        };
839        let req = Request::align(AudioInput::Buffer(buf), "hello", "en");
840        assert_eq!(
841            json(&req.to_json().unwrap()),
842            json(
843                r#"{"text":"hello","language":"en","audio":{"sample_rate":16000,"channels":1,"samples":[0.0,0.5]}}"#
844            )
845        );
846    }
847
848    #[test]
849    fn tts_voice_condition() {
850        // 说话人参考 + 风格控制映射到上游 `voice` 字段。
851        let req = Request::tts("Hi").voice(
852            VoiceCondition::speaker("./ref.wav")
853                .emotion("happy")
854                .speaking_rate(1.1)
855                .tag("gender", "female"),
856        );
857        assert_eq!(
858            json(&req.to_json().unwrap()),
859            json(
860                r#"{"text":"Hi","voice":{"speaker":{"audio_path":"./ref.wav"},"style":{"emotion":"happy","speaking_rate":1.100000023841858,"tags":{"gender":"female"}}}}"#
861            )
862        );
863    }
864
865    #[test]
866    fn tts_voice_empty_style_omitted() {
867        // speaker/style 为空对象时直接省略(上游容忍缺失)。
868        let req = Request::tts("Hi").voice(VoiceCondition::default());
869        assert_eq!(
870            json(&req.to_json().unwrap()),
871            json(r#"{"text":"Hi","voice":{}}"#)
872        );
873    }
874
875    #[test]
876    fn tts_voice_cached_id() {
877        // 复用已缓存音色:仅传 cached_voice_id。
878        let req = Request::tts("Hi").voice(VoiceCondition::cached("spk_abc"));
879        assert_eq!(
880            json(&req.to_json().unwrap()),
881            json(r#"{"text":"Hi","voice":{"speaker":{"cached_voice_id":"spk_abc"}}}"#)
882        );
883    }
884
885    #[test]
886    fn embedded_audio_buffer() {
887        let buf = WavAudio {
888            sample_rate: 16000,
889            channels: 1,
890            samples: vec![0.0, 0.5, -0.5],
891        };
892        let req = Request::vad(AudioInput::Buffer(buf));
893        assert_eq!(
894            json(&req.to_json().unwrap()),
895            json(r#"{"audio":{"sample_rate":16000,"channels":1,"samples":[0.0,0.5,-0.5]}}"#)
896        );
897    }
898
899    #[test]
900    fn raw_json_pass_through() {
901        let s = r#"{"text":"hi","options":{"a":1}}"#;
902        assert_eq!(Request::json(s).to_json().unwrap(), s);
903        assert_eq!(
904            <&str as IntoRequest>::into_request(s)
905                .unwrap()
906                .to_json()
907                .unwrap(),
908            s
909        );
910    }
911
912    #[test]
913    fn empty_request() {
914        assert_eq!(
915            IntoRequest::into_request(()).unwrap().to_json().unwrap(),
916            "{}"
917        );
918    }
919}