1use std::collections::BTreeMap;
26use std::path::{Path, PathBuf};
27
28use serde_json::Value;
29
30use crate::audio::WavAudio;
31use crate::error::Error;
32
33#[derive(Debug, Clone)]
35pub enum AudioInput {
36 Path(String),
38 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#[derive(Debug, Clone, Default)]
80pub struct AudioRequest {
81 pub audio: Option<AudioInput>,
83 pub options: BTreeMap<String, Value>,
86}
87
88impl AudioRequest {
89 pub fn new(audio: impl Into<AudioInput>) -> Self {
91 Self {
92 audio: Some(audio.into()),
93 options: BTreeMap::new(),
94 }
95 }
96
97 pub fn options_only() -> Self {
103 Self {
104 audio: None,
105 options: BTreeMap::new(),
106 }
107 }
108
109 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 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#[derive(Debug, Clone, Default)]
133pub struct VoiceCondition {
134 pub speaker_audio: Option<AudioInput>,
136 pub cached_voice_id: Option<String>,
138 pub language: Option<String>,
140 pub emotion: Option<String>,
142 pub speaking_rate: Option<f32>,
144 pub pitch_shift: Option<f32>,
146 pub energy_scale: Option<f32>,
148 pub tags: BTreeMap<String, String>,
150}
151
152impl VoiceCondition {
153 pub fn speaker(audio: impl Into<AudioInput>) -> Self {
155 Self {
156 speaker_audio: Some(audio.into()),
157 ..Default::default()
158 }
159 }
160
161 pub fn cached(id: impl Into<String>) -> Self {
163 Self {
164 cached_voice_id: Some(id.into()),
165 ..Default::default()
166 }
167 }
168
169 pub fn language(mut self, language: impl Into<String>) -> Self {
171 self.language = Some(language.into());
172 self
173 }
174
175 pub fn emotion(mut self, emotion: impl Into<String>) -> Self {
177 self.emotion = Some(emotion.into());
178 self
179 }
180
181 pub fn speaking_rate(mut self, rate: f32) -> Self {
183 self.speaking_rate = Some(rate);
184 self
185 }
186
187 pub fn pitch_shift(mut self, shift: f32) -> Self {
189 self.pitch_shift = Some(shift);
190 self
191 }
192
193 pub fn energy_scale(mut self, scale: f32) -> Self {
195 self.energy_scale = Some(scale);
196 self
197 }
198
199 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#[derive(Debug, Clone)]
208pub struct TtsRequest {
209 pub text: String,
211 pub language: Option<String>,
213 pub reference_audio: Option<AudioInput>,
215 pub reference_text: Option<String>,
217 pub voice: Option<VoiceCondition>,
219 pub options: BTreeMap<String, Value>,
221}
222
223impl TtsRequest {
224 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 pub fn language(mut self, language: impl Into<String>) -> Self {
238 self.language = Some(language.into());
239 self
240 }
241
242 pub fn reference(mut self, audio: impl Into<AudioInput>) -> Self {
244 self.reference_audio = Some(audio.into());
245 self
246 }
247
248 pub fn reference_text(mut self, text: impl Into<String>) -> Self {
250 self.reference_text = Some(text.into());
251 self
252 }
253
254 pub fn voice(mut self, voice: VoiceCondition) -> Self {
258 self.voice = Some(voice);
259 self
260 }
261
262 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 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#[derive(Debug, Clone)]
287pub struct AlignRequest {
288 pub audio: Option<AudioInput>,
290 pub text: String,
292 pub language: String,
294 pub options: BTreeMap<String, Value>,
296}
297
298impl AlignRequest {
299 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 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 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#[derive(Debug, Clone)]
354pub enum Request {
355 Vad(AudioRequest),
357 Asr(AudioRequest),
359 Diar(AudioRequest),
361 SourceSeparation(AudioRequest),
363 Tts(Box<TtsRequest>),
367 Align(Box<AlignRequest>),
371 Json(String),
373}
374
375impl Request {
376 pub fn vad(audio: impl Into<AudioInput>) -> Self {
378 Request::Vad(AudioRequest::new(audio))
379 }
380
381 pub fn asr(audio: impl Into<AudioInput>) -> Self {
383 Request::Asr(AudioRequest::new(audio))
384 }
385
386 pub fn stream_asr() -> Self {
397 Request::Asr(AudioRequest::options_only())
398 }
399
400 #[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 pub fn diar(audio: impl Into<AudioInput>) -> Self {
411 Request::Diar(AudioRequest::new(audio))
412 }
413
414 pub fn source_separation(audio: impl Into<AudioInput>) -> Self {
416 Request::SourceSeparation(AudioRequest::new(audio))
417 }
418
419 pub fn tts(text: impl Into<String>) -> Self {
421 Request::Tts(Box::new(TtsRequest::new(text)))
422 }
423
424 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 pub fn json(s: impl Into<String>) -> Self {
456 Request::Json(s.into())
457 }
458
459 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 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 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 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 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 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 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 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
591fn 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
610fn 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 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
669pub trait IntoRequest {
676 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 #![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 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 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 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 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 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 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 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}