From ba8662f844fdef77cc36a602ede48eeebe4b0d07 Mon Sep 17 00:00:00 2001 From: Linghong Date: Wed, 8 Jul 2026 03:50:44 +0000 Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E4=BD=9C=E8=A7=86=E9=A2=91=E8=83=8C?= =?UTF-8?q?=E6=99=AF=E8=89=B2=E5=A4=9A=E8=89=B2=E8=87=AA=E5=8A=A8=E5=86=B3?= =?UTF-8?q?=E7=AD=96=E5=B9=B6=E6=8E=A5=E5=85=A5=E9=98=BF=E9=87=8C=E4=BA=91?= =?UTF-8?q?=E6=8A=A0=E5=B8=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 动画生成请求新增 screenColor,继承生图入口的 auto 决策行为, 提示词从写死绿幕改为纯色背景条款(LLM 决策 11 色) - 后端抽帧抠图改为逐帧阿里云优先(并发 3 保序),单帧失败降级 本地键色算法(使用决策出的背景色) - 清理 legacy 纯绿幕常量与提示词 Co-Authored-By: Claude Fable 5 --- .../src/character_animation_assets.rs | 138 +++++++++++++----- .../api-server/src/editor_green_screen.rs | 10 -- .../src/editor_screen_background_decision.rs | 2 + .../crates/shared-contracts/src/assets.rs | 3 + ...ageCanvasGenerationSubmissionModel.test.ts | 1 + .../ImageCanvasGenerationSubmissionModel.ts | 2 + .../image-editor/editorProjectClient.ts | 2 + 7 files changed, 113 insertions(+), 45 deletions(-) diff --git a/server-rs/crates/api-server/src/character_animation_assets.rs b/server-rs/crates/api-server/src/character_animation_assets.rs index ea44c174b..49eb850fb 100644 --- a/server-rs/crates/api-server/src/character_animation_assets.rs +++ b/server-rs/crates/api-server/src/character_animation_assets.rs @@ -61,10 +61,13 @@ use crate::{ editor_generation_source_entity_id, enqueue_editor_generation_job, }, editor_green_screen::{ - LEGACY_EDITOR_GREEN_SCREEN_CHARACTER_PROMPT_CLAUSE, - legacy_editor_green_screen_background_color, + EditorScreenBackgroundColor, editor_green_screen_character_prompt_clause, remove_editor_generated_green_screen_background, }, + editor_screen_background_decision::{ + EditorScreenBackgroundDecisionInput, EditorScreenBackgroundDecisionKind, + resolve_editor_screen_background_color, + }, editor_project::{ EditorCanvasGeneratedLayerInput, PersistEditorGeneratedAssetRequest, build_editor_canvas_generated_layer_item, complete_editor_canvas_generation_with_items, @@ -564,9 +567,13 @@ pub async fn generate_editor_character_animation( })), ) })?; - let normalized = - normalize_editor_character_animation_request_with_pricing(payload.clone(), &pricing) - .map_err(|error| character_animation_error_response(&request_context, error))?; + // 队列路径只取定价,背景色决策留到实际执行时再做,这里用默认色占位。 + let normalized = normalize_editor_character_animation_request_with_pricing( + payload.clone(), + &pricing, + crate::editor_green_screen::default_editor_screen_background_color(), + ) + .map_err(|error| character_animation_error_response(&request_context, error))?; let source_entity_id = editor_generation_source_entity_id( payload.project_id.as_deref(), payload.source_layer_id.as_str(), @@ -628,9 +635,25 @@ pub(crate) async fn generate_editor_character_animation_for_owner( })), ) })?; - let normalized = - normalize_editor_character_animation_request_with_pricing(payload, &pricing) - .map_err(|error| character_animation_error_response(&request_context, error))?; + // 背景色决策:显式传入 → 继承源角色图(前端从图层 generationInputs 带入)→ LLM 自动决策 → 默认色。 + let screen_background_decision = resolve_editor_screen_background_color( + state.llm_client(), + EditorScreenBackgroundDecisionInput { + kind: EditorScreenBackgroundDecisionKind::CharacterAnimation, + screen_color: payload.screen_color.clone(), + prompt: payload.prompt_text.clone(), + icon_descriptions: Vec::new(), + reference_count: 1, + }, + ) + .await + .map_err(|error| character_animation_error_response(&request_context, error))?; + let normalized = normalize_editor_character_animation_request_with_pricing( + payload, + &pricing, + screen_background_decision.color, + ) + .map_err(|error| character_animation_error_response(&request_context, error))?; let settings = require_editor_character_animation_settings(&state, &normalized) .map_err(|error| character_animation_error_response(&request_context, error))?; let extraction_settings = resolve_backend_frame_extraction_settings(&state); @@ -2075,10 +2098,13 @@ async fn extract_and_persist_editor_character_animation_frames( ) .await?; let finalized_frames = remove_editor_character_animation_frame_backgrounds( + state, finalized_frames, request.frame_width, request.frame_height, - )?; + request.screen_color, + ) + .await?; let mut frame_payloads = Vec::with_capacity(finalized_frames.len()); for (index, frame) in finalized_frames.into_iter().enumerate() { @@ -2147,30 +2173,58 @@ async fn persist_editor_character_animation_green_screen_source_frames( Ok(()) } -fn remove_editor_character_animation_frame_backgrounds( +/// 逐帧抠图:优先阿里云通用抠图,失败降级本地键色(使用生成时的纯色背景色)。 +/// 小并发保序处理,单帧降级不影响其他帧。 +async fn remove_editor_character_animation_frame_backgrounds( + state: &AppState, frames: Vec, frame_width: u32, frame_height: u32, + screen_color: EditorScreenBackgroundColor, ) -> Result, AppError> { - let mut removed_frames = Vec::with_capacity(frames.len()); - for frame in frames { - let removed = remove_editor_generated_green_screen_background( - &DownloadedOpenAiImage { + use futures_util::{StreamExt as _, TryStreamExt as _}; + + const FRAME_MATTING_CONCURRENCY: usize = 3; + + futures_util::stream::iter(frames.into_iter().enumerate().map( + |(frame_index, frame)| async move { + let image = DownloadedOpenAiImage { bytes: frame.bytes, mime_type: frame.mime_type, extension: frame.extension, - }, - legacy_editor_green_screen_background_color(), - )?; - removed_frames.push(finalize_animation_frame_payload( - removed.bytes.as_slice(), - removed.mime_type.as_str(), - frame_width, - frame_height, - false, - )?); - } - Ok(removed_frames) + }; + let removed = match crate::aliyun_matting::segment_image_with_aliyun_matting( + state, + &image, + "editor-animation-frame", + ) + .await + { + Ok(removed) => removed, + Err(error) => { + tracing::warn!( + provider = "aliyun-matting", + frame_index, + screen_color = screen_color.hex, + error = %error, + error_details = ?error.details(), + "editor_animation_frame_aliyun_matting_fallback_to_local" + ); + remove_editor_generated_green_screen_background(&image, screen_color)? + } + }; + finalize_animation_frame_payload( + removed.bytes.as_slice(), + removed.mime_type.as_str(), + frame_width, + frame_height, + false, + ) + }, + )) + .buffered(FRAME_MATTING_CONCURRENCY) + .try_collect::>() + .await } async fn publish_animation_set( @@ -2835,12 +2889,17 @@ fn normalize_editor_character_animation_request( ) -> Result { let pricing = crate::editor_generation_config::load_editor_generation_pricing_from_paths(None) .expect("默认模型定价配置必须合法"); - normalize_editor_character_animation_request_with_pricing(payload, &pricing) + normalize_editor_character_animation_request_with_pricing( + payload, + &pricing, + crate::editor_green_screen::default_editor_screen_background_color(), + ) } fn normalize_editor_character_animation_request_with_pricing( payload: EditorCharacterAnimationGenerateRequest, pricing: &EditorGenerationPricingConfig, + screen_color: EditorScreenBackgroundColor, ) -> Result { let source_layer_id = normalize_required_text(payload.source_layer_id.as_str(), ""); if source_layer_id.is_empty() { @@ -2880,12 +2939,13 @@ fn normalize_editor_character_animation_request_with_pricing( ratio, resolution, ); - let prompt = build_editor_character_animation_prompt(prompt_text.as_str()); + let prompt = build_editor_character_animation_prompt(prompt_text.as_str(), screen_color); Ok(NormalizedEditorCharacterAnimationRequest { source_layer_id, source_image_src, prompt, + screen_color, resolution: resolution.to_string(), ratio: resolve_editor_character_animation_provider_ratio( ratio, @@ -3385,10 +3445,13 @@ fn editor_video_bad_request(message: impl Into) -> AppError { })) } -fn build_editor_character_animation_prompt(prompt_text: &str) -> String { +fn build_editor_character_animation_prompt( + prompt_text: &str, + screen_color: EditorScreenBackgroundColor, +) -> String { format!( "生成游戏角色动画,参考图作为首帧和尾帧,画面中心构图,角色主体完整置于画面中央,禁止镜头透视,禁止特写。{};禁止出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素、文字或其他角色以外的场景内容。\n动作描述:\n{}", - LEGACY_EDITOR_GREEN_SCREEN_CHARACTER_PROMPT_CLAUSE, + editor_green_screen_character_prompt_clause(screen_color), prompt_text.trim() ) } @@ -5091,6 +5154,7 @@ struct NormalizedEditorCharacterAnimationRequest { frame_width: u32, frame_height: u32, fps: u32, + screen_color: EditorScreenBackgroundColor, } #[derive(Debug)] @@ -5319,6 +5383,7 @@ mod tests { source_width: 768, source_height: 1024, prompt_text: "待机呼吸,轻微摆动。".to_string(), + screen_color: None, resolution: "720p".to_string(), ratio: "same".to_string(), frame_count: 48, @@ -5357,6 +5422,7 @@ mod tests { source_width: 1024, source_height: 1024, prompt_text: "待机呼吸".to_string(), + screen_color: None, resolution: "480p".to_string(), ratio: "same".to_string(), frame_count: 32, @@ -5384,6 +5450,7 @@ mod tests { source_width: 1024, source_height: 1024, prompt_text: "奔跑".to_string(), + screen_color: None, resolution: "480p".to_string(), ratio: "1:1".to_string(), frame_count: 48, @@ -5400,14 +5467,15 @@ mod tests { } #[test] - fn editor_character_animation_builds_required_green_screen_prompt() { - let prompt = build_editor_character_animation_prompt("行走两步后回到站姿。"); + fn editor_character_animation_builds_required_screen_background_prompt() { + let color = crate::editor_green_screen::default_editor_screen_background_color(); + let prompt = build_editor_character_animation_prompt("行走两步后回到站姿。", color); assert!(prompt.contains("生成游戏角色动画")); assert!(prompt.contains("参考图作为首帧和尾帧")); - assert!(prompt.contains("背景固定为单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕")); - assert!(prompt.contains("绿幕背景必须平整无纹理、无渐变、无阴影")); - assert!(prompt.contains("角色主体不得带绿色描边、绿色投影或绿色反光")); + assert!(prompt.contains(color.hex)); + assert!(prompt.contains("纯色背景必须平整无纹理、无渐变、无阴影")); + assert!(prompt.contains("角色主体不得带与背景色相同或相近的描边、投影或反光")); assert!(prompt.contains("动作描述:\n行走两步后回到站姿。")); } diff --git a/server-rs/crates/api-server/src/editor_green_screen.rs b/server-rs/crates/api-server/src/editor_green_screen.rs index 586b682d1..6ea4be842 100644 --- a/server-rs/crates/api-server/src/editor_green_screen.rs +++ b/server-rs/crates/api-server/src/editor_green_screen.rs @@ -100,15 +100,6 @@ pub(crate) fn default_editor_screen_background_color() -> EditorScreenBackground EDITOR_SCREEN_BACKGROUND_COLORS[0] } -pub(crate) fn legacy_editor_green_screen_background_color() -> EditorScreenBackgroundColor { - EditorScreenBackgroundColor { - label: "纯绿色", - hex: "#00FF00", - red: 0, - green: 255, - blue: 0, - } -} pub(crate) fn parse_editor_screen_background_color( value: Option<&str>, @@ -139,7 +130,6 @@ pub(crate) const EDITOR_GREEN_SCREEN_ASSET_GUARDRAILS: &str = "素材自身不要出现与背景色相同或相近的描边、底板、投影或反光"; pub(crate) const EDITOR_GREEN_SCREEN_CHARACTER_GUARDRAILS: &str = "角色主体不得带与背景色相同或相近的描边、投影或反光"; -pub(crate) const LEGACY_EDITOR_GREEN_SCREEN_CHARACTER_PROMPT_CLAUSE: &str = "背景固定为单一纯绿色 #00FF00 / RGB(0,255,0) 绿幕,只作为抠像底色;绿幕背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具;角色主体不得带绿色描边、绿色投影或绿色反光"; fn editor_screen_background_color_prompt(color: EditorScreenBackgroundColor) -> String { if color.hex == "#00FF00" { diff --git a/server-rs/crates/api-server/src/editor_screen_background_decision.rs b/server-rs/crates/api-server/src/editor_screen_background_decision.rs index 021988940..848520451 100644 --- a/server-rs/crates/api-server/src/editor_screen_background_decision.rs +++ b/server-rs/crates/api-server/src/editor_screen_background_decision.rs @@ -31,6 +31,7 @@ pub(crate) struct EditorScreenBackgroundDecision { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum EditorScreenBackgroundDecisionKind { Character, + CharacterAnimation, IconSpritesheet, UiDesignAssetExtraction, } @@ -39,6 +40,7 @@ impl EditorScreenBackgroundDecisionKind { fn label(self) -> &'static str { match self { Self::Character => "角色形象生成", + Self::CharacterAnimation => "角色动作视频生成", Self::IconSpritesheet => "图标素材 spritesheet 生成", Self::UiDesignAssetExtraction => "UI 设计图素材提取", } diff --git a/server-rs/crates/shared-contracts/src/assets.rs b/server-rs/crates/shared-contracts/src/assets.rs index 90a03a889..0251a8f32 100644 --- a/server-rs/crates/shared-contracts/src/assets.rs +++ b/server-rs/crates/shared-contracts/src/assets.rs @@ -314,6 +314,9 @@ pub struct EditorCharacterAnimationGenerateRequest { pub source_width: u32, pub source_height: u32, pub prompt_text: String, + /// 纯色抠像背景色(hex)。None/auto 走自动决策;通常继承源角色图的背景色。 + #[serde(default, skip_serializing_if = "Option::is_none")] + pub screen_color: Option, pub resolution: String, pub ratio: String, pub frame_count: u32, diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts index 7abdb2cf7..85c6078e4 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts @@ -726,6 +726,7 @@ describe('ImageCanvasGenerationSubmissionModel', () => { sourceWidth: 960, sourceHeight: 1280, promptText: '循环奔跑动作', + screenColor: 'auto', resolution: '720p', ratio: 'same', frameCount: 48, diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts index 4d57e52e9..6f2d1c139 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts @@ -649,6 +649,8 @@ export function buildCharacterAnimationSubmissionPlan({ sourceWidth: sourceLayer.originalWidth, sourceHeight: sourceLayer.originalHeight, promptText, + // 与生图入口一致:默认 auto,由后端做背景色自动决策。 + screenColor: DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR, resolution: panel.resolution, ratio: panel.ratio, frameCount: panel.frameCount, diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index bc831182b..5c0ef5c53 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -328,6 +328,8 @@ export type EditorCharacterAnimationGenerationInput = { sourceWidth: number; sourceHeight: number; promptText: string; + /** 纯色抠像背景色(hex 或 'auto'),继承生图的背景色选项行为。 */ + screenColor?: string; resolution: EditorCharacterAnimationResolution; ratio: EditorCharacterAnimationRatio; frameCount: EditorCharacterAnimationFrameCount;