3b69b48860
前端接入规范化提示词、同步排他锁及账号与项目分层回调门禁 关闭BGM正式POST自动重试并补充真实传输层单次请求回归 后端统一站内BGM规范载荷并保留External v1原始载荷幂等语义 补齐队列、直返、平台音频及生命周期边界测试 新增并更新T5独立实施记录
141 lines
5.1 KiB
Rust
141 lines
5.1 KiB
Rust
use serde_json::{Map, Value, json};
|
|
|
|
use crate::{
|
|
AudioError, BackgroundMusicTaskRequest, EditorBackgroundMusicTaskRequest,
|
|
EditorSoundEffectTaskRequest, 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<Value, AudioError> {
|
|
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<Value, AudioError> {
|
|
let prompt = normalize_limited_text(&request.prompt, "prompt", VIDU_PROMPT_MAX_CHARS)?;
|
|
if !(2..=10).contains(&request.duration) {
|
|
return Err(AudioError::invalid_request("duration 必须在 2-10 秒之间"));
|
|
}
|
|
let mut body = Map::from_iter([
|
|
(
|
|
"model".to_string(),
|
|
Value::String(VIDU_AUDIO_MODEL.to_string()),
|
|
),
|
|
("prompt".to_string(), Value::String(prompt.clone())),
|
|
// 中文注释:VectorEngine Apifox 当前仍写 prompt,但线上 Vidu 网关曾返回
|
|
// missing field sound;同时发送 sound 作为同义字段,兼容网关实际反序列化。
|
|
("sound".to_string(), Value::String(prompt)),
|
|
("duration".to_string(), json!(request.duration)),
|
|
]);
|
|
if let Some(seed) = request.seed {
|
|
body.insert("seed".to_string(), json!(seed));
|
|
}
|
|
Ok(Value::Object(body))
|
|
}
|
|
|
|
pub fn build_editor_background_music_task_body(
|
|
request: EditorBackgroundMusicTaskRequest,
|
|
) -> Result<Value, AudioError> {
|
|
let prompt =
|
|
crate::validate_background_music_generation_prompt(&request.gpt_description_prompt)?;
|
|
let model = normalize_optional_text(request.model.as_deref())
|
|
.unwrap_or_else(|| SUNO_DEFAULT_MODEL.to_string());
|
|
Ok(json!({
|
|
"mv": model,
|
|
"gpt_description_prompt": prompt.prompt,
|
|
"make_instrumental": request.make_instrumental,
|
|
}))
|
|
}
|
|
|
|
pub fn build_editor_sound_effect_task_body(
|
|
request: EditorSoundEffectTaskRequest,
|
|
) -> Result<Value, AudioError> {
|
|
let prompt = normalize_limited_text(&request.prompt, "prompt", VIDU_PROMPT_MAX_CHARS)?;
|
|
if !(2..=10).contains(&request.duration) {
|
|
return Err(AudioError::invalid_request("duration 必须在 2-10 秒之间"));
|
|
}
|
|
let model = normalize_optional_text(request.model.as_deref())
|
|
.unwrap_or_else(|| VIDU_AUDIO_MODEL.to_string());
|
|
if model != VIDU_AUDIO_MODEL {
|
|
return Err(AudioError::invalid_request(
|
|
"编辑器音效暂只支持 Vidu audio1.0 模型",
|
|
));
|
|
}
|
|
let mut body = Map::from_iter([
|
|
("model".to_string(), Value::String(model)),
|
|
("prompt".to_string(), Value::String(prompt.clone())),
|
|
// 中文注释:同上,编辑器音效也必须带 sound,避免上游按 sound 字段解析时报缺字段。
|
|
("sound".to_string(), Value::String(prompt)),
|
|
("duration".to_string(), json!(request.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<String, AudioError> {
|
|
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<String, AudioError> {
|
|
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<String> {
|
|
value
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
}
|