use crate::character_animation_assets::find_motion_template; use shared_contracts::assets::CharacterAnimationStrategy; pub(crate) fn build_character_animation_prompt( strategy: &CharacterAnimationStrategy, prompt_text: &str, character_brief_text: Option<&str>, action_template_id: Option<&str>, animation: &str, frame_count: u32, fps: u32, duration_seconds: u32, loop_: bool, use_chroma_key: bool, ) -> String { match strategy { CharacterAnimationStrategy::ImageToVideo => build_ark_character_animation_prompt( animation, prompt_text, character_brief_text, action_template_id, loop_, use_chroma_key, ), CharacterAnimationStrategy::ImageSequence => { build_image_sequence_prompt(animation, prompt_text, frame_count, use_chroma_key) } CharacterAnimationStrategy::MotionTransfer | CharacterAnimationStrategy::ReferenceToVideo => build_npc_animation_prompt( animation, prompt_text, character_brief_text, action_template_id, loop_, use_chroma_key, fps, duration_seconds, ), } } fn build_image_sequence_prompt( animation: &str, prompt_text: &str, frame_count: u32, use_chroma_key: bool, ) -> String { [ format!( "同一角色连续 {} 帧动作序列,动作主题是 {}。", frame_count, animation ), "固定机位,单人,全身,侧身朝右,保持同一套服装、发型、武器和体型。".to_string(), "帧间动作连续,姿态逐步推进,不要换人,不要跳变,不要多余物体。".to_string(), if use_chroma_key { "纯绿色背景,无地面装饰,方便后期抠像。".to_string() } else { "背景尽量纯净,避免复杂场景。".to_string() }, prompt_text.trim().to_string(), ] .into_iter() .filter(|value| !value.trim().is_empty()) .collect::>() .join(" ") } fn build_npc_animation_prompt( animation: &str, prompt_text: &str, character_brief_text: Option<&str>, action_template_id: Option<&str>, loop_: bool, use_chroma_key: bool, fps: u32, duration_seconds: u32, ) -> String { let character_brief = build_compact_animation_character_brief(character_brief_text); let action_detail_text = sanitize_animation_prompt_text(prompt_text, 140); let loop_rule = if loop_ { "这是循环动作,直接进入动作循环中段,不要开场静止站桩,不要把主参考图原样作为第一帧。" .to_string() } else if animation == "die" { "这是死亡终结动作,首帧参考主图角色形象即可,尾帧停在死亡结束姿态,不要回到主图形象。" .to_string() } else { "这是非循环动作,首帧和尾帧都要回到参考主图角色形象,中段完成动作变化。".to_string() }; if let Some(template) = action_template_id.and_then(|id| find_motion_template(id)) { return [ format!( "单人 NPC 全身动作视频,动作主题是 {}。角色固定为同一人,右向斜侧身,镜头稳定,轮廓清晰,武器不可丢失。", template.animation ), if use_chroma_key { "背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string() } else { "背景简洁纯净,无复杂场景。".to_string() }, if character_brief.is_empty() { String::new() } else { format!("角色设定:{}。", character_brief) }, format!("动作补充:{}。", template.prompt_suffix), if action_detail_text.is_empty() { String::new() } else { format!("动作细节:{}。", action_detail_text) }, format!("目标帧率 {} fps,时长约 {} 秒。", fps.clamp(1, 60), duration_seconds.clamp(1, 8)), loop_rule, ] .into_iter() .filter(|value| !value.trim().is_empty()) .collect::>() .join(" "); } [ format!("单人 NPC 全身动作视频,动作主题是 {}。", animation), "角色固定为同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。".to_string(), "动作连贯,避免服装、发型、面部、武器随机漂移。".to_string(), if use_chroma_key { "背景为纯绿色绿幕,无其他人物和场景元素,方便后期抠像。".to_string() } else { "背景简洁纯净,无复杂场景。".to_string() }, if character_brief.is_empty() { String::new() } else { format!("角色设定:{}。", character_brief) }, if action_detail_text.is_empty() { String::new() } else { action_detail_text }, format!( "目标帧率 {} fps,时长约 {} 秒。", fps.clamp(1, 60), duration_seconds.clamp(1, 8) ), loop_rule, ] .into_iter() .filter(|value| !value.trim().is_empty()) .collect::>() .join(" ") } fn build_ark_character_animation_prompt( animation: &str, prompt_text: &str, character_brief_text: Option<&str>, action_template_id: Option<&str>, loop_: bool, use_chroma_key: bool, ) -> String { let normalized_animation_name = animation.trim().replace(char::is_whitespace, "_"); let normalized_animation_name = if normalized_animation_name.is_empty() { "idle".to_string() } else { normalized_animation_name }; let character_brief = build_compact_animation_character_brief(character_brief_text); let action_detail_text = sanitize_animation_prompt_text(prompt_text, 140); if let Some(template) = action_template_id.and_then(find_motion_template) { return build_video_action_prompt( template.id, template.prompt_suffix, action_detail_text.as_str(), Some(character_brief.as_str()), use_chroma_key, ); } build_video_action_prompt( normalized_animation_name.as_str(), if loop_ { "循环动作必须自然闭环,不要静止开场。" } else { "中段完成完整动作变化,收束干净。" }, action_detail_text.as_str(), Some(character_brief.as_str()), use_chroma_key, ) } /// 角色动作视频统一提示词骨架,按每个动作模板与补充描述生成。 fn build_video_action_prompt( action_id: &str, action_sequence: &str, action_detail_text: &str, character_brief_text: Option<&str>, use_chroma_key: bool, ) -> String { [ format!("生成有创意细节饱满的角色动作视频,动作英文名是 {}。", action_id), "角色固定为图1同一角色,保持右向斜侧身动作视角,镜头稳定,轮廓清晰,禁止退化成完全90度纯右视图。".to_string(), "画面要求:1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景等场景内容。".to_string(), format!("动作结构:{}。结尾要求:动作收束清楚,便于后续抽帧。", action_sequence), if use_chroma_key { "背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。".to_string() } else { "背景简洁纯净,无其他人物和复杂场景元素,方便后期抽帧。".to_string() }, format!( "动作补充细节:{}", if action_detail_text.trim().is_empty() { "保持动作清晰、节奏明确、适合后续抽帧为 sprite sheet。" } else { action_detail_text.trim() } ), character_brief_text .map(str::trim) .filter(|value| !value.is_empty()) .map(|value| format!("角色设定:{}。", value)) .unwrap_or_default(), "目标是后续抽帧为横版动作游戏精灵表,因此不要镜头切换,不要景别变化,不要角色漂移。".to_string(), ] .into_iter() .filter(|value| !value.trim().is_empty()) .collect::>() .join(" ") } pub(crate) fn build_fallback_moderation_safe_animation_prompt( animation: &str, loop_: bool, use_chroma_key: bool, ) -> String { [ format!("单人全身角色动作视频,动作主题是 {}。", animation), "角色固定为同一人,右向斜侧身,镜头稳定,轮廓清楚。".to_string(), if loop_ { "循环动作直接进入稳定循环,不要静止开场,不要定格首帧。".to_string() } else { "非循环动作首尾回到角色标准站姿,中段完成动作变化。".to_string() }, if use_chroma_key { "背景为纯绿色绿幕,无其他人物和场景元素。".to_string() } else { "背景简洁纯净。".to_string() }, ] .join(" ") } fn sanitize_animation_prompt_text(value: &str, max_length: usize) -> String { value .replace(char::is_whitespace, " ") .replace("血浆", "") .replace("喷血", "") .replace("鲜血", "") .replace("断肢", "") .replace("斩首", "") .replace("裸体", "") .replace("裸露", "") .replace("色情", "") .replace("性交", "") .replace("死亡", "倒地结束") .replace("死去", "倒地结束") .replace("击杀", "倒地结束") .replace("受击", "失衡") .replace("受伤", "失衡") .replace("砍杀", "挥击") .replace("斩击", "挥击") .split_whitespace() .collect::>() .join(" ") .chars() .take(max_length) .collect::() .trim() .to_string() } fn build_compact_animation_character_brief(value: Option<&str>) -> String { let normalized = sanitize_animation_prompt_text(value.unwrap_or_default(), 160); if normalized.is_empty() { return String::new(); } normalized .split(['/', '|', '\n', ',', ',', '。', ';', ';']) .map(str::trim) .filter(|item| !item.is_empty()) .take(4) .collect::>() .join(",") }