Refactor server-rs runtime and update related docs

This commit is contained in:
2026-04-25 14:29:44 +08:00
parent 019dd9efba
commit 6be3afe45a
56 changed files with 1561 additions and 1158 deletions

View File

@@ -1,365 +1,6 @@
use crate::character_animation_assets::find_motion_template;
use shared_contracts::assets::CharacterAnimationStrategy;
/// 自定义世界角色主图提示词脚本。
pub(crate) fn build_character_visual_prompt(
prompt_text: &str,
character_brief_text: Option<&str>,
) -> String {
let character_brief = [character_brief_text.unwrap_or_default(), prompt_text]
.into_iter()
.map(str::trim)
.filter(|value| !value.is_empty())
.collect::<Vec<_>>()
.join("\n");
build_master_prompt(character_brief.as_str())
}
/// 角色主图统一提示词骨架,迁移自旧共享 qwenSprite 主链。
fn build_master_prompt(character_brief: &str) -> String {
[
"单人2D 横版游戏角色标准设定图,主体完整可见,底部轮廓完整,身体比例稳定,轮廓清楚,适合后续制作 sprite sheet 动画。".to_string(),
"视角要求:角色采用横版动作素材常用的右向斜侧身站姿,身体整体朝右,但保留少量正面信息,能读到面部轮廓与胸肩结构,不是完全 90 度纯右视图,也不是正面立绘。".to_string(),
"主体要求:画面中只保留单个角色主体,不要额外人物、动物、召唤物、载具或陪体。".to_string(),
"画面要求1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素或其他角色以外的场景内容。".to_string(),
"风格要求:横版像素动作角色体型,头身比优先控制在 1 到 1.5 头身,保留清楚的头、躯干、双臂和双腿轮廓。明确的像素动作角色设定稿气质,整体按像素游戏角色设计方向组织,使用深色清楚轮廓、稳定剪影、有限大色块和硬朗边缘,不要柔和厚涂插画感,发型、服装、配饰优先形成醒目可读的像素级识别点,适合横版动作 sprite 资产。高可读性游戏角色设定图,形体清晰,服装层次明确,便于后续连续动作生成。".to_string(),
"请先拆解设定中的“身份词、主题词、身体结构词”。如果文字设定没有明确要求非人身体结构,默认优先使用参考图对应的人类或类人动作角色骨架,保持清楚的头、躯干、手臂和双腿轮廓,只有当文字设定明确要求非人结构时,才改为对应非人身体。".to_string(),
"主题词默认只作用在角色自身的服装剪裁、材质、纹样、饰品、发光细节上,不要把主题词自动扩写成背景建筑、自然场景、漂浮装饰或额外环境物件。".to_string(),
"视觉优先级应当是:身体结构词第一,身份词第二,主题词第三。没有明确身体结构词时,默认用人形拟人化表现,再把主题词转译成服装和装饰。".to_string(),
character_brief.trim().to_string(),
]
.into_iter()
.filter(|value| !value.trim().is_empty())
.collect::<Vec<_>>()
.join("\n")
}
/// 自定义世界角色主图负面提示词脚本。
pub(crate) fn build_character_visual_negative_prompt() -> String {
[
"正面视角",
"左朝向",
"完全 90 度纯右视图",
"镜头透视",
"半身像",
"脚被裁切",
"头顶被裁切",
"多角色",
"复杂背景",
"建筑场景",
"漂浮物",
"烟雾环境",
"武器消失",
"武器换手",
"额外手臂",
"额外腿",
"服装变化",
"脸部变化",
"模糊",
"运动模糊",
"文字",
"水印",
"UI 元素",
"软萌 Q版大头贴",
"儿童绘本风",
"厚涂插画感",
"低对比柔边",
]
.join("")
}
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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(),
"视角要求:角色采用横版动作素材常用的右向斜侧身站姿,身体整体朝右,但保留少量正面信息,能读到面部轮廓与胸肩结构,不是完全 90 度纯右视图,也不是正面立绘。".to_string(),
"主体要求:画面中只保留单个角色主体,不要额外人物、动物、召唤物、载具或陪体。".to_string(),
"画面要求1:1 正方形画布,画面中心构图,角色主体完整置于画面中央,不要裁切主体顶部和底部,不要镜头透视,不要特写。背景固定为纯绿色绿幕,只作为抠像底色,不出现建筑、室内布景、风景、地面道具、漂浮物、烟雾叙事元素或其他角色以外的场景内容。".to_string(),
"风格要求:横版像素动作角色体型,头身比优先控制在 1 到 1.5 头身,保留清楚的头、躯干、双臂和双腿轮廓。明确的像素动作角色设定稿气质,整体按像素游戏角色设计方向组织,使用深色清楚轮廓、稳定剪影、有限大色块和硬朗边缘,不要柔和厚涂插画感,发型、服装、配饰优先形成醒目可读的像素级识别点,适合横版动作 sprite 资产。高可读性游戏角色设定图,形体清晰,服装层次明确,便于后续连续动作生成。".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::<Vec<_>>()
.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::<Vec<_>>()
.join(" ")
.chars()
.take(max_length)
.collect::<String>()
.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::<Vec<_>>()
.join("")
}
pub(crate) use crate::prompt::character_animation::{
build_character_animation_prompt, build_fallback_moderation_safe_animation_prompt,
};
pub(crate) use crate::prompt::character_visual::{
build_character_visual_negative_prompt, build_character_visual_prompt,
};