修复AGC资源生成的序列帧持久化与画布完成上下文
- 本地manifest资产新增imageSequenceFrames和imageSequenceDurationMs,角色动画完成后随账本恢复并写回manifest - 新建视频、音效和背景音乐时创建或复用同名画板项目与素材库目录,并携带projectId、assetFolderId和canvasCompletion - 资源编辑请求指纹纳入generationMode,旧账本继续通过legacy指纹兼容 - 同步更新共享契约、前端资源投影、实施计划文档与定向测试
This commit is contained in:
@@ -1638,6 +1638,8 @@ pub(crate) fn register_local_asset_entry(
|
||||
kind: kind.to_string(),
|
||||
media_type: media_type.to_string(),
|
||||
local_path: normalized_path.clone(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source,
|
||||
});
|
||||
Ok((id, "asset.register"))
|
||||
|
||||
@@ -2312,6 +2312,8 @@ mod ui_editor_font_tests {
|
||||
kind: "font".to_string(),
|
||||
media_type: "font/ttf".to_string(),
|
||||
local_path: relative_path.to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -2762,6 +2762,8 @@ fn commit_asset_canvas_at_internal(
|
||||
kind: asset_kind,
|
||||
media_type: staged.media_type.clone(),
|
||||
local_path: final_relative_path.clone(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source,
|
||||
};
|
||||
manifest.assets.push(asset.clone());
|
||||
|
||||
@@ -2958,6 +2958,8 @@ mod tests {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -4108,6 +4110,8 @@ mod tests {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -408,6 +408,8 @@ fn refine_preserves_source_and_records_non_destructive_lineage() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -488,6 +490,8 @@ fn discovers_unique_active_refine_draft_and_rejects_ambiguous_candidates() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
@@ -979,6 +983,8 @@ fn rejects_symlinked_or_hardlinked_media_at_source_staging_and_final_paths() {
|
||||
kind: "illustration".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/source.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Uploaded,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -183,6 +183,8 @@ fn successful_first_playable_registration_creates_one_initial_version_with_asset
|
||||
kind: "character".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: "assets/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -633,6 +633,8 @@ mod tests {
|
||||
kind: "test".to_string(),
|
||||
media_type: "image/png".to_string(),
|
||||
local_path: format!("assets/{id}.png"),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: None,
|
||||
|
||||
@@ -362,6 +362,10 @@ struct ResourceEditLedger {
|
||||
remote_object_key: Option<String>,
|
||||
#[serde(default)]
|
||||
remote_legacy_public_path: Option<String>,
|
||||
#[serde(default)]
|
||||
remote_sequence_frames_json: Option<String>,
|
||||
#[serde(default)]
|
||||
remote_sequence_duration_ms: Option<u64>,
|
||||
remote_asset_object_id: Option<String>,
|
||||
remote_model: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -802,6 +806,7 @@ fn resource_edit_request_fingerprint(
|
||||
"projectId": input.expected_project_id,
|
||||
"operationId": input.operation_id,
|
||||
"editKind": input.edit_kind,
|
||||
"generationMode": input.generation_mode,
|
||||
"sourceResourceId": source.canonical_resource_id,
|
||||
"sourcePath": source.source_path,
|
||||
"sourceSha256": source.source_sha256,
|
||||
@@ -1928,6 +1933,7 @@ fn resource_edit_remote_request(
|
||||
prompt: &str,
|
||||
asset_name: &str,
|
||||
source_reference: Option<&str>,
|
||||
canvas_context: Option<&ExternalCanvasGenerationContext>,
|
||||
) -> Result<(&'static str, serde_json::Value), String> {
|
||||
let generation_inputs = serde_json::json!({
|
||||
"source": RESOURCE_EDIT_QUEUE_SOURCE,
|
||||
@@ -1979,27 +1985,57 @@ fn resource_edit_remote_request(
|
||||
source_reference.ok_or_else(|| "视频编辑缺少稳定源引用".to_string())?
|
||||
]);
|
||||
}
|
||||
if let Some(context) = canvas_context {
|
||||
body["projectId"] = serde_json::json!(context.project_id);
|
||||
body["assetFolderId"] = serde_json::json!(context.asset_folder_id);
|
||||
body["canvasCompletion"] = serde_json::json!({
|
||||
"title": asset_name,
|
||||
"placeholder": external_canvas_placeholder("16:9"),
|
||||
});
|
||||
}
|
||||
Ok(("/api/external/v1/editor/videos/generations", body))
|
||||
}
|
||||
LocalProjectResourceEditKind::SoundEffect => Ok((
|
||||
"/api/external/v1/editor/audios/sound-effects/generations",
|
||||
serde_json::json!({
|
||||
LocalProjectResourceEditKind::SoundEffect => {
|
||||
let mut body = serde_json::json!({
|
||||
"prompt": build_resource_edit_audio_prompt(source, prompt, 2_048)?,
|
||||
"model": "eleven_text_to_sound_v2",
|
||||
"loop": false,
|
||||
"assetLabel": asset_name,
|
||||
"generationInputs": generation_inputs,
|
||||
}),
|
||||
)),
|
||||
LocalProjectResourceEditKind::BackgroundMusic => Ok((
|
||||
"/api/external/v1/editor/audios/background-music/generations",
|
||||
serde_json::json!({
|
||||
});
|
||||
if let Some(context) = canvas_context {
|
||||
body["projectId"] = serde_json::json!(context.project_id);
|
||||
body["assetFolderId"] = serde_json::json!(context.asset_folder_id);
|
||||
body["canvasCompletion"] = serde_json::json!({
|
||||
"title": asset_name,
|
||||
"placeholder": external_canvas_placeholder("1:1"),
|
||||
});
|
||||
}
|
||||
Ok((
|
||||
"/api/external/v1/editor/audios/sound-effects/generations",
|
||||
body,
|
||||
))
|
||||
}
|
||||
LocalProjectResourceEditKind::BackgroundMusic => {
|
||||
let mut body = serde_json::json!({
|
||||
"gptDescriptionPrompt": build_resource_edit_audio_prompt(source, prompt, 200)?,
|
||||
"makeInstrumental": true,
|
||||
"assetLabel": asset_name,
|
||||
"generationInputs": generation_inputs,
|
||||
}),
|
||||
)),
|
||||
});
|
||||
if let Some(context) = canvas_context {
|
||||
body["projectId"] = serde_json::json!(context.project_id);
|
||||
body["assetFolderId"] = serde_json::json!(context.asset_folder_id);
|
||||
body["canvasCompletion"] = serde_json::json!({
|
||||
"title": asset_name,
|
||||
"placeholder": external_canvas_placeholder("1:1"),
|
||||
});
|
||||
}
|
||||
Ok((
|
||||
"/api/external/v1/editor/audios/background-music/generations",
|
||||
body,
|
||||
))
|
||||
}
|
||||
_ => Err("当前资源类型不是远端媒体派生".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -2208,6 +2244,9 @@ struct ResourceEditRemoteIdentity {
|
||||
resource_id: Option<String>,
|
||||
object_key: Option<String>,
|
||||
legacy_public_path: Option<String>,
|
||||
sequence_frames:
|
||||
Option<Vec<shared_contracts::game_creation_app::GameCreationAppImageSequenceFrame>>,
|
||||
sequence_duration_ms: Option<u64>,
|
||||
asset_object_id: Option<String>,
|
||||
model: Option<String>,
|
||||
}
|
||||
@@ -2235,6 +2274,41 @@ fn extract_resource_edit_remote_identity(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let sequence_frames = if edit_kind == LocalProjectResourceEditKind::CharacterAnimation {
|
||||
data.get("frames")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|frames| {
|
||||
frames
|
||||
.iter()
|
||||
.filter_map(|frame| {
|
||||
let image_src = json_string_field(frame, "imageSrc")?;
|
||||
let width = frame.get("width").and_then(serde_json::Value::as_u64)? as u32;
|
||||
let height =
|
||||
frame.get("height").and_then(serde_json::Value::as_u64)? as u32;
|
||||
Some(shared_contracts::game_creation_app::GameCreationAppImageSequenceFrame {
|
||||
image_src,
|
||||
object_key: json_string_field(frame, "objectKey"),
|
||||
asset_object_id: json_string_field(frame, "assetObjectId"),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let sequence_duration_ms = if edit_kind == LocalProjectResourceEditKind::CharacterAnimation {
|
||||
data.get("durationSeconds")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|seconds| seconds.saturating_mul(1_000))
|
||||
.or_else(|| {
|
||||
data.get("imageSequenceDurationMs")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if object_key.is_none() && legacy_public_path.is_none() {
|
||||
return Err("资源编辑结果缺少稳定媒体引用".to_string());
|
||||
}
|
||||
@@ -2248,6 +2322,8 @@ fn extract_resource_edit_remote_identity(
|
||||
resource_id,
|
||||
object_key,
|
||||
legacy_public_path,
|
||||
sequence_frames,
|
||||
sequence_duration_ms,
|
||||
asset_object_id,
|
||||
model,
|
||||
})
|
||||
@@ -2574,12 +2650,27 @@ async fn prepare_remote_resource_edit(
|
||||
write_resource_edit_ledger(root, ledger)?;
|
||||
}
|
||||
if ledger.endpoint.is_none() || ledger.request_body_json.is_none() {
|
||||
let canvas_context =
|
||||
if input.generation_mode == LocalProjectResourceGenerationMode::Create {
|
||||
Some(
|
||||
prepare_external_canvas_generation_context(
|
||||
root,
|
||||
&client,
|
||||
&api_base_url,
|
||||
&api_key,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (endpoint, body) = resource_edit_remote_request(
|
||||
input,
|
||||
source,
|
||||
prompt,
|
||||
asset_name,
|
||||
source_reference.as_deref(),
|
||||
canvas_context.as_ref(),
|
||||
)?;
|
||||
ledger.endpoint = Some(endpoint.to_string());
|
||||
ledger.request_body_json = Some(
|
||||
@@ -2606,6 +2697,15 @@ async fn prepare_remote_resource_edit(
|
||||
ledger.remote_resource_id = identity.resource_id;
|
||||
ledger.remote_object_key = identity.object_key.clone();
|
||||
ledger.remote_legacy_public_path = identity.legacy_public_path.clone();
|
||||
ledger.remote_sequence_frames_json = identity
|
||||
.sequence_frames
|
||||
.as_ref()
|
||||
.map(|frames| {
|
||||
serde_json::to_string(frames)
|
||||
.map_err(|error| format!("序列化角色动画序列帧失败:{error}"))
|
||||
})
|
||||
.transpose()?;
|
||||
ledger.remote_sequence_duration_ms = identity.sequence_duration_ms;
|
||||
ledger.remote_asset_object_id = identity.asset_object_id;
|
||||
ledger.remote_model = identity.model;
|
||||
update_resource_edit_phase(root, ledger, ResourceEditLedgerPhase::RemoteCompleted)?;
|
||||
@@ -3090,6 +3190,8 @@ pub(crate) fn normalize_local_project_raster_resource_at(
|
||||
kind: source_subtype.to_string(),
|
||||
media_type: verified_media_type,
|
||||
local_path: source_path,
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Generated,
|
||||
canvas_project_id: None,
|
||||
@@ -3301,11 +3403,24 @@ fn commit_resource_edit_asset_internal(
|
||||
staged_extension
|
||||
);
|
||||
let remote = ledger.remote_object_key.is_some() || ledger.remote_legacy_public_path.is_some();
|
||||
let image_sequence_frames = ledger
|
||||
.remote_sequence_frames_json
|
||||
.as_deref()
|
||||
.map(|json| {
|
||||
serde_json::from_str::<
|
||||
Vec<shared_contracts::game_creation_app::GameCreationAppImageSequenceFrame>,
|
||||
>(json)
|
||||
.map_err(|error| format!("解析角色动画序列帧失败:{error}"))
|
||||
})
|
||||
.transpose()?;
|
||||
let image_sequence_duration_ms = ledger.remote_sequence_duration_ms;
|
||||
let asset = GameCreationAppAssetManifestEntry {
|
||||
id: asset_id.clone(),
|
||||
kind: source.asset_kind.clone(),
|
||||
media_type: staged_media_type.to_string(),
|
||||
local_path: relative_path.clone(),
|
||||
image_sequence_frames,
|
||||
image_sequence_duration_ms,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: if remote {
|
||||
GameCreationAppAssetSourceKind::Canvas
|
||||
@@ -4293,6 +4408,8 @@ pub(crate) async fn derive_local_project_resource_at(
|
||||
remote_resource_id: None,
|
||||
remote_object_key: None,
|
||||
remote_legacy_public_path: None,
|
||||
remote_sequence_frames_json: None,
|
||||
remote_sequence_duration_ms: None,
|
||||
remote_asset_object_id: None,
|
||||
remote_model: None,
|
||||
terminal_failure_code: None,
|
||||
@@ -4579,6 +4696,8 @@ mod tests {
|
||||
remote_resource_id: None,
|
||||
remote_object_key: None,
|
||||
remote_legacy_public_path: None,
|
||||
remote_sequence_frames_json: None,
|
||||
remote_sequence_duration_ms: None,
|
||||
remote_asset_object_id: None,
|
||||
remote_model: None,
|
||||
terminal_failure_code: None,
|
||||
@@ -5692,7 +5811,7 @@ mod tests {
|
||||
);
|
||||
bgm.prompt = prompt.to_string();
|
||||
let (bgm_endpoint, bgm_body) =
|
||||
resource_edit_remote_request(&bgm, &source, prompt, "主题音乐编辑版", None)
|
||||
resource_edit_remote_request(&bgm, &source, prompt, "主题音乐编辑版", None, None)
|
||||
.expect("build BGM request");
|
||||
assert_eq!(
|
||||
bgm_endpoint,
|
||||
@@ -5719,6 +5838,7 @@ mod tests {
|
||||
prompt,
|
||||
"视频编辑版",
|
||||
Some("stable-video-reference"),
|
||||
None,
|
||||
)
|
||||
.expect("build video request");
|
||||
assert_eq!(video_endpoint, "/api/external/v1/editor/videos/generations");
|
||||
@@ -5731,7 +5851,7 @@ mod tests {
|
||||
let mut sound_effect = bgm.clone();
|
||||
sound_effect.edit_kind = LocalProjectResourceEditKind::SoundEffect;
|
||||
let (sound_effect_endpoint, _) =
|
||||
resource_edit_remote_request(&sound_effect, &source, prompt, "音效编辑版", None)
|
||||
resource_edit_remote_request(&sound_effect, &source, prompt, "音效编辑版", None, None)
|
||||
.expect("build sound effect request");
|
||||
assert_eq!(
|
||||
sound_effect_endpoint,
|
||||
@@ -5746,6 +5866,7 @@ mod tests {
|
||||
prompt,
|
||||
"图片编辑版",
|
||||
Some("stable-image-reference"),
|
||||
None,
|
||||
)
|
||||
.expect("build image request");
|
||||
assert_eq!(image_endpoint, "/api/external/v1/editor/images/edits");
|
||||
@@ -5776,6 +5897,11 @@ mod tests {
|
||||
source_version: None,
|
||||
};
|
||||
let prompt = "全新的森林场景";
|
||||
let canvas_context = ExternalCanvasGenerationContext {
|
||||
project_id: "project-1".to_string(),
|
||||
asset_folder_id: "folder-1".to_string(),
|
||||
canvas_name: "测试画板".to_string(),
|
||||
};
|
||||
|
||||
let mut create_video = input(
|
||||
std::path::Path::new("."),
|
||||
@@ -5784,14 +5910,51 @@ mod tests {
|
||||
"create:video".to_string(),
|
||||
);
|
||||
create_video.generation_mode = LocalProjectResourceGenerationMode::Create;
|
||||
let (create_video_endpoint, create_video_body) =
|
||||
resource_edit_remote_request(&create_video, &source, prompt, "新视频", None)
|
||||
.expect("build create video request");
|
||||
let (create_video_endpoint, create_video_body) = resource_edit_remote_request(
|
||||
&create_video,
|
||||
&source,
|
||||
prompt,
|
||||
"新视频",
|
||||
None,
|
||||
Some(&canvas_context),
|
||||
)
|
||||
.expect("build create video request");
|
||||
assert_eq!(
|
||||
create_video_endpoint,
|
||||
"/api/external/v1/editor/videos/generations"
|
||||
);
|
||||
assert!(create_video_body.get("referenceVideoSrcs").is_none());
|
||||
assert_eq!(
|
||||
create_video_body["projectId"],
|
||||
serde_json::json!("project-1")
|
||||
);
|
||||
assert_eq!(
|
||||
create_video_body["assetFolderId"],
|
||||
serde_json::json!("folder-1")
|
||||
);
|
||||
assert!(create_video_body.get("canvasCompletion").is_some());
|
||||
|
||||
let mut create_sound_effect = create_video.clone();
|
||||
create_sound_effect.edit_kind = LocalProjectResourceEditKind::SoundEffect;
|
||||
let (create_sound_effect_endpoint, create_sound_effect_body) =
|
||||
resource_edit_remote_request(
|
||||
&create_sound_effect,
|
||||
&source,
|
||||
"清脆的点击声",
|
||||
"新音效",
|
||||
None,
|
||||
Some(&canvas_context),
|
||||
)
|
||||
.expect("build create sound effect request");
|
||||
assert_eq!(
|
||||
create_sound_effect_endpoint,
|
||||
"/api/external/v1/editor/audios/sound-effects/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
create_sound_effect_body["projectId"],
|
||||
serde_json::json!("project-1")
|
||||
);
|
||||
assert!(create_sound_effect_body.get("canvasCompletion").is_some());
|
||||
|
||||
let mut animation = create_video.clone();
|
||||
animation.edit_kind = LocalProjectResourceEditKind::CharacterAnimation;
|
||||
@@ -5802,6 +5965,7 @@ mod tests {
|
||||
"让角色自然呼吸",
|
||||
"角色动画",
|
||||
Some("stable-image-object-key"),
|
||||
None,
|
||||
)
|
||||
.expect("build character animation request");
|
||||
assert_eq!(
|
||||
@@ -5819,6 +5983,16 @@ mod tests {
|
||||
"data": {
|
||||
"previewVideoPath": "/generated-character-drafts/layer/animation/preview.mp4",
|
||||
"objectKey": "generated-character-drafts/layer/animation/frame-01.png",
|
||||
"durationSeconds": 4,
|
||||
"frames": [
|
||||
{
|
||||
"imageSrc": "generated-character-drafts/layer/animation/frame-01.png",
|
||||
"objectKey": "generated-character-drafts/layer/animation/frame-01.png",
|
||||
"assetObjectId": "animation-frame-object",
|
||||
"width": 720,
|
||||
"height": 1280
|
||||
}
|
||||
],
|
||||
"resource": {
|
||||
"resourceId": "editor-resource-animation",
|
||||
"objectKey": "generated-character-drafts/layer/animation/frame-01.png"
|
||||
@@ -5838,6 +6012,16 @@ mod tests {
|
||||
identity.object_key.as_deref(),
|
||||
Some("generated-character-drafts/layer/animation/frame-01.png")
|
||||
);
|
||||
assert_eq!(identity.sequence_duration_ms, Some(4_000));
|
||||
assert_eq!(identity.sequence_frames.as_ref().map(Vec::len), Some(1));
|
||||
assert_eq!(
|
||||
identity
|
||||
.sequence_frames
|
||||
.as_ref()
|
||||
.and_then(|frames| frames.first())
|
||||
.map(|frame| frame.width),
|
||||
Some(720)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -45,6 +45,14 @@ export type ProjectResource = {
|
||||
dependencyDepth: number;
|
||||
content?: string;
|
||||
version?: ProjectVersionResourceSummary;
|
||||
imageSequenceFrames?: Array<{
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
}> | null;
|
||||
imageSequenceDurationMs?: number | null;
|
||||
};
|
||||
|
||||
const documentExtension =
|
||||
@@ -200,6 +208,8 @@ export function projectResourcesFromReadModels(
|
||||
referenceResourceIds: asset.source.referenceResourceIds ?? [],
|
||||
dependencies: [],
|
||||
dependencyDepth: 0,
|
||||
imageSequenceFrames: asset.imageSequenceFrames ?? null,
|
||||
imageSequenceDurationMs: asset.imageSequenceDurationMs ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
|
||||
- 统一资源编辑命令新增 `generationMode=create|derive`:`create` 用于无源生成视频、音效和背景音乐,`derive` 保持现有“基于已有资源派生”。视频新建请求不再携带 `referenceVideoSrcs`;音效 / 背景音乐新建使用同款文本生成端点,但提示词前缀改为“生成新音频”。
|
||||
- 图片资源新增“生成动画”派生入口:复用 `/api/external/v1/editor/character-animations/generations`(账号模式自动映射 `/api/editor/character-animations/generations`),以源图片的稳定 objectKey、真实宽高和固定 `seedance2.0-fast / 720p / same / 32帧 / 4秒` 参数提交;完成后下载 `previewVideoPath` 预览视频并以 `character-animation` 资源类型登记为本地 MP4/WebM 媒体,避免只保存首帧。
|
||||
- 本地 manifest 资产模型新增 `imageSequenceFrames` 与 `imageSequenceDurationMs`:角色动画完成时把远端 `frames` 与 `durationSeconds` 随账本持久化,恢复或重新提交后仍写回 manifest,前端资源投影同步暴露。
|
||||
- 资源页顶部新增“生成视频 / 生成音效 / 生成背景音乐”三个入口,图片聚焦态新增“生成动画”按钮;统一资源编辑面板按 `generationMode` 显示“新建媒体资源”或“编辑现有资源”。
|
||||
- 新建视频 / 音效 / 背景音乐时,先复用 `prepare_external_canvas_generation_context` 创建或匹配同名画板项目与素材库目录,并在生成请求中携带 `projectId / assetFolderId / canvasCompletion`,结果同时进入平台画布 / 素材库与本地 manifest。
|
||||
- durable 账本新增 `remoteLegacyPublicPath` 字段(旧账本默认缺省),用于角色动画预览视频的换签下载与恢复;资源提交、轮询、下载、manifest 提交和恢复队列继续复用既有资源编辑事务链。
|
||||
- 资源编辑请求指纹纳入 `generationMode`,同一 `operationId` 换生成模式重试时不再按旧账本模式恢复;旧账本继续通过 legacy 指纹兼容。
|
||||
- 去背景(`/api/editor/images/background-removals`)仍缺 External v1 契约,不在本次补齐范围;本次只补视频 / 角色动画 / 音效 / 背景音乐。
|
||||
|
||||
## 2026-08-19 UI Editor 节点右键菜单
|
||||
|
||||
@@ -466,6 +466,14 @@ export interface GameCreationAppAssetManifestEntry {
|
||||
mediaType: string;
|
||||
localPath: string;
|
||||
source: GameCreationAppAssetSource;
|
||||
imageSequenceFrames?: Array<{
|
||||
imageSrc: string;
|
||||
objectKey?: string | null;
|
||||
assetObjectId?: string | null;
|
||||
width: number;
|
||||
height: number;
|
||||
}> | null;
|
||||
imageSequenceDurationMs?: number | null;
|
||||
}
|
||||
|
||||
export const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION =
|
||||
|
||||
@@ -478,6 +478,18 @@ pub struct GameCreationAppAssetSource {
|
||||
pub reference_resource_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameCreationAppImageSequenceFrame {
|
||||
pub image_src: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub object_key: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub asset_object_id: Option<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameCreationAppAssetManifestEntry {
|
||||
@@ -486,6 +498,10 @@ pub struct GameCreationAppAssetManifestEntry {
|
||||
pub media_type: String,
|
||||
pub local_path: String,
|
||||
pub source: GameCreationAppAssetSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image_sequence_frames: Option<Vec<GameCreationAppImageSequenceFrame>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub image_sequence_duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION: &str = "game-creator-resource-layout.v1";
|
||||
@@ -1777,6 +1793,8 @@ mod tests {
|
||||
kind: "character".to_string(),
|
||||
media_type: "image".to_string(),
|
||||
local_path: "assets/images/player.png".to_string(),
|
||||
image_sequence_frames: None,
|
||||
image_sequence_duration_ms: None,
|
||||
source: GameCreationAppAssetSource {
|
||||
kind: GameCreationAppAssetSourceKind::Canvas,
|
||||
canvas_project_id: Some("canvas-project-1".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user