use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde_json::Value;
use crate::audio::WavAudio;
use crate::error::Error;
#[derive(Debug, Clone)]
pub enum AudioInput {
Path(String),
Buffer(WavAudio),
}
impl From<&str> for AudioInput {
fn from(s: &str) -> Self {
AudioInput::Path(s.to_owned())
}
}
impl From<String> for AudioInput {
fn from(s: String) -> Self {
AudioInput::Path(s)
}
}
impl From<&String> for AudioInput {
fn from(s: &String) -> Self {
AudioInput::Path(s.clone())
}
}
impl From<WavAudio> for AudioInput {
fn from(a: WavAudio) -> Self {
AudioInput::Buffer(a)
}
}
impl From<&Path> for AudioInput {
fn from(p: &Path) -> Self {
AudioInput::Path(p.to_string_lossy().into_owned())
}
}
impl From<PathBuf> for AudioInput {
fn from(p: PathBuf) -> Self {
AudioInput::Path(p.to_string_lossy().into_owned())
}
}
#[derive(Debug, Clone, Default)]
pub struct AudioRequest {
pub audio: Option<AudioInput>,
pub options: BTreeMap<String, Value>,
}
impl AudioRequest {
pub fn new(audio: impl Into<AudioInput>) -> Self {
Self {
audio: Some(audio.into()),
options: BTreeMap::new(),
}
}
pub fn options_only() -> Self {
Self {
audio: None,
options: BTreeMap::new(),
}
}
pub fn option<V: Into<Value>>(mut self, key: impl Into<String>, value: V) -> Self {
self.options.insert(key.into(), value.into());
self
}
pub fn options<K, V>(mut self, opts: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<Value>,
{
self.options
.extend(opts.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
}
#[derive(Debug, Clone, Default)]
pub struct VoiceCondition {
pub speaker_audio: Option<AudioInput>,
pub cached_voice_id: Option<String>,
pub language: Option<String>,
pub emotion: Option<String>,
pub speaking_rate: Option<f32>,
pub pitch_shift: Option<f32>,
pub energy_scale: Option<f32>,
pub tags: BTreeMap<String, String>,
}
impl VoiceCondition {
pub fn speaker(audio: impl Into<AudioInput>) -> Self {
Self {
speaker_audio: Some(audio.into()),
..Default::default()
}
}
pub fn cached(id: impl Into<String>) -> Self {
Self {
cached_voice_id: Some(id.into()),
..Default::default()
}
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn emotion(mut self, emotion: impl Into<String>) -> Self {
self.emotion = Some(emotion.into());
self
}
pub fn speaking_rate(mut self, rate: f32) -> Self {
self.speaking_rate = Some(rate);
self
}
pub fn pitch_shift(mut self, shift: f32) -> Self {
self.pitch_shift = Some(shift);
self
}
pub fn energy_scale(mut self, scale: f32) -> Self {
self.energy_scale = Some(scale);
self
}
pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.tags.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub struct TtsRequest {
pub text: String,
pub language: Option<String>,
pub reference_audio: Option<AudioInput>,
pub reference_text: Option<String>,
pub voice: Option<VoiceCondition>,
pub options: BTreeMap<String, Value>,
}
impl TtsRequest {
pub fn new(text: impl Into<String>) -> Self {
Self {
text: text.into(),
language: None,
reference_audio: None,
reference_text: None,
voice: None,
options: BTreeMap::new(),
}
}
pub fn language(mut self, language: impl Into<String>) -> Self {
self.language = Some(language.into());
self
}
pub fn reference(mut self, audio: impl Into<AudioInput>) -> Self {
self.reference_audio = Some(audio.into());
self
}
pub fn reference_text(mut self, text: impl Into<String>) -> Self {
self.reference_text = Some(text.into());
self
}
pub fn voice(mut self, voice: VoiceCondition) -> Self {
self.voice = Some(voice);
self
}
pub fn option<V: Into<Value>>(mut self, key: impl Into<String>, value: V) -> Self {
self.options.insert(key.into(), value.into());
self
}
pub fn options<K, V>(mut self, opts: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<Value>,
{
self.options
.extend(opts.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}
}
#[derive(Debug, Clone)]
pub enum Request {
Vad(AudioRequest),
Asr(AudioRequest),
Diar(AudioRequest),
SourceSeparation(AudioRequest),
Tts(Box<TtsRequest>),
Json(String),
}
impl Request {
pub fn vad(audio: impl Into<AudioInput>) -> Self {
Request::Vad(AudioRequest::new(audio))
}
pub fn asr(audio: impl Into<AudioInput>) -> Self {
Request::Asr(AudioRequest::new(audio))
}
pub fn stream_asr() -> Self {
Request::Asr(AudioRequest::options_only())
}
#[deprecated(
since = "0.4.0",
note = "请改用 `Request::stream_asr`(原名易被误用于 TTS 流式)"
)]
pub fn stream() -> Self {
Self::stream_asr()
}
pub fn diar(audio: impl Into<AudioInput>) -> Self {
Request::Diar(AudioRequest::new(audio))
}
pub fn source_separation(audio: impl Into<AudioInput>) -> Self {
Request::SourceSeparation(AudioRequest::new(audio))
}
pub fn tts(text: impl Into<String>) -> Self {
Request::Tts(Box::new(TtsRequest::new(text)))
}
pub fn json(s: impl Into<String>) -> Self {
Request::Json(s.into())
}
pub fn option<V: Into<Value>>(self, key: impl Into<String>, value: V) -> Self {
let key = key.into();
let value = value.into();
match self {
Request::Vad(r) => Request::Vad(r.option(key, value)),
Request::Asr(r) => Request::Asr(r.option(key, value)),
Request::Diar(r) => Request::Diar(r.option(key, value)),
Request::SourceSeparation(r) => Request::SourceSeparation(r.option(key, value)),
Request::Tts(r) => Request::Tts(Box::new(r.option(key, value))),
Request::Json(_) => self,
}
}
pub fn options<K, V>(self, opts: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<Value>,
{
let opts: Vec<(String, Value)> = opts
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
match self {
Request::Vad(r) => Request::Vad(r.options(opts)),
Request::Asr(r) => Request::Asr(r.options(opts)),
Request::Diar(r) => Request::Diar(r.options(opts)),
Request::SourceSeparation(r) => Request::SourceSeparation(r.options(opts)),
Request::Tts(r) => Request::Tts(Box::new(r.options(opts))),
Request::Json(_) => self,
}
}
pub fn reference(self, audio: impl Into<AudioInput>) -> Self {
match self {
Request::Tts(r) => Request::Tts(Box::new(r.reference(audio))),
other => other,
}
}
pub fn reference_text(self, text: impl Into<String>) -> Self {
match self {
Request::Tts(r) => Request::Tts(Box::new(r.reference_text(text))),
other => other,
}
}
pub fn voice(self, voice: VoiceCondition) -> Self {
match self {
Request::Tts(r) => Request::Tts(Box::new(r.voice(voice))),
other => other,
}
}
pub fn language(self, language: impl Into<String>) -> Self {
match self {
Request::Tts(r) => Request::Tts(Box::new(r.language(language))),
other => other,
}
}
pub fn to_json(&self) -> Result<String, Error> {
let mut obj = serde_json::Map::new();
match self {
Request::Json(s) => return Ok(s.clone()),
Request::Vad(r) | Request::Asr(r) | Request::Diar(r) | Request::SourceSeparation(r) => {
if let Some(audio) = &r.audio {
write_audio(&mut obj, audio);
}
if !r.options.is_empty() {
obj.insert(
"options".into(),
Value::Object(r.options.clone().into_iter().collect()),
);
}
}
Request::Tts(r) => {
obj.insert("text".into(), Value::String(r.text.clone()));
if let Some(lang) = &r.language {
obj.insert("language".into(), Value::String(lang.clone()));
}
if let Some(audio) = &r.reference_audio {
write_audio(&mut obj, audio);
}
let mut options = r.options.clone();
if let Some(rt) = &r.reference_text {
options.insert("reference_text".into(), Value::String(rt.clone()));
}
if !options.is_empty() {
obj.insert(
"options".into(),
Value::Object(options.into_iter().collect()),
);
}
if let Some(voice) = &r.voice {
obj.insert("voice".into(), serialize_voice(voice));
}
}
}
Ok(serde_json::to_string(&Value::Object(obj))?)
}
}
fn write_audio(obj: &mut serde_json::Map<String, Value>, input: &AudioInput) {
match input {
AudioInput::Path(p) => {
obj.insert("audio_path".into(), Value::String(p.clone()));
}
AudioInput::Buffer(buf) => {
obj.insert(
"audio".into(),
serde_json::json!({
"sample_rate": buf.sample_rate,
"channels": buf.channels,
"samples": buf.samples,
}),
);
}
}
}
fn serialize_voice(voice: &VoiceCondition) -> Value {
let mut speaker = serde_json::Map::new();
if let Some(id) = &voice.cached_voice_id {
speaker.insert("cached_voice_id".into(), Value::String(id.clone()));
}
if let Some(audio) = &voice.speaker_audio {
match audio {
AudioInput::Path(p) => {
speaker.insert("audio_path".into(), Value::String(p.clone()));
}
AudioInput::Buffer(buf) => {
speaker.insert(
"audio".into(),
serde_json::json!({
"sample_rate": buf.sample_rate,
"channels": buf.channels,
"samples": buf.samples,
}),
);
}
}
}
let mut style = serde_json::Map::new();
if let Some(v) = &voice.language {
style.insert("language".into(), Value::String(v.clone()));
}
if let Some(v) = &voice.emotion {
style.insert("emotion".into(), Value::String(v.clone()));
}
if let Some(v) = &voice.speaking_rate {
style.insert("speaking_rate".into(), serde_json::json!(v));
}
if let Some(v) = &voice.pitch_shift {
style.insert("pitch_shift".into(), serde_json::json!(v));
}
if let Some(v) = &voice.energy_scale {
style.insert("energy_scale".into(), serde_json::json!(v));
}
if !voice.tags.is_empty() {
let mut tags = serde_json::Map::new();
for (k, v) in &voice.tags {
tags.insert(k.clone(), Value::String(v.clone()));
}
style.insert("tags".into(), Value::Object(tags));
}
let mut voice_obj = serde_json::Map::new();
if !speaker.is_empty() {
voice_obj.insert("speaker".into(), Value::Object(speaker));
}
if !style.is_empty() {
voice_obj.insert("style".into(), Value::Object(style));
}
Value::Object(voice_obj)
}
pub trait IntoRequest {
fn into_request(self) -> Result<Request, Error>;
}
impl IntoRequest for Request {
fn into_request(self) -> Result<Request, Error> {
Ok(self)
}
}
impl IntoRequest for &Request {
fn into_request(self) -> Result<Request, Error> {
Ok(self.clone())
}
}
impl IntoRequest for () {
fn into_request(self) -> Result<Request, Error> {
Ok(Request::Json("{}".into()))
}
}
impl IntoRequest for &str {
fn into_request(self) -> Result<Request, Error> {
Ok(Request::Json(self.to_string()))
}
}
impl IntoRequest for String {
fn into_request(self) -> Result<Request, Error> {
Ok(Request::Json(self))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
fn json(s: &str) -> serde_json::Value {
serde_json::from_str(s).expect("合法 JSON")
}
#[test]
fn stream_start_options_only() {
let req = Request::stream_asr()
.option("language", "auto")
.option("audio_chunk_seconds", 3.0);
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"options":{"audio_chunk_seconds":3.0,"language":"auto"}}"#)
);
}
#[test]
fn stream_start_empty() {
let req = Request::stream_asr();
assert_eq!(json(&req.to_json().unwrap()), json(r#"{}"#));
}
#[test]
#[allow(deprecated)]
fn stream_deprecated_alias() {
let req = Request::stream().option("language", "auto");
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"options":{"language":"auto"}}"#)
);
}
#[test]
fn vad_offline() {
let req = Request::vad("./a.wav").option("vad_threshold", 0.5);
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"audio_path":"./a.wav","options":{"vad_threshold":0.5}}"#)
);
}
#[test]
fn asr_streaming_window() {
let req = Request::asr("./a.wav").option("audio_chunk_seconds", 3.0);
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"audio_path":"./a.wav","options":{"audio_chunk_seconds":3.0}}"#)
);
}
#[test]
fn tts_text() {
let req = Request::tts("Hello!");
assert_eq!(json(&req.to_json().unwrap()), json(r#"{"text":"Hello!"}"#));
}
#[test]
fn tts_voice_clone() {
let req = Request::tts("Hi")
.reference("./ref.wav")
.reference_text("参考文本")
.language("zh");
assert_eq!(
json(&req.to_json().unwrap()),
json(
r#"{"text":"Hi","language":"zh","audio_path":"./ref.wav","options":{"reference_text":"参考文本"}}"#
)
);
}
#[test]
fn windows_path_no_manual_escaping() {
let req = Request::asr("C:\\dir\\spe\"ch.wav");
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"audio_path":"C:\\dir\\spe\"ch.wav"}"#)
);
}
#[test]
fn diar_offline() {
let req = Request::diar("./meeting.wav").option("num_speakers", 4);
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"audio_path":"./meeting.wav","options":{"num_speakers":4}}"#)
);
}
#[test]
fn source_separation_offline() {
let req = Request::source_separation("./song.wav").options([("stem", "vocals")]);
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"audio_path":"./song.wav","options":{"stem":"vocals"}}"#)
);
}
#[test]
fn tts_voice_condition() {
let req = Request::tts("Hi").voice(
VoiceCondition::speaker("./ref.wav")
.emotion("happy")
.speaking_rate(1.1)
.tag("gender", "female"),
);
assert_eq!(
json(&req.to_json().unwrap()),
json(
r#"{"text":"Hi","voice":{"speaker":{"audio_path":"./ref.wav"},"style":{"emotion":"happy","speaking_rate":1.100000023841858,"tags":{"gender":"female"}}}}"#
)
);
}
#[test]
fn tts_voice_empty_style_omitted() {
let req = Request::tts("Hi").voice(VoiceCondition::default());
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"text":"Hi","voice":{}}"#)
);
}
#[test]
fn tts_voice_cached_id() {
let req = Request::tts("Hi").voice(VoiceCondition::cached("spk_abc"));
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"text":"Hi","voice":{"speaker":{"cached_voice_id":"spk_abc"}}}"#)
);
}
#[test]
fn embedded_audio_buffer() {
let buf = WavAudio {
sample_rate: 16000,
channels: 1,
samples: vec![0.0, 0.5, -0.5],
};
let req = Request::vad(AudioInput::Buffer(buf));
assert_eq!(
json(&req.to_json().unwrap()),
json(r#"{"audio":{"sample_rate":16000,"channels":1,"samples":[0.0,0.5,-0.5]}}"#)
);
}
#[test]
fn raw_json_pass_through() {
let s = r#"{"text":"hi","options":{"a":1}}"#;
assert_eq!(Request::json(s).to_json().unwrap(), s);
assert_eq!(
<&str as IntoRequest>::into_request(s)
.unwrap()
.to_json()
.unwrap(),
s
);
}
#[test]
fn empty_request() {
assert_eq!(
IntoRequest::into_request(()).unwrap().to_json().unwrap(),
"{}"
);
}
}