修复 External SFX 轮询契约
新增具名 SFX compact 结果联合与稳定引用约束 补强轮询 OpenAPI 以及音效 metadata 结果测试
This commit is contained in:
@@ -4818,6 +4818,152 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ExternalEditorGenerationCompactResourceReference": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"resourceId"
|
||||
],
|
||||
"properties": {
|
||||
"resourceId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"projectId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"objectKey": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"assetObjectId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"sourceResourceId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"description": "完成态 compact result 中的稳定画布资源引用;不是完整 resource 快照。"
|
||||
},
|
||||
"ExternalEditorGenerationCompactAssetReference": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"assetId"
|
||||
],
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"folderId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"objectKey": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"assetObjectId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"description": "完成态 compact result 中的稳定素材库引用;不是完整 asset 快照。"
|
||||
},
|
||||
"ExternalEditorSoundEffectCompactResult": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"audioKind"
|
||||
],
|
||||
"properties": {
|
||||
"audioKind": {
|
||||
"type": "string",
|
||||
"const": "sound-effect"
|
||||
},
|
||||
"durationSeconds": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0,
|
||||
"maximum": 600,
|
||||
"description": "SFX V2 保存 MP3 后探测得到的实际时长;不是请求时长。历史完成结果可缺少此字段。"
|
||||
},
|
||||
"loop": {
|
||||
"type": "boolean",
|
||||
"description": "SFX V2 冻结并发送给 provider 的 Loop;历史完成结果可缺少此字段。"
|
||||
},
|
||||
"taskId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"objectKey": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "持久化音频对象的稳定引用;需要临时下载或预览 URL 时使用 assets/read-url。"
|
||||
},
|
||||
"assetObjectId": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"resource": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ExternalEditorGenerationCompactResourceReference"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"asset": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ExternalEditorGenerationCompactAssetReference"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"description": "External SFX 完成态的 compact result。仅包含稳定产物引用和可安全消费的 SFX 元数据;不包含完整 project/canvas/asset 快照或脱敏的 Prompt、模型与 provider 字段。"
|
||||
},
|
||||
"ExternalEditorGenerationGenericCompactResult": {
|
||||
"type": "object",
|
||||
"not": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"audioKind"
|
||||
],
|
||||
"properties": {
|
||||
"audioKind": {
|
||||
"const": "sound-effect"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"description": "其它生成类型或历史结果的兼容 compact result。该 fallback 明确排除 audioKind=sound-effect,避免吞掉 SFX 专用分支。"
|
||||
},
|
||||
"ExternalEditorGenerationCompletedResult": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ExternalEditorSoundEffectCompactResult"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/ExternalEditorGenerationGenericCompactResult"
|
||||
}
|
||||
],
|
||||
"discriminator": {
|
||||
"propertyName": "audioKind",
|
||||
"mapping": {
|
||||
"sound-effect": "#/components/schemas/ExternalEditorSoundEffectCompactResult"
|
||||
}
|
||||
},
|
||||
"description": "External v1 轮询 completed 状态的命名 compact result 联合。"
|
||||
},
|
||||
"ExternalEditorGenerationSubmissionResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -4902,9 +5048,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"result": {
|
||||
"type": "object",
|
||||
"description": "completed 时返回的 compact 稳定结果引用;不包含完整 project/canvas、Data URL、Blob URL 或临时签名 URL。",
|
||||
"additionalProperties": true
|
||||
"$ref": "#/components/schemas/ExternalEditorGenerationCompletedResult",
|
||||
"description": "仅在 completed 时返回的 compact 稳定结果引用;queued、running 和 failed 不返回该字段。为兼容历史完成结果,此字段不作为无版本迁移的必填约束。"
|
||||
},
|
||||
"pollAfterMs": {
|
||||
"type": "integer",
|
||||
|
||||
@@ -2157,6 +2157,79 @@ mod tests {
|
||||
audio_response["properties"]["loop"]["type"],
|
||||
json!(["boolean", "null"])
|
||||
);
|
||||
let generation_job =
|
||||
&parsed["components"]["schemas"]["ExternalEditorGenerationJobResponse"];
|
||||
assert_eq!(
|
||||
generation_job["properties"]["result"]["$ref"],
|
||||
"#/components/schemas/ExternalEditorGenerationCompletedResult"
|
||||
);
|
||||
assert!(
|
||||
generation_job["required"]
|
||||
.as_array()
|
||||
.is_some_and(|required| !required.contains(&json!("result"))),
|
||||
"历史 completed 轮询结果可能没有 result,不能在无版本迁移时改为必填"
|
||||
);
|
||||
let completed_result =
|
||||
&parsed["components"]["schemas"]["ExternalEditorGenerationCompletedResult"];
|
||||
assert_eq!(
|
||||
completed_result["oneOf"][0]["$ref"],
|
||||
"#/components/schemas/ExternalEditorSoundEffectCompactResult"
|
||||
);
|
||||
assert_eq!(
|
||||
completed_result["oneOf"][1]["$ref"],
|
||||
"#/components/schemas/ExternalEditorGenerationGenericCompactResult"
|
||||
);
|
||||
assert_eq!(
|
||||
completed_result["discriminator"]["propertyName"],
|
||||
"audioKind"
|
||||
);
|
||||
assert_eq!(
|
||||
completed_result["discriminator"]["mapping"]["sound-effect"],
|
||||
"#/components/schemas/ExternalEditorSoundEffectCompactResult"
|
||||
);
|
||||
let sound_effect_result =
|
||||
&parsed["components"]["schemas"]["ExternalEditorSoundEffectCompactResult"];
|
||||
assert_eq!(sound_effect_result["required"], json!(["audioKind"]));
|
||||
assert_eq!(
|
||||
sound_effect_result["properties"]["audioKind"]["const"],
|
||||
"sound-effect"
|
||||
);
|
||||
assert_eq!(
|
||||
sound_effect_result["properties"]["durationSeconds"]["type"],
|
||||
"number"
|
||||
);
|
||||
assert_eq!(
|
||||
sound_effect_result["properties"]["durationSeconds"]["exclusiveMinimum"],
|
||||
json!(0)
|
||||
);
|
||||
assert_eq!(
|
||||
sound_effect_result["properties"]["durationSeconds"]["maximum"],
|
||||
json!(600)
|
||||
);
|
||||
assert_eq!(sound_effect_result["properties"]["loop"]["type"], "boolean");
|
||||
for stable_reference in ["taskId", "objectKey", "assetObjectId", "resource", "asset"] {
|
||||
assert!(
|
||||
sound_effect_result["properties"]
|
||||
.get(stable_reference)
|
||||
.is_some(),
|
||||
"SFX compact result must expose {stable_reference} when it is present"
|
||||
);
|
||||
}
|
||||
for redacted_field in ["prompt", "actualPrompt", "model", "provider"] {
|
||||
assert!(
|
||||
sound_effect_result["properties"]
|
||||
.get(redacted_field)
|
||||
.is_none(),
|
||||
"SFX compact schema must not declare redacted {redacted_field}"
|
||||
);
|
||||
}
|
||||
let generic_result =
|
||||
&parsed["components"]["schemas"]["ExternalEditorGenerationGenericCompactResult"];
|
||||
assert_eq!(generic_result["not"]["required"], json!(["audioKind"]));
|
||||
assert_eq!(
|
||||
generic_result["not"]["properties"]["audioKind"]["const"],
|
||||
"sound-effect"
|
||||
);
|
||||
assert!(
|
||||
parsed["paths"]
|
||||
.get("/api/external/v1/editor/audios/background-music/generations")
|
||||
|
||||
@@ -1582,12 +1582,14 @@ fn editor_audio_bad_request(message: impl Into<String>) -> AppError {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::assets;
|
||||
|
||||
use super::super::sound_effect_worker::{PersistedSoundEffectAudio, SoundEffectWritebackInput};
|
||||
use super::{
|
||||
CanonicalEditorBackgroundMusicSubmissionPayload, NormalizedEditorBackgroundMusicRequest,
|
||||
build_editor_background_music_generate_response, build_inline_editor_audio_caller,
|
||||
NormalizedEditorSoundEffectRequest, build_editor_background_music_generate_response,
|
||||
build_inline_editor_audio_caller, build_sound_effect_job_result_payload,
|
||||
normalize_editor_background_music_request, normalize_editor_sound_effect_request,
|
||||
prepare_editor_background_music_queue_job,
|
||||
};
|
||||
@@ -1596,6 +1598,7 @@ mod tests {
|
||||
EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND,
|
||||
serialize_editor_generation_job_payload_for_test,
|
||||
},
|
||||
editor_project::{EditorGenerationCaller, EditorGenerationOperationContext},
|
||||
request_context::RequestContext,
|
||||
};
|
||||
|
||||
@@ -1703,6 +1706,63 @@ mod tests {
|
||||
assert!(body.contains("\"actualPrompt\": input.prompt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sound_effect_compact_result_uses_persisted_metadata_for_duration_and_loop() {
|
||||
let caller = EditorGenerationCaller {
|
||||
owner_user_id: "user-1".to_string(),
|
||||
audit_subject_user_id: Some("user-1".to_string()),
|
||||
audit_project_id: Some("project-1".to_string()),
|
||||
phase_reporter: None,
|
||||
operation: Some(EditorGenerationOperationContext {
|
||||
operation_kind: EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND.to_string(),
|
||||
operation_id: "operation-sfx-1".to_string(),
|
||||
operation_fingerprint: "fingerprint-sfx-1".to_string(),
|
||||
worker_id: Some("worker-sfx-1".to_string()),
|
||||
lease_token: Some("lease-sfx-1".to_string()),
|
||||
queue_result_context: None,
|
||||
}),
|
||||
};
|
||||
let normalized = NormalizedEditorSoundEffectRequest {
|
||||
prompt: "请求的音效描述".to_string(),
|
||||
model: assets::EDITOR_SOUND_EFFECT_MODEL.to_string(),
|
||||
duration: Some(1.0),
|
||||
loop_enabled: false,
|
||||
price_mud_points: 7,
|
||||
};
|
||||
let input = SoundEffectWritebackInput {
|
||||
actual_prompt: "provider prompt".to_string(),
|
||||
generation_inputs: json!({
|
||||
"soundEffect": {
|
||||
"requestedDurationSeconds": 1.0,
|
||||
"actualDurationSeconds": 7.42,
|
||||
"loop": true
|
||||
}
|
||||
}),
|
||||
persisted_audio: PersistedSoundEffectAudio {
|
||||
asset_object_id: "asset-object-sfx-1".to_string(),
|
||||
object_key: "generated/editor/sfx-1.mp3".to_string(),
|
||||
audio_src: "/generated-editor-audios/sfx-1.mp3".to_string(),
|
||||
asset_object: None,
|
||||
binding: None,
|
||||
},
|
||||
};
|
||||
|
||||
let payload: Value = serde_json::from_str(
|
||||
build_sound_effect_job_result_payload(&caller, &normalized, &input, None, None)
|
||||
.expect("queued SFX worker result should be serialized")
|
||||
.as_str(),
|
||||
)
|
||||
.expect("queued SFX worker result should be JSON");
|
||||
|
||||
assert_eq!(payload["result"]["durationSeconds"], 7.42);
|
||||
assert_eq!(payload["result"]["loop"], true);
|
||||
assert_ne!(
|
||||
payload["result"]["durationSeconds"],
|
||||
json!(normalized.duration)
|
||||
);
|
||||
assert_ne!(payload["result"]["loop"], json!(normalized.loop_enabled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_audio_targets_preflight_before_provider_or_queue() {
|
||||
let source = include_str!("generation.rs");
|
||||
|
||||
Reference in New Issue
Block a user