合并主分支并同步角色动作资产修复

同步角色动作前端展示、下载、后端数据结构与精选链路修复
保留画布 Agent 图片编辑素材类型约束
整合前后端手动换签组合校验,避免非法类型调用错误接口
This commit is contained in:
2026-08-06 17:01:46 +08:00
111 changed files with 10825 additions and 1262 deletions
+119 -12
View File
@@ -33,13 +33,14 @@ use shared_contracts::admin::{
AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload, AdminDebugHeaderInput,
AdminDebugHttpRequest, AdminDebugHttpResponse, AdminDirectUploadTicketPayload,
AdminEditorAssetListQuery, AdminEditorAssetListResponse, AdminEditorAssetPayload,
AdminEditorShowcaseAssetPayload, AdminEditorShowcaseAssetResponse,
AdminEditorShowcaseCampaignPayload, AdminEditorShowcaseCampaignResponse,
AdminEditorShowcaseDisplayRequest, AdminEditorShowcaseListQuery,
AdminEditorShowcaseListResponse, AdminEditorShowcaseReviewRequest,
AdminFeatureGateConfigPayload, AdminFeatureGateConfigResponse, AdminLoginRequest,
AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, AdminServiceOverviewPayload,
AdminSessionPayload, AdminTrackingEventEntryPayload, AdminTrackingEventKeyListResponse,
AdminEditorImageSequenceFramePayload, AdminEditorShowcaseAssetPayload,
AdminEditorShowcaseAssetResponse, AdminEditorShowcaseCampaignPayload,
AdminEditorShowcaseCampaignResponse, AdminEditorShowcaseDisplayRequest,
AdminEditorShowcaseListQuery, AdminEditorShowcaseListResponse,
AdminEditorShowcaseReviewRequest, AdminFeatureGateConfigPayload,
AdminFeatureGateConfigResponse, AdminLoginRequest, AdminLoginResponse, AdminMeResponse,
AdminOverviewResponse, AdminServiceOverviewPayload, AdminSessionPayload,
AdminTrackingEventEntryPayload, AdminTrackingEventKeyListResponse,
AdminTrackingEventKeyPayload, AdminTrackingEventListQuery, AdminTrackingEventListResponse,
AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertFeatureGateConfigRequest,
};
@@ -77,7 +78,10 @@ use crate::{
create_direct_upload_ticket_for_owner, get_asset_read_url_with_query,
},
editor_generation_config::EditorGenerationPricingConfig,
editor_project::current_utc_micros,
editor_project::{
current_utc_micros, normalize_editor_image_sequence_frames_value,
resolve_editor_asset_kind, sanitize_editor_generation_inputs,
},
http_error::AppError,
request_context::RequestContext,
state::{AdminRuntime, AppState},
@@ -1051,11 +1055,27 @@ fn map_admin_feature_gate_config(
}
}
fn normalize_admin_editor_image_sequence_frames(
frames: &Value,
) -> Option<Vec<AdminEditorImageSequenceFramePayload>> {
let normalized = normalize_editor_image_sequence_frames_value(frames)?;
serde_json::from_value(normalized).ok()
}
fn admin_editor_showcase_asset_payload_from_record(
state: &AppState,
record: EditorShowcaseAssetRecord,
) -> AdminEditorShowcaseAssetPayload {
let author = resolve_work_author_by_user_id(state, &record.owner_user_id, None, None);
let formal_image_sequence_frames = record
.image_sequence_frames
.as_ref()
.and_then(normalize_admin_editor_image_sequence_frames);
let asset_kind = resolve_editor_asset_kind(
record.asset_kind,
record.image_src.as_str(),
record.object_key.as_deref(),
);
AdminEditorShowcaseAssetPayload {
showcase_id: record.showcase_id,
asset_id: record.asset_id,
@@ -1072,8 +1092,8 @@ fn admin_editor_showcase_asset_payload_from_record(
model: record.model,
provider: record.provider,
task_id: record.task_id,
asset_kind: record.asset_kind,
generation_inputs: record.generation_inputs,
asset_kind,
generation_inputs: sanitize_editor_generation_inputs(record.generation_inputs),
thumbnail_src: record.thumbnail_src,
generation_cost_mud_points: record.generation_cost_mud_points,
refund_mud_points: record.refund_mud_points,
@@ -1091,6 +1111,8 @@ fn admin_editor_showcase_asset_payload_from_record(
rejected_at: record.rejected_at,
updated_at: record.updated_at,
showcase_category: record.showcase_category,
image_sequence_frames: formal_image_sequence_frames,
image_sequence_duration_ms: record.image_sequence_duration_ms,
}
}
@@ -1193,6 +1215,15 @@ fn admin_editor_asset_payload_from_record(
) -> AdminEditorAssetPayload {
let author = resolve_work_author_by_user_id(state, &record.owner_user_id, None, None);
let generator = resolve_admin_editor_asset_generator(&record);
let formal_image_sequence_frames = record
.image_sequence_frames
.as_ref()
.and_then(normalize_admin_editor_image_sequence_frames);
let asset_kind = resolve_editor_asset_kind(
record.asset_kind,
record.image_src.as_str(),
record.object_key.as_deref(),
);
AdminEditorAssetPayload {
asset_id: record.asset_id,
owner_user_id: record.owner_user_id,
@@ -1212,8 +1243,8 @@ fn admin_editor_asset_payload_from_record(
provider: record.provider,
task_id: record.task_id,
group_task_id: record.group_task_id,
asset_kind: record.asset_kind,
generation_inputs: record.generation_inputs,
asset_kind,
generation_inputs: sanitize_editor_generation_inputs(record.generation_inputs),
source_resource_id: record.source_resource_id,
thumbnail_src: record.thumbnail_src,
generation_cost_mud_points: record.generation_cost_mud_points,
@@ -1223,6 +1254,8 @@ fn admin_editor_asset_payload_from_record(
task_cost_mud_points: record.generation_cost_mud_points,
generator,
children: Vec::new(),
image_sequence_frames: formal_image_sequence_frames,
image_sequence_duration_ms: record.image_sequence_duration_ms,
}
}
@@ -4118,6 +4151,8 @@ mod tests {
rejected_at: None,
updated_at: "2026-07-04T10:00:00.000Z".to_string(),
showcase_category: None,
image_sequence_frames: None,
image_sequence_duration_ms: None,
}
}
@@ -4152,6 +4187,8 @@ mod tests {
generation_cost_mud_points,
created_at: "2026-07-04T10:00:00Z".to_string(),
updated_at: "2026-07-04T10:00:00Z".to_string(),
image_sequence_frames: None,
image_sequence_duration_ms: None,
}
}
@@ -4316,6 +4353,76 @@ mod tests {
);
}
#[test]
fn admin_editor_showcase_payload_uses_formal_action_fields_only() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let formal_frames = json!([
{
"imageSrc": "/generated/action/formal-01.png",
"objectKey": "generated/action/formal-01.png",
"assetObjectId": "asset-object-formal-01",
"width": 192,
"height": 256
},
{
"imageSrc": "/generated/action/formal-02.png",
"objectKey": "generated/action/formal-02.png",
"assetObjectId": "asset-object-formal-02",
"width": 192,
"height": 256
}
]);
let mut record = test_admin_editor_showcase_asset_record("user-author-1");
record.asset_kind = Some("character-animation".to_string());
record.image_sequence_frames = Some(formal_frames.clone());
record.image_sequence_duration_ms = Some(6_000);
record.generation_inputs = Some(json!({
"fields": [{ "title": "动作", "value": "挥手" }]
}));
let payload = admin_editor_showcase_asset_payload_from_record(&state, record);
assert_eq!(payload.asset_kind.as_deref(), Some("character-animation"));
assert_eq!(
serde_json::to_value(payload.image_sequence_frames)
.expect("formal frames should serialize"),
formal_frames
);
assert_eq!(payload.image_sequence_duration_ms, Some(6_000));
assert_eq!(
payload.generation_inputs,
Some(json!({ "fields": [{ "title": "动作", "value": "挥手" }] }))
);
}
#[test]
fn admin_editor_asset_payload_preserves_formal_character_action_metadata() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let frames = json!([
{ "imageSrc": "/generated/action/frame-01.png", "objectKey": "generated/action/frame-01.png", "assetObjectId": "asset-object-frame-01", "width": 192, "height": 256 },
{ "imageSrc": "/generated/action/frame-02.png", "objectKey": "generated/action/frame-02.png", "assetObjectId": "asset-object-frame-02", "width": 192, "height": 256 }
]);
let mut record = test_admin_editor_asset_record(
"action-asset",
"action-task",
"角色动作",
"generated/action/frame-01.png",
12,
);
record.asset_kind = Some("character-animation".to_string());
record.image_sequence_frames = Some(frames.clone());
record.image_sequence_duration_ms = Some(5_000);
let payload = admin_editor_asset_payload_from_record(&state, record);
assert_eq!(
serde_json::to_value(payload.image_sequence_frames)
.expect("formal frames should serialize"),
frames
);
assert_eq!(payload.image_sequence_duration_ms, Some(5_000));
}
#[test]
fn admin_editor_asset_group_nests_each_intermediate_output_under_final_asset() {
let final_output = test_admin_editor_asset_record(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -42,7 +42,7 @@ use crate::{
enqueue_editor_ui_design_asset_extraction_for_owner, map_editor_project_error,
normalize_editor_persisted_media_src, normalize_optional_string,
parse_editor_generation_json_payload, sanitize_editor_untrusted_generation_inputs,
save_editor_project_layout_with_revision_and_get, serialize_editor_asset_metadata,
save_editor_project_layout_with_revision_and_get, serialize_editor_generation_inputs,
},
external_api_auth::ExternalApiPrincipal,
external_generation::map_external_generation_job_status_detail,
@@ -54,6 +54,7 @@ use crate::{
enqueue_editor_sound_effect_generation_for_owner,
},
};
use std::num::NonZeroU64;
const EXTERNAL_EDITOR_PROVIDER: &str = "external-editor-api";
const SCOPE_EDITOR_PROJECT: &str = "editor:project";
@@ -117,6 +118,8 @@ pub struct ExternalEditorAssetCreateRequest {
task_id: Option<String>,
asset_kind: Option<String>,
generation_inputs: Option<Value>,
image_sequence_frames: Option<Value>,
image_sequence_duration_ms: Option<NonZeroU64>,
}
#[derive(Debug, Deserialize)]
@@ -136,6 +139,8 @@ pub struct ExternalEditorProjectResourceCreateRequest {
source_resource_id: Option<String>,
asset_kind: Option<String>,
generation_inputs: Option<Value>,
image_sequence_frames: Option<Value>,
image_sequence_duration_ms: Option<NonZeroU64>,
}
#[derive(Debug, Deserialize)]
@@ -479,8 +484,12 @@ pub async fn create_external_editor_asset(
Json(payload): Json<ExternalEditorAssetCreateRequest>,
) -> Result<Json<Value>, AppError> {
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let generation_inputs_json =
serialize_external_editor_generation_inputs(payload.generation_inputs.clone())?;
let generation_inputs_json = serialize_external_editor_generation_inputs(
payload.asset_kind.as_deref(),
payload.generation_inputs.clone(),
)?;
let image_sequence_frames_json =
serialize_external_editor_image_sequence_frames(payload.image_sequence_frames)?;
let object_key = normalize_optional_string(payload.object_key);
let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?;
let asset = state
@@ -509,6 +518,8 @@ pub async fn create_external_editor_asset(
thumbnail_src: None,
group_task_id: None,
group_task_expected_asset_count: None,
image_sequence_frames_json,
image_sequence_duration_ms: payload.image_sequence_duration_ms.map(NonZeroU64::get),
})
.await
.map_err(map_editor_project_error)?;
@@ -581,8 +592,12 @@ pub async fn create_external_editor_project_resource(
Json(payload): Json<ExternalEditorProjectResourceCreateRequest>,
) -> Result<Json<Value>, AppError> {
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let generation_inputs_json =
serialize_external_editor_generation_inputs(payload.generation_inputs.clone())?;
let generation_inputs_json = serialize_external_editor_generation_inputs(
payload.asset_kind.as_deref(),
payload.generation_inputs.clone(),
)?;
let image_sequence_frames_json =
serialize_external_editor_image_sequence_frames(payload.image_sequence_frames)?;
let object_key = normalize_optional_string(payload.object_key);
let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?;
let resource = state
@@ -606,6 +621,8 @@ pub async fn create_external_editor_project_resource(
asset_kind: normalize_optional_string(payload.asset_kind),
generation_inputs_json,
updated_at_micros: current_utc_micros(),
image_sequence_frames_json,
image_sequence_duration_ms: payload.image_sequence_duration_ms.map(NonZeroU64::get),
})
.await
.map_err(map_editor_project_error)?;
@@ -989,17 +1006,30 @@ fn normalize_project_title(title: Option<String>) -> String {
}
fn serialize_external_editor_generation_inputs(
asset_kind: Option<&str>,
generation_inputs: Option<Value>,
) -> Result<Option<String>, AppError> {
serialize_editor_asset_metadata(sanitize_editor_untrusted_generation_inputs(
generation_inputs,
))
serialize_editor_generation_inputs(
asset_kind,
sanitize_editor_untrusted_generation_inputs(generation_inputs),
)
}
fn serialize_external_editor_image_sequence_frames(
frames: Option<Value>,
) -> Result<Option<String>, AppError> {
crate::editor_project::serialize_editor_image_sequence_frames(frames)
}
#[cfg(test)]
mod tests {
use super::*;
const EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS: [&str; 2] = [
"ExternalEditorAssetCreateRequest",
"ExternalEditorProjectResourceCreateRequest",
];
fn external_generation_job_fixture(status: &str) -> ExternalGenerationJobRecord {
ExternalGenerationJobRecord {
job_id: "task-external-test".to_string(),
@@ -1060,32 +1090,124 @@ mod tests {
}
#[test]
fn external_editor_generation_inputs_cannot_forge_internal_audit_fields() {
let serialized = serialize_external_editor_generation_inputs(Some(json!({
"fields": [{"label": "角色设定", "value": "骑士"}],
"screenColorHex": "#00FF00",
"mattingProvider": "forged-provider",
"mattingModel": "forged-model",
"references": [{
"title": "伪造引用",
"label": "其他用户素材",
"refType": "asset",
"refId": "asset-forged"
}],
"characterAnimation": {"durationSeconds": 4}
})))
fn external_editor_generation_inputs_sanitize_audit_fields_for_regular_assets() {
let serialized = serialize_external_editor_generation_inputs(
Some("image"),
Some(json!({
"fields": [{"label": "角色设定", "value": "骑士"}],
"screenColorHex": "#00FF00",
"mattingProvider": "forged-provider",
"mattingModel": "forged-model",
"references": [{
"title": "伪造引用",
"label": "其他用户素材",
"refType": "asset",
"refId": "asset-forged"
}],
"extension": {"durationSeconds": 4}
})),
)
.expect("外部编辑器生成输入应可序列化")
.expect("非空生成输入应保留");
let parsed: Value = serde_json::from_str(&serialized).expect("生成输入应为合法 JSON");
assert_eq!(parsed["fields"][0]["value"], "骑士");
assert_eq!(parsed["characterAnimation"]["durationSeconds"], 4);
assert_eq!(parsed["extension"]["durationSeconds"], 4);
assert!(parsed.get("screenColorHex").is_none());
assert!(parsed.get("mattingProvider").is_none());
assert!(parsed.get("mattingModel").is_none());
assert!(parsed.get("references").is_none());
}
#[test]
fn external_editor_character_animation_generation_inputs_reject_legacy_runtime_fields() {
for (field, value) in [
("characterAnimation", json!({ "durationSeconds": 4 })),
("frames", json!([])),
("previewVideoPath", json!("/generated/action/preview.mp4")),
("frameCount", json!(32)),
("fps", json!(8)),
("durationSeconds", json!(4)),
] {
let mut generation_inputs = serde_json::Map::new();
generation_inputs.insert("fields".to_string(), json!([]));
generation_inputs.insert(field.to_string(), value);
let error = serialize_external_editor_generation_inputs(
Some("character-animation"),
Some(Value::Object(generation_inputs)),
)
.expect_err("External v1 不应再接受角色动作旧运行字段");
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(
error
.details()
.and_then(|details| details["fields"][0].as_str()),
Some(field)
);
assert!(error.body_text().contains("imageSequenceFrames"));
assert!(error.body_text().contains("imageSequenceDurationMs"));
}
}
#[test]
fn external_editor_sequence_frames_reject_temporary_signed_urls_without_stable_references() {
let error = serialize_external_editor_image_sequence_frames(Some(json!([
{
"imageSrc": "https://example.invalid/frame-01.png?Expires=60&Signature=temporary-1",
"width": 192,
"height": 256
},
{
"imageSrc": "https://example.invalid/frame-02.png?Expires=60&Signature=temporary-2",
"width": 192,
"height": 256
}
])))
.expect_err("External v1 不能把仅含临时签名 URL 的帧持久化为正式动作");
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(
error.details().and_then(|details| details.get("fields")),
Some(&json!(["objectKey", "assetObjectId"]))
);
assert!(error.body_text().contains("临时签名 URL"));
}
#[test]
fn external_editor_sequence_frames_derive_persistent_paths_from_object_keys() {
let serialized = serialize_external_editor_image_sequence_frames(Some(json!([
{
"imageSrc": "https://example.invalid/frame-01.png?Expires=60&Signature=temporary-1",
"objectKey": "/generated/action/frame-01.png",
"assetObjectId": "asset-object-frame-01",
"width": 192,
"height": 256
},
{
"imageSrc": "https://example.invalid/frame-02.png?Expires=60&Signature=temporary-2",
"objectKey": "generated/action/frame-02.png",
"assetObjectId": "asset-object-frame-02",
"width": 192,
"height": 256
}
])))
.expect("带稳定对象引用的 External v1 正式帧应可持久化")
.expect("正式帧序列应产生 JSON");
let frames: Value = serde_json::from_str(&serialized).expect("正式帧序列应为合法 JSON");
assert_eq!(frames[0]["imageSrc"], "/generated/action/frame-01.png");
assert_eq!(frames[1]["imageSrc"], "/generated/action/frame-02.png");
assert!(frames.as_array().is_some_and(|frames| {
frames.iter().all(|frame| {
!frame["imageSrc"]
.as_str()
.is_some_and(|image_src| image_src.contains("Signature="))
})
}));
}
#[test]
fn external_generation_requires_bounded_printable_idempotency_key() {
let missing = HeaderMap::new();
@@ -1197,11 +1319,111 @@ mod tests {
assert!(!unavailable.body_text().contains("ConnectDropped"));
}
#[test]
fn external_openapi_character_animation_create_requires_complete_sequence_metadata() {
let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse");
for request_schema_name in EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS {
let request_schema = &parsed["components"]["schemas"][request_schema_name];
let conditional = &request_schema["allOf"][0];
assert_eq!(
conditional["then"]["required"],
json!(["imageSequenceFrames", "imageSequenceDurationMs"]),
"{request_schema_name}"
);
assert_eq!(
request_schema["properties"]["imageSequenceFrames"]["minItems"],
json!(2),
"{request_schema_name}"
);
let frame_schema = &parsed["components"]["schemas"]["EditorImageSequenceFrame"];
assert_eq!(
frame_schema["required"],
json!(["imageSrc", "objectKey", "assetObjectId", "width", "height"]),
"{request_schema_name}"
);
for stable_field in ["objectKey", "assetObjectId"] {
assert_eq!(
frame_schema["properties"][stable_field]["type"],
json!("string"),
"{request_schema_name}.{stable_field}"
);
assert_eq!(
frame_schema["properties"][stable_field]["minLength"],
json!(1),
"{request_schema_name}.{stable_field}"
);
}
assert_eq!(
request_schema["properties"]["imageSequenceDurationMs"]["minimum"],
json!(1),
"{request_schema_name}"
);
assert_eq!(
conditional["then"]["properties"]["generationInputs"]["$ref"],
"#/components/schemas/ExternalCharacterAnimationGenerationInputs",
"{request_schema_name}"
);
}
}
#[test]
fn external_openapi_non_animation_create_forbids_sequence_metadata() {
let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse");
for request_schema_name in EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS {
let request_schema = &parsed["components"]["schemas"][request_schema_name];
let conditional = &request_schema["allOf"][0];
assert_eq!(
conditional["if"]["required"],
json!(["assetKind"]),
"{request_schema_name}"
);
let forbidden_fields = conditional["else"]["not"]["anyOf"]
.as_array()
.expect("非角色动作分支必须逐项禁止序列字段")
.iter()
.filter_map(|schema| schema["required"][0].as_str())
.collect::<Vec<_>>();
assert_eq!(
forbidden_fields,
["imageSequenceFrames", "imageSequenceDurationMs"],
"{request_schema_name}"
);
}
}
#[test]
fn exported_openapi_json_contains_external_editor_routes_and_security() {
let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse");
assert_eq!(parsed["openapi"], "3.1.0");
let legacy_runtime_fields = [
"characterAnimation",
"frames",
"previewVideoPath",
"frameCount",
"fps",
"durationSeconds",
];
let forbidden_generation_inputs = &parsed["components"]["schemas"]["ExternalCharacterAnimationGenerationInputs"]
["not"]["anyOf"];
assert_eq!(
forbidden_generation_inputs
.as_array()
.expect("角色动作 generationInputs 约束必须是数组")
.iter()
.filter_map(|schema| schema["required"][0].as_str())
.collect::<Vec<_>>(),
legacy_runtime_fields
);
for request_schema in EXTERNAL_MEDIA_CREATE_REQUEST_SCHEMAS {
assert_eq!(
parsed["components"]["schemas"][request_schema]["allOf"][0]["if"]["properties"]["assetKind"]
["const"],
"character-animation"
);
}
assert!(
parsed["paths"]
.get("/api/external/v1/editor/projects")
@@ -1396,7 +1396,7 @@ fn compact_external_api_generation_result(result: Value) -> Value {
frame.retain(|key, _| {
matches!(
key.as_str(),
"frameIndex" | "imageSrc" | "objectKey" | "width" | "height"
"frameIndex" | "imageSrc" | "objectKey" | "assetObjectId" | "width" | "height"
)
});
remove_unstable_external_generation_media_fields(frame);
@@ -2365,6 +2365,23 @@ mod tests {
"height": 1024,
"generationInputs": {"private": true}
},
"frames": [
{
"imageSrc": "/generated/action/frame-01.png",
"objectKey": "generated/action/frame-01.png",
"assetObjectId": "asset-object-frame-01",
"width": 192,
"height": 256,
"provider": "internal-provider-must-not-persist"
},
{
"imageSrc": "/generated/action/frame-02.png",
"objectKey": "generated/action/frame-02.png",
"assetObjectId": "asset-object-frame-02",
"width": 192,
"height": 256
}
],
"project": {
"projectId": "project-1",
"canvas": {"layers": ["large-layout-must-not-persist"]}
@@ -2421,6 +2438,16 @@ mod tests {
);
assert!(result["asset"].get("thumbnailSrc").is_none());
assert!(result["asset"].get("generationInputs").is_none());
assert_eq!(
result["frames"][0],
json!({
"imageSrc": "/generated/action/frame-01.png",
"objectKey": "generated/action/frame-01.png",
"assetObjectId": "asset-object-frame-01",
"width": 192,
"height": 256
})
);
assert_eq!(
result["warning"],
json!({
@@ -23,7 +23,7 @@ use crate::{
editor_project::{
EditorCanvasGeneratedLayerInput, PersistEditorGeneratedAssetRequest,
build_editor_canvas_generated_layer_item, complete_editor_canvas_generation_with_items,
persist_editor_generated_media_asset,
persist_editor_generated_media_asset, with_editor_media_duration_generation_input,
},
http_error::AppError,
request_context::RequestContext,
@@ -220,6 +220,10 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
let normalized = normalize_editor_sound_effect_request_with_pricing(payload, &pricing)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
let generation_inputs = with_editor_media_duration_generation_input(
generation_inputs,
u32::from(normalized.duration),
);
let settings = require_vector_engine_audio_settings(&state)
.map_err(|error| error.into_response_with_context(Some(&request_context)))?;
let http_client = platform_audio::build_vector_engine_audio_http_client(&settings)
@@ -284,6 +288,8 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
generation_inputs: generation_inputs.clone(),
thumbnail_src: None,
generation_cost_mud_points: u64::from(normalized.price_mud_points),
image_sequence_frames: None,
image_sequence_duration_ms: None,
},
)
.await
@@ -301,7 +307,6 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
resource_id,
title: completion.title.trim().to_string(),
src: audio_src.clone(),
media_type: Some("audio".to_string()),
thumbnail_src: None,
original_width: EDITOR_AUDIO_WIDTH,
original_height: EDITOR_AUDIO_HEIGHT,
@@ -315,12 +320,9 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
task_id: Some(generated.task_id.clone()),
object_key: generated.object_key.clone(),
asset_object_id: generated.asset_object_id.clone(),
duration_seconds: Some(u32::from(normalized.duration)),
source_resource_id: None,
asset_kind: Some("sound-effect".to_string()),
generation_inputs,
image_sequence_frames: None,
preview_video_path: None,
},
);
complete_editor_canvas_generation_with_items(
@@ -521,6 +523,8 @@ pub(crate) async fn generate_editor_background_music_for_owner(
generation_inputs: generation_inputs.clone(),
thumbnail_src: None,
generation_cost_mud_points: u64::from(normalized.price_mud_points),
image_sequence_frames: None,
image_sequence_duration_ms: None,
},
)
.await
@@ -542,7 +546,6 @@ pub(crate) async fn generate_editor_background_music_for_owner(
}),
title: completion.title.trim().to_string(),
src: audio_src.clone(),
media_type: Some("audio".to_string()),
thumbnail_src: None,
original_width: EDITOR_AUDIO_WIDTH,
original_height: EDITOR_AUDIO_HEIGHT,
@@ -556,12 +559,9 @@ pub(crate) async fn generate_editor_background_music_for_owner(
task_id: Some(generated.task_id.clone()),
object_key: generated.object_key.clone(),
asset_object_id: generated.asset_object_id.clone(),
duration_seconds: None,
source_resource_id: None,
asset_kind: Some("background-music".to_string()),
generation_inputs,
image_sequence_frames: None,
preview_video_path: None,
},
);
complete_editor_canvas_generation_with_items(