use serde_json::{Map, Value, json}; use crate::{ AudioError, BackgroundMusicTaskRequest, SUNO_DEFAULT_MODEL, SUNO_PROMPT_MAX_CHARS, SUNO_TAGS_MAX_CHARS, SUNO_TITLE_MAX_CHARS, SoundEffectTaskRequest, VIDU_AUDIO_MODEL, VIDU_PROMPT_MAX_CHARS, }; pub fn build_background_music_task_body( request: BackgroundMusicTaskRequest, ) -> Result { let prompt = normalize_limited_text_allow_empty(&request.prompt, "prompt", SUNO_PROMPT_MAX_CHARS)?; let title = normalize_limited_text(&request.title, "title", SUNO_TITLE_MAX_CHARS)?; let tags = request .tags .as_deref() .map(|value| normalize_limited_text(value, "tags", SUNO_TAGS_MAX_CHARS)) .transpose()?; let model = normalize_optional_text(request.model.as_deref()) .unwrap_or_else(|| SUNO_DEFAULT_MODEL.to_string()); let mut body = Map::from_iter([ ("prompt".to_string(), Value::String(prompt)), ("mv".to_string(), Value::String(model)), ("title".to_string(), Value::String(title)), ("task".to_string(), Value::String("generate".to_string())), ( "make_instrumental".to_string(), Value::Bool(request.instrumental), ), ]); if let Some(tags) = tags { body.insert("tags".to_string(), Value::String(tags)); } Ok(Value::Object(body)) } pub fn build_sound_effect_task_body(request: SoundEffectTaskRequest) -> Result { let prompt = normalize_limited_text(&request.prompt, "prompt", VIDU_PROMPT_MAX_CHARS)?; let duration = request.duration.clamp(2, 10); let mut body = Map::from_iter([ ( "model".to_string(), Value::String(VIDU_AUDIO_MODEL.to_string()), ), ("prompt".to_string(), Value::String(prompt)), ("duration".to_string(), json!(duration)), ]); if let Some(seed) = request.seed { body.insert("seed".to_string(), json!(seed)); } Ok(Value::Object(body)) } pub fn normalize_limited_text( value: &str, field: &'static str, max_chars: usize, ) -> Result { let normalized = value.trim().to_string(); if normalized.is_empty() { return Err(AudioError::invalid_request(format!("{field} 不能为空"))); } if normalized.chars().count() > max_chars { return Err(AudioError::invalid_request(format!( "{field} 超过 {} 字符", max_chars ))); } Ok(normalized) } pub fn normalize_limited_text_allow_empty( value: &str, field: &'static str, max_chars: usize, ) -> Result { let normalized = value.trim().to_string(); if normalized.chars().count() > max_chars { return Err(AudioError::invalid_request(format!( "{field} 超过 {} 字符", max_chars ))); } Ok(normalized) } pub fn normalize_optional_text(value: Option<&str>) -> Option { value .map(str::trim) .filter(|value| !value.is_empty()) .map(ToOwned::to_owned) }