收紧角色动作后端正式字段契约

角色动作生成只持久化并返回正式 project resource 与 account asset。
删除 API、后台 mapper 对 generationInputs 和布局动作结果的运行时回退。
动作 generationInputs 旧运行字段、screenColorHex 与正式帧 frameIndex 明确拒绝。
动作画布完成项只写资源引用与 placement,普通 layer 响应移除顶层 mediaType。
This commit is contained in:
2026-08-04 10:20:24 +08:00
parent 230613b07a
commit 8a4ba6283b
5 changed files with 919 additions and 241 deletions
+99 -9
View File
@@ -77,7 +77,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},
@@ -1056,6 +1059,15 @@ fn admin_editor_showcase_asset_payload_from_record(
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_editor_image_sequence_frames_value);
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 +1084,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 +1103,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 +1207,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_editor_image_sequence_frames_value);
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 +1235,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 +1246,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,
}
}
@@ -4043,10 +4068,11 @@ mod tests {
use super::{
AdminDashboardGranularity, AdminDisplayNameDirectory, EditorShowcaseAssetRecord,
admin_dashboard_user_stats_from_record, admin_dashboard_user_stats_from_result,
admin_editor_asset_group_payload, admin_editor_showcase_asset_payload_from_record,
append_spacetime_sql_response_chunk, apply_admin_database_table_filters,
build_admin_asset_read_url_audit, build_admin_dashboard_chart,
build_admin_database_table_row, build_admin_editor_showcase_campaign_image_confirm_request,
admin_editor_asset_group_payload, admin_editor_asset_payload_from_record,
admin_editor_showcase_asset_payload_from_record, append_spacetime_sql_response_chunk,
apply_admin_database_table_filters, build_admin_asset_read_url_audit,
build_admin_dashboard_chart, build_admin_database_table_row,
build_admin_editor_showcase_campaign_image_confirm_request,
build_admin_tracking_event_keys_sql, build_admin_tracking_events_sql, build_body_preview,
build_debug_base_url, build_spacetime_schema_url, clamp_admin_database_table_limit,
clamp_admin_tracking_event_limit, enforce_admin_request_permission,
@@ -4117,6 +4143,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,
}
}
@@ -4151,6 +4179,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,
}
}
@@ -4279,6 +4309,66 @@ 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",
"width": 192,
"height": 256
},
{
"imageSrc": "/generated/action/formal-02.png",
"objectKey": "generated/action/formal-02.png",
"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!(payload.image_sequence_frames, Some(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", "width": 192, "height": 256 },
{ "imageSrc": "/generated/action/frame-02.png", "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!(payload.image_sequence_frames, Some(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(
@@ -70,9 +70,8 @@ use crate::{
EDITOR_ASSET_ID_PREFIX, EDITOR_BGFILTER_CROSS_CHECK_ENABLED,
EDITOR_BGFILTER_DEFAULT_SEG_MODEL, EDITOR_RESOURCE_ID_PREFIX,
EditorCanvasGeneratedLayerInput, EditorGenerationPhaseReporter,
PersistEditorGeneratedAssetRequest,
apply_editor_screen_background_decision_to_generation_inputs,
build_editor_canvas_generated_layer_item, complete_editor_canvas_generation_with_items,
PersistEditorGeneratedAssetRequest, build_editor_canvas_generated_layer_item,
build_editor_canvas_resource_placement_item, complete_editor_canvas_generation_with_items,
persist_editor_generated_media_asset,
remove_editor_generated_screen_background_with_bgfilter,
resolve_editor_reference_object_key_for_owner,
@@ -117,6 +116,7 @@ const SOURCE_IMAGE_RESOLVE_TIMEOUT_MS: u64 = 30_000;
const EDITOR_CHARACTER_ANIMATION_MODEL: &str = "seedance2.0-fast";
const EDITOR_CHARACTER_ANIMATION_ASSET_KIND: &str = "editor_character_animation";
const EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND: &str = "character-animation";
const EDITOR_CHARACTER_ANIMATION_PREVIEW_RESOURCE_ASSET_KIND: &str = "video";
const EDITOR_CHARACTER_ANIMATION_PROVIDER_SOURCE_SLOT: &str = "provider_source";
const EDITOR_VIDEO_ASSET_KIND: &str = "editor_video";
const EDITOR_VIDEO_ENTITY_KIND: &str = "editor_canvas";
@@ -739,11 +739,6 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
}),
)
.await?;
// 把实际选定的背景色落进资产记录,供后续以该资产为参考图时反查旧背景色。
let generation_inputs = apply_editor_screen_background_decision_to_generation_inputs(
generation_inputs,
Some(&screen_background_decision),
);
// 用决策色重新归一化以拿到最终提示词(价格 / 校验已在预扣前用占位色完成)。
let normalized = normalize_editor_character_animation_request_with_pricing(
payload,
@@ -792,11 +787,13 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
task_id: task_id.clone(),
source_resource_id: source_resource_id.clone(),
asset_kind: Some(
EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND.to_string(),
EDITOR_CHARACTER_ANIMATION_PREVIEW_RESOURCE_ASSET_KIND.to_string(),
),
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?;
@@ -819,23 +816,22 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
)
.await?;
let frames = persisted_frames.frames;
let generation_inputs = attach_editor_character_animation_result_metadata(
generation_inputs,
generated.preview_video_path.as_str(),
frames.as_slice(),
normalized.fps,
normalized.duration_seconds,
);
let first_frame = frames.first().ok_or_else(|| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "editor-character-animation",
"message": "角色动作抽帧完成但没有可保存的序列帧。",
}))
})?;
persist_editor_generated_media_asset(
let frame_payload_json = serde_json::to_value(&frames).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "editor-character-animation",
"message": format!("角色动作序列帧无法持久化:{error}"),
}))
})?;
let (final_resource, final_asset) = persist_editor_generated_media_asset(
&state,
PersistEditorGeneratedAssetRequest {
project_id: None,
project_id: project_id.clone(),
owner_user_id: owner_user_id.clone(),
folder_id: asset_folder_id
.clone()
@@ -851,70 +847,72 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
model: EDITOR_CHARACTER_ANIMATION_MODEL.to_string(),
provider: "Ark".to_string(),
task_id: task_id.clone(),
source_resource_id: provider_source_resource_id,
source_resource_id: provider_source_resource_id.clone(),
asset_kind: Some(
EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND.to_string(),
),
generation_inputs: generation_inputs.clone(),
thumbnail_src: Some(first_frame.image_src.clone()),
generation_cost_mud_points: 0,
image_sequence_frames: Some(frame_payload_json),
image_sequence_duration_ms: Some(
u64::from(normalized.duration_seconds) * 1_000,
),
},
)
.await?;
let final_asset = final_asset.ok_or_else(|| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "editor-character-animation",
"message": "角色动作透明序列帧已生成,但最终素材记录缺失。",
}))
})?;
let final_asset = serde_json::to_value(final_asset).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "editor-character-animation",
"message": format!("角色动作最终素材响应无法序列化:{error}"),
}))
})?;
Ok::<_, AppError>((generated, frames, normalized, generation_inputs))
Ok::<_, AppError>((
generated,
frames,
normalized,
final_resource,
final_asset,
))
},
)
.await;
let (generated, frames, normalized, generation_inputs) =
let (generated, frames, normalized, final_resource, final_asset) =
result.map_err(|error| character_animation_error_response(&request_context, error))?;
let completed_project = if let Some(completion) = canvas_completion.as_ref() {
let frame_payload_json = serde_json::to_value(&frames).map_err(|error| {
if project_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_none()
{
return Err(character_animation_error_response(
&request_context,
editor_character_animation_bad_request("角色动作画布完成需要有效的 projectId。"),
));
}
let final_resource = final_resource.as_ref().ok_or_else(|| {
character_animation_error_response(
&request_context,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "editor-character-animation",
"message": format!("角色动作序列帧无法写入画布:{error}"),
"message": "角色动作画布完成缺少最终项目资源。",
})),
)
})?;
let preview_frame = frames.first();
let layer_id = format!("layer-editor-character-animation-{task_id}");
let item = build_editor_canvas_generated_layer_item(
let item = build_editor_canvas_resource_placement_item(
completion,
EditorCanvasGeneratedLayerInput {
layer_id: layer_id.clone(),
resource_id: format!("local-resource-editor-character-animation-{task_id}"),
title: completion.title.trim().to_string(),
src: preview_frame
.map(|frame| frame.image_src.clone())
.unwrap_or_else(|| generated.preview_video_path.clone()),
media_type: Some("image-sequence".to_string()),
thumbnail_src: preview_frame.map(|frame| frame.image_src.clone()),
original_width: preview_frame
.map(|frame| frame.width)
.unwrap_or(normalized.frame_width),
original_height: preview_frame
.map(|frame| frame.height)
.unwrap_or(normalized.frame_height),
display_width: completion.placeholder.width,
display_height: completion.placeholder.height,
source_type: "generated".to_string(),
prompt: Some(generated.submitted_prompt.clone()),
actual_prompt: Some(generated.submitted_prompt.clone()),
model: Some(EDITOR_CHARACTER_ANIMATION_MODEL.to_string()),
provider: Some("ark".to_string()),
task_id: Some(task_id.clone()),
object_key: None,
asset_object_id: None,
duration_seconds: Some(normalized.duration_seconds),
source_resource_id,
asset_kind: Some("character-animation".to_string()),
generation_inputs,
image_sequence_frames: Some(frame_payload_json),
preview_video_path: Some(generated.preview_video_path.clone()),
},
final_resource,
layer_id.clone(),
);
complete_editor_canvas_generation_with_items(
&state,
@@ -931,6 +929,7 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
};
let completed_project =
completed_project.and_then(|project| serde_json::to_value(project).ok());
let final_resource = final_resource.and_then(|resource| serde_json::to_value(resource).ok());
Ok(json_success_body(
Some(&request_context),
@@ -948,6 +947,8 @@ pub(crate) async fn generate_editor_character_animation_for_owner(
price_mud_points: normalized.price_mud_points,
frames,
project: completed_project,
resource: final_resource,
asset: Some(final_asset),
queue_state: None,
},
))
@@ -1093,6 +1094,8 @@ pub(crate) async fn generate_editor_video_for_owner(
generation_inputs: generation_inputs.clone(),
thumbnail_src: generated.thumbnail_src.clone(),
generation_cost_mud_points: u64::from(normalized.price_mud_points),
image_sequence_frames: None,
image_sequence_duration_ms: None,
},
)
.await
@@ -1110,7 +1113,6 @@ pub(crate) async fn generate_editor_video_for_owner(
resource_id,
title: completion.title.trim().to_string(),
src: image_src.clone(),
media_type: Some("video".to_string()),
thumbnail_src: generated.thumbnail_src.clone(),
original_width: normalized.width,
original_height: normalized.height,
@@ -1128,8 +1130,6 @@ pub(crate) async fn generate_editor_video_for_owner(
source_resource_id,
asset_kind: Some(asset_kind.clone()),
generation_inputs,
image_sequence_frames: None,
preview_video_path: None,
},
);
complete_editor_canvas_generation_with_items(
@@ -2359,7 +2359,7 @@ async fn extract_and_persist_editor_character_animation_frames(
Err(frame_error) => frame_errors.push(frame_error),
}
}
frame_payloads.sort_by_key(|frame| frame.payload.frame_index);
frame_payloads.sort_by_key(|frame| frame.frame_index);
frame_errors.sort_by_key(|(frame_index, _)| *frame_index);
if let Some((frame_index, error)) = frame_errors.into_iter().next() {
tracing::warn!(
@@ -2491,8 +2491,8 @@ async fn process_and_persist_editor_character_animation_frame(
.await?;
Ok(ProcessedEditorCharacterAnimationFrame {
frame_index,
payload: EditorCharacterAnimationFramePayload {
frame_index: frame_index as u32 + 1,
image_src: put_result.legacy_public_path,
width: frame_width,
height: frame_height,
@@ -5138,31 +5138,6 @@ fn editor_character_animation_source_asset_label(asset_label: &str) -> String {
format!("{base}{suffix}")
}
fn attach_editor_character_animation_result_metadata(
generation_inputs: Option<Value>,
preview_video_path: &str,
frames: &[EditorCharacterAnimationFramePayload],
fps: u32,
duration_seconds: u32,
) -> Option<Value> {
let mut object = match generation_inputs {
Some(Value::Object(object)) => object,
Some(value) => serde_json::Map::from_iter([("sourceGenerationInputs".to_string(), value)]),
None => serde_json::Map::new(),
};
object.insert(
"characterAnimation".to_string(),
json!({
"previewVideoPath": preview_video_path,
"frames": frames,
"frameCount": frames.len(),
"fps": fps,
"durationSeconds": duration_seconds,
}),
);
Some(Value::Object(object))
}
fn clamp_prompt_seed_text(value: Option<&str>) -> String {
trim_optional_text(value)
.unwrap_or_default()
@@ -5951,6 +5926,7 @@ struct PersistedEditorCharacterAnimationFrameSet {
}
struct ProcessedEditorCharacterAnimationFrame {
frame_index: usize,
payload: EditorCharacterAnimationFramePayload,
object_key: String,
asset_object_id: String,
@@ -6409,7 +6385,8 @@ mod tests {
"persist_editor_generated_media_asset",
"reporter.report_processing(&state).await?",
"extract_and_persist_editor_character_animation_frames",
"Ok::<_, AppError>((generated, frames, normalized, generation_inputs))",
"Ok::<_, AppError>((",
"final_asset",
],
);
}
@@ -6608,49 +6585,6 @@ mod tests {
assert!(!frame_pipeline.contains("DownloadedOpenAiImage"));
}
#[test]
fn editor_character_animation_result_metadata_keeps_the_whole_frame_set() {
let frames = vec![
EditorCharacterAnimationFramePayload {
frame_index: 1,
image_src: "/generated-animations/editor/layer/task/frame01.png".to_string(),
width: 192,
height: 256,
},
EditorCharacterAnimationFramePayload {
frame_index: 2,
image_src: "/generated-animations/editor/layer/task/frame02.png".to_string(),
width: 192,
height: 256,
},
];
let metadata = attach_editor_character_animation_result_metadata(
Some(json!({
"fields": [{ "title": "动作", "value": "待机" }],
"references": [],
})),
"/generated-character-drafts/editor/layer/task/preview.mp4",
frames.as_slice(),
8,
4,
)
.expect("character animation metadata should exist");
assert_eq!(metadata["fields"][0]["value"], "待机");
assert_eq!(metadata["characterAnimation"]["frameCount"], 2);
assert_eq!(metadata["characterAnimation"]["fps"], 8);
assert_eq!(metadata["characterAnimation"]["durationSeconds"], 4);
assert_eq!(
metadata["characterAnimation"]["previewVideoPath"],
"/generated-character-drafts/editor/layer/task/preview.mp4"
);
assert_eq!(
metadata["characterAnimation"]["frames"][1]["imageSrc"],
"/generated-animations/editor/layer/task/frame02.png"
);
}
#[test]
fn editor_character_animation_persists_one_final_asset_after_frame_extraction() {
let source = include_str!("character_animation_assets.rs");
@@ -6660,11 +6594,16 @@ mod tests {
"pub async fn generate_editor_video",
&[
"let persisted_frames = extract_and_persist_editor_character_animation_frames",
"attach_editor_character_animation_result_metadata",
"project_id: None",
"let frame_payload_json = serde_json::to_value(&frames)",
"let (final_resource, final_asset)",
"project_id: project_id.clone()",
"label: asset_label.clone()",
"EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND",
"generation_cost_mud_points: 0",
"image_sequence_frames: Some(frame_payload_json)",
"image_sequence_duration_ms: Some(",
"resource: final_resource",
"asset: Some(final_asset)",
],
);
assert_function_contains(
@@ -6687,6 +6626,22 @@ mod tests {
);
}
#[test]
fn editor_character_animation_canvas_completion_uses_the_final_resource_lineage() {
let source = include_str!("character_animation_assets.rs");
assert_function_contains_in_order(
source,
"pub(crate) async fn generate_editor_character_animation_for_owner",
"pub async fn generate_editor_video",
&[
"let (final_resource, final_asset)",
"source_resource_id: provider_source_resource_id.clone()",
"provider_source_resource_id,",
"source_resource_id: final_source_resource_id",
],
);
}
#[test]
fn editor_video_intermediate_outputs_are_registered_before_derivatives() {
let source = include_str!("character_animation_assets.rs");
@@ -6706,7 +6661,7 @@ mod tests {
"pub async fn generate_editor_video",
&[
"or_else(|| Some(\"project\".to_string()))",
"EDITOR_CHARACTER_ANIMATION_RESOURCE_ASSET_KIND",
"EDITOR_CHARACTER_ANIMATION_PREVIEW_RESOURCE_ASSET_KIND",
"generation_cost_mud_points: u64::from(normalized.price_mud_points)",
],
);
File diff suppressed because it is too large Load Diff
@@ -1,20 +1,3 @@
use axum::{
Json,
extract::{Extension, Path, State, rejection::JsonRejection},
http::{StatusCode, header::CONTENT_TYPE},
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderUpdateRecordInput,
EditorAssetUpdateRecordInput, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
EditorProjectGetRecordInput, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput,
};
use crate::{
api_response::json_success_body,
character_animation_assets::{
@@ -43,6 +26,23 @@ use crate::{
generate_editor_background_music_for_owner, generate_editor_sound_effect_for_owner,
},
};
use axum::{
Json,
extract::{Extension, Path, State, rejection::JsonRejection},
http::{StatusCode, header::CONTENT_TYPE},
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_kernel::build_prefixed_uuid_id;
use spacetime_client::{
EditorAssetCreateRecordInput, EditorAssetDeleteRecordInput, EditorAssetFolderCreateRecordInput,
EditorAssetFolderDeleteRecordInput, EditorAssetFolderUpdateRecordInput,
EditorAssetUpdateRecordInput, EditorProjectCreateRecordInput, EditorProjectDeleteRecordInput,
EditorProjectGetRecordInput, EditorProjectRenameRecordInput,
EditorProjectResourceCreateRecordInput,
};
use std::num::NonZeroU64;
const EXTERNAL_EDITOR_PROVIDER: &str = "external-editor-api";
const SCOPE_EDITOR_PROJECT: &str = "editor:project";
@@ -104,6 +104,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)]
@@ -123,6 +125,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)]
@@ -468,6 +472,9 @@ pub async fn create_external_editor_asset(
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let generation_inputs_json =
serialize_editor_asset_metadata(payload.generation_inputs.clone())?;
let image_sequence_frames_json = crate::editor_project::serialize_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
@@ -496,6 +503,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)?;
@@ -570,6 +579,9 @@ pub async fn create_external_editor_project_resource(
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let generation_inputs_json =
serialize_editor_asset_metadata(payload.generation_inputs.clone())?;
let image_sequence_frames_json = crate::editor_project::serialize_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
@@ -593,6 +605,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)?;
@@ -268,6 +268,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
@@ -285,7 +287,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,
@@ -303,8 +304,6 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
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(
@@ -488,6 +487,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
@@ -509,7 +510,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,
@@ -527,8 +527,6 @@ pub(crate) async fn generate_editor_background_music_for_owner(
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(