1
This commit is contained in:
@@ -6,21 +6,33 @@ pub(crate) fn build_character_visual_prompt(
|
||||
prompt_text: &str,
|
||||
character_brief_text: Option<&str>,
|
||||
) -> String {
|
||||
let merged = [character_brief_text.unwrap_or_default(), prompt_text]
|
||||
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");
|
||||
|
||||
format!(
|
||||
"{}\n单人全身,右向斜侧身,3 到 4 头身,像素动作角色,纯绿色背景,服装完整,轮廓清晰,不要复杂背景。",
|
||||
if merged.is_empty() {
|
||||
"自定义世界角色,服装完整,姿态自然。"
|
||||
} else {
|
||||
merged.as_str()
|
||||
}
|
||||
)
|
||||
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")
|
||||
}
|
||||
|
||||
/// 自定义世界角色主图负面提示词脚本。
|
||||
@@ -222,67 +234,64 @@ fn build_ark_character_animation_prompt(
|
||||
};
|
||||
let character_brief = build_compact_animation_character_brief(character_brief_text);
|
||||
let action_detail_text = sanitize_animation_prompt_text(prompt_text, 140);
|
||||
let frame_rule = if loop_ {
|
||||
"首帧严格使用图片1,尾帧严格使用图片2,循环动作必须自然闭环,不要静止开场。".to_string()
|
||||
} else {
|
||||
"首帧严格使用图片1,尾帧严格使用图片2,中段完成完整动作变化,收束干净。".to_string()
|
||||
};
|
||||
|
||||
if let Some(template) = action_template_id.and_then(find_motion_template) {
|
||||
return [
|
||||
format!(
|
||||
"单人 NPC 全身动作视频,动作英文名是 {}。角色固定为图片1和图片2中的同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。",
|
||||
normalized_animation_name
|
||||
),
|
||||
"动作连贯,避免服装、发型、面部、武器随机漂移,不要多角色,不要镜头切换。".to_string(),
|
||||
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)
|
||||
},
|
||||
frame_rule,
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
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!(
|
||||
"单人 NPC 全身动作视频,动作英文名是 {}。",
|
||||
normalized_animation_name
|
||||
),
|
||||
"角色固定为图片1和图片2中的同一人,侧身朝右,镜头稳定,轮廓清晰,武器不可丢失。"
|
||||
.to_string(),
|
||||
"动作连贯,避免服装、发型、面部、武器随机漂移,不要多角色,不要镜头切换。".to_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()
|
||||
"背景为纯绿色绿幕,无其他人物和场景元素,方便后期抽帧与抠像。".to_string()
|
||||
} else {
|
||||
"背景简洁纯净,无复杂场景。".to_string()
|
||||
"背景简洁纯净,无其他人物和复杂场景元素,方便后期抽帧。".to_string()
|
||||
},
|
||||
if character_brief.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("角色设定:{}。", character_brief)
|
||||
},
|
||||
if action_detail_text.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("动作细节:{}。", action_detail_text)
|
||||
},
|
||||
frame_rule,
|
||||
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())
|
||||
|
||||
@@ -670,7 +670,7 @@ fn build_custom_world_role_outline_batch_prompt(
|
||||
};
|
||||
[
|
||||
format!("请根据下面的世界核心信息,生成一批{label}框架名单。"),
|
||||
"后续我会继续补全人物档案,所以这一步每个角色只保留最少字段。".to_string(),
|
||||
"后续我会继续补全人物档案,所以这一步每个角色只保留身份骨架与资产默认描述字段。".to_string(),
|
||||
"你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。".to_string(),
|
||||
"世界核心信息:".to_string(),
|
||||
build_framework_summary_text(framework, 0),
|
||||
@@ -684,6 +684,9 @@ fn build_custom_world_role_outline_batch_prompt(
|
||||
" \"title\": \"称号\",".to_string(),
|
||||
" \"role\": \"身份\",".to_string(),
|
||||
" \"description\": \"极简定位描述\",".to_string(),
|
||||
" \"visualDescription\": \"默认角色形象描述\",".to_string(),
|
||||
" \"actionDescription\": \"默认角色动作描述\",".to_string(),
|
||||
" \"sceneVisualDescription\": \"默认出现场景描述\",".to_string(),
|
||||
" \"initialAffinity\": 18,".to_string(),
|
||||
" \"relationshipHooks\": [\"一个关系切入口\"],".to_string(),
|
||||
" \"tags\": [\"标签1\", \"标签2\"]".to_string(),
|
||||
@@ -695,7 +698,10 @@ fn build_custom_world_role_outline_batch_prompt(
|
||||
format!("- 必须生成恰好 {batch_count} 个{label}。"),
|
||||
"- 这是一个完全独立的自定义世界;不要把角色写成来自“武侠世界”“仙侠世界”等现成世界。".to_string(),
|
||||
"- 名称必须具体且互不重复,不要使用 角色1、NPC1、场景角色1 之类的占位名。".to_string(),
|
||||
"- 只保留:name、title、role、description、initialAffinity、relationshipHooks、tags。".to_string(),
|
||||
"- 只保留:name、title、role、description、visualDescription、actionDescription、sceneVisualDescription、initialAffinity、relationshipHooks、tags。".to_string(),
|
||||
"- visualDescription 是打开角色形象图像生成面板时默认填入的角色形象描述,必须具体到体型、服装、轮廓与识别点,控制在 24 到 60 个汉字内。".to_string(),
|
||||
"- actionDescription 是打开每个角色动作视频生成面板时默认填入的动作描述,必须体现该角色默认动作节奏、武器或施法方式,控制在 18 到 48 个汉字内。".to_string(),
|
||||
"- sceneVisualDescription 是该角色常出现或关联的场景画面描述,会作为场景生图描述框的默认候选,控制在 24 到 60 个汉字内。".to_string(),
|
||||
"- relationshipHooks 最多 1 条;tags 保持 1 到 2 个。".to_string(),
|
||||
"- description 控制在 8 到 18 个汉字内,title 和 role 也尽量短。".to_string(),
|
||||
"- initialAffinity 必须是 -40 到 90 的整数。".to_string(),
|
||||
@@ -718,7 +724,7 @@ fn build_custom_world_role_outline_batch_json_repair_prompt(
|
||||
format!("顶层必须只包含一个 {key} 数组。"),
|
||||
format!("必须保留恰好 {expected_count} 个角色对象。"),
|
||||
if forbidden_names.is_empty() { "".to_string() } else { format!("禁止使用这些重复名:{}。", forbidden_names.join("、")) },
|
||||
"每个角色只包含:name、title、role、description、initialAffinity、relationshipHooks、tags。".to_string(),
|
||||
"每个角色只包含:name、title、role、description、visualDescription、actionDescription、sceneVisualDescription、initialAffinity、relationshipHooks、tags。".to_string(),
|
||||
"如果缺少字段:字符串补空字符串,relationshipHooks 和 tags 补空数组,initialAffinity 补默认整数。".to_string(),
|
||||
"不要输出 backstory、skills、landmarks 或任何其他字段。".to_string(),
|
||||
"原始文本:".to_string(),
|
||||
@@ -732,7 +738,7 @@ fn build_custom_world_landmark_seed_batch_prompt(
|
||||
) -> String {
|
||||
[
|
||||
"请根据下面的世界核心信息,生成一批关键场景框架名单。".to_string(),
|
||||
"后续我会继续补全场景网络,所以这一步每个地点只保留最少字段。".to_string(),
|
||||
"后续我会继续补全场景网络,所以这一步每个地点只保留场景骨架与默认生图描述。".to_string(),
|
||||
"你必须只输出一个能被 JSON.parse 直接解析的 JSON 对象,不要输出 Markdown、代码块、注释或解释。".to_string(),
|
||||
"世界核心信息:".to_string(),
|
||||
build_framework_summary_text(framework, 0),
|
||||
@@ -744,6 +750,7 @@ fn build_custom_world_landmark_seed_batch_prompt(
|
||||
" {".to_string(),
|
||||
" \"name\": \"场景名称\",".to_string(),
|
||||
" \"description\": \"场景极简描述\",".to_string(),
|
||||
" \"visualDescription\": \"默认场景生图描述\",".to_string(),
|
||||
" \"dangerLevel\": \"low|medium|high|extreme\"".to_string(),
|
||||
" }".to_string(),
|
||||
" ]".to_string(),
|
||||
@@ -753,7 +760,8 @@ fn build_custom_world_landmark_seed_batch_prompt(
|
||||
format!("- 必须生成恰好 {batch_count} 个关键场景。"),
|
||||
"- 这是一个完全独立的自定义世界;地点名称必须直接服务玩家输入主题。".to_string(),
|
||||
"- 名称必须具体且互不重复,不要使用 地点1、场景1 之类的占位名。".to_string(),
|
||||
"- 每个地点只保留:name、description、dangerLevel。".to_string(),
|
||||
"- 每个地点只保留:name、description、visualDescription、dangerLevel。".to_string(),
|
||||
"- visualDescription 是打开场景背景图像生成面板时默认填入的场景描述,必须具体到画面主体、远近景层次、地面可站立区域和氛围识别点,控制在 32 到 80 个汉字内。".to_string(),
|
||||
"- description 控制在 12 到 24 个汉字内。".to_string(),
|
||||
"- dangerLevel 只能是 low、medium、high、extreme 之一。".to_string(),
|
||||
"- 所有生成文本都必须使用中文。".to_string(),
|
||||
@@ -772,7 +780,7 @@ fn build_custom_world_landmark_seed_batch_json_repair_prompt(
|
||||
"顶层必须只包含一个 landmarks 数组。".to_string(),
|
||||
format!("必须保留恰好 {expected_count} 个地点对象。"),
|
||||
if forbidden_names.is_empty() { "".to_string() } else { format!("禁止使用这些重复名:{}。", forbidden_names.join("、")) },
|
||||
"每个地点只包含:name、description、dangerLevel。".to_string(),
|
||||
"每个地点只包含:name、description、visualDescription、dangerLevel。".to_string(),
|
||||
"如果缺少字段:字符串补空字符串,dangerLevel 补 medium。".to_string(),
|
||||
"不要输出 sceneNpcNames、connectedLandmarks、items 或任何其他字段。".to_string(),
|
||||
"原始文本:".to_string(),
|
||||
|
||||
@@ -217,6 +217,8 @@ impl AppState {
|
||||
self.spacetime_client
|
||||
.upsert_auth_store_snapshot(snapshot_json, updated_at_micros)
|
||||
.await?;
|
||||
// ?????????????????????????????????
|
||||
self.spacetime_client.import_auth_store_snapshot().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -229,19 +231,38 @@ impl AppState {
|
||||
token: config.spacetime_token.clone(),
|
||||
pool_size: config.spacetime_pool_size,
|
||||
});
|
||||
match spacetime_client
|
||||
.export_auth_store_snapshot_from_tables()
|
||||
.await
|
||||
{
|
||||
Ok(snapshot) => {
|
||||
if let Some(snapshot_json) = snapshot.snapshot_json {
|
||||
if !snapshot_json.trim().is_empty() {
|
||||
let auth_store = InMemoryAuthStore::from_snapshot_json(&snapshot_json)
|
||||
.map_err(AppStateInitError::AuthStore)?;
|
||||
info!("?? SpacetimeDB ???????????");
|
||||
return Self::new_with_auth_store(config, auth_store);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(error = %error, "? SpacetimeDB ????????????????");
|
||||
}
|
||||
}
|
||||
|
||||
match spacetime_client.get_auth_store_snapshot().await {
|
||||
Ok(snapshot) => {
|
||||
if let Some(snapshot_json) = snapshot.snapshot_json {
|
||||
if !snapshot_json.trim().is_empty() {
|
||||
let auth_store = InMemoryAuthStore::from_snapshot_json(&snapshot_json)
|
||||
.map_err(AppStateInitError::AuthStore)?;
|
||||
info!("已从 SpacetimeDB 恢复认证快照");
|
||||
info!("?? SpacetimeDB ???????????");
|
||||
return Self::new_with_auth_store(config, auth_store);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!(error = %error, "从 SpacetimeDB 恢复认证快照失败,回退到本地快照");
|
||||
warn!(error = %error, "? SpacetimeDB ?????????????????");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user