167 lines
6.8 KiB
Rust
167 lines
6.8 KiB
Rust
#[derive(Clone, Debug, Default)]
|
||
pub(crate) struct SceneImagePromptProfile<'a> {
|
||
pub name: &'a str,
|
||
pub subtitle: &'a str,
|
||
pub tone: &'a str,
|
||
pub player_goal: &'a str,
|
||
pub summary: &'a str,
|
||
pub setting_text: &'a str,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default)]
|
||
pub(crate) struct SceneImagePromptLandmark<'a> {
|
||
pub name: &'a str,
|
||
pub description: &'a str,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct SceneImagePromptParams<'a> {
|
||
pub profile: SceneImagePromptProfile<'a>,
|
||
pub landmark: SceneImagePromptLandmark<'a>,
|
||
pub user_prompt: &'a str,
|
||
pub has_reference_image: bool,
|
||
pub fallback_landmark_name: Option<&'a str>,
|
||
pub fallback_world_name: &'a str,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct SceneActBackgroundPromptParams<'a> {
|
||
pub world_name: &'a str,
|
||
pub world_tone: &'a str,
|
||
pub scene_name: &'a str,
|
||
pub title: &'a str,
|
||
pub summary: &'a str,
|
||
pub act_goal: &'a str,
|
||
pub transition_hook: &'a str,
|
||
pub primary_role_name: &'a str,
|
||
pub support_role_names: Vec<String>,
|
||
pub prompt_text: &'a str,
|
||
}
|
||
|
||
pub(crate) const DEFAULT_CUSTOM_WORLD_SCENE_IMAGE_NEGATIVE_PROMPT: &str = "文字,水印,logo,UI界面,对话框,边框,人物近景特写,多人合照,模糊,低清晰度,畸形建筑,现代车辆,监控摄像头";
|
||
|
||
pub(crate) fn build_custom_world_scene_image_prompt(params: SceneImagePromptParams<'_>) -> String {
|
||
let world_name = clamp_scene_image_text(
|
||
if params.profile.name.trim().is_empty() {
|
||
params.fallback_world_name
|
||
} else {
|
||
params.profile.name
|
||
},
|
||
18,
|
||
);
|
||
let world_subtitle = clamp_scene_image_text(params.profile.subtitle, 18);
|
||
let world_tone = clamp_scene_image_text(params.profile.tone, 48);
|
||
let world_goal = clamp_scene_image_text(params.profile.player_goal, 48);
|
||
let world_summary = clamp_scene_image_text(params.profile.summary, 72);
|
||
let world_setting = clamp_scene_image_text(params.profile.setting_text, 72);
|
||
let landmark_name = clamp_scene_image_text(
|
||
if params.landmark.name.trim().is_empty() {
|
||
params.fallback_landmark_name.unwrap_or("未命名场景")
|
||
} else {
|
||
params.landmark.name
|
||
},
|
||
18,
|
||
);
|
||
let landmark_description = clamp_scene_image_text(params.landmark.description, 96);
|
||
let requested_visual = clamp_scene_image_text(params.user_prompt, 120);
|
||
|
||
vec![
|
||
"为横版 16:9 2D RPG 生成高完成度像素风场景背景,适合作为剧情探索与战斗底图。".to_string(),
|
||
"画面构图必须严格按上下 1:1 分区:上半部分严格控制在整张图的 1/2 高度内,只描绘场景远景与中远景轮廓,不要让背景内容向下侵占超过半屏。".to_string(),
|
||
"下半部分严格占据整张图的 1/2 高度,用于玩家角色站位与展示,必须是模拟 3D 游戏视角的地面近景,有明确的透视延伸和近大远小关系,不是平铺的 2D 侧视地面。".to_string(),
|
||
"下半部分的内容必须是明确可站立的地面本体,例如道路、石板、平台、广场、甲板、沙地或草地,要有连续、稳定、可落脚的站位逻辑,不能只是装饰性前景、坑洞、障碍堆、栏杆带或不可通行的景物。".to_string(),
|
||
"下半部分地面近景要保持相对简洁、低细节、轮廓清楚、便于角色站立,不要堆满道具、植被、碎石、栏杆或复杂装饰。".to_string(),
|
||
if params.has_reference_image {
|
||
"已提供一张自定义参考图,请沿用其构图、镜头或氛围线索,同时继续满足本次场景需求。".to_string()
|
||
} else {
|
||
String::new()
|
||
},
|
||
format!(
|
||
"世界:{}{}。",
|
||
if world_name.is_empty() {
|
||
"未命名世界"
|
||
} else {
|
||
world_name.as_str()
|
||
},
|
||
if world_subtitle.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!(",{world_subtitle}")
|
||
}
|
||
),
|
||
conditional_prompt_line("玩家设定", world_setting.as_str()),
|
||
conditional_prompt_line("世界概述", world_summary.as_str()),
|
||
conditional_prompt_line("整体基调", world_tone.as_str()),
|
||
conditional_prompt_line("玩家目标关联", world_goal.as_str()),
|
||
format!(
|
||
"场景名称:{}。",
|
||
if landmark_name.is_empty() {
|
||
"未命名场景"
|
||
} else {
|
||
landmark_name.as_str()
|
||
}
|
||
),
|
||
conditional_prompt_line("场景描述", landmark_description.as_str()),
|
||
conditional_prompt_line("本次想要生成的画面内容", requested_visual.as_str()),
|
||
"不要出现 UI、字幕、文字、水印、logo 或装饰边框,人物仅可作为很小的远景剪影,画面重点放在场景本身,不要遮挡下半部分的角色展示区域。".to_string(),
|
||
]
|
||
.into_iter()
|
||
.filter(|line| !line.is_empty())
|
||
.collect::<Vec<_>>()
|
||
.join("")
|
||
}
|
||
|
||
pub(crate) fn build_scene_act_background_image_prompt(
|
||
params: SceneActBackgroundPromptParams<'_>,
|
||
) -> String {
|
||
// 幕背景图不是普通地点图,必须把世界、幕目标、过渡钩子和角色关系一起写入图像提示词,
|
||
// 同时明确禁止角色立绘和 UI 元素进入背景资产。
|
||
[
|
||
format!("这是世界《{}》中的场景幕背景图。", params.world_name),
|
||
format!("场景:{}", params.scene_name),
|
||
format!("幕标题:{}", params.title),
|
||
format!("幕摘要:{}", params.summary),
|
||
format!("幕目标:{}", params.act_goal),
|
||
format!("过渡钩子:{}", params.transition_hook),
|
||
format!(
|
||
"主角色:{}",
|
||
if params.primary_role_name.trim().is_empty() {
|
||
"待补主角色"
|
||
} else {
|
||
params.primary_role_name.trim()
|
||
}
|
||
),
|
||
if params.support_role_names.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("辅助角色:{}", params.support_role_names.join("、"))
|
||
},
|
||
format!("世界气质:{}", params.world_tone),
|
||
format!("背景描述:{}", params.prompt_text),
|
||
"要求:只生成环境背景,不出现角色立绘、站位 UI、对白框、按钮或文字。".to_string(),
|
||
]
|
||
.into_iter()
|
||
.filter(|line| !line.trim().is_empty())
|
||
.collect::<Vec<_>>()
|
||
.join("\n")
|
||
}
|
||
|
||
fn clamp_scene_image_text(value: &str, max_length: usize) -> String {
|
||
value
|
||
.trim()
|
||
.replace(char::is_whitespace, " ")
|
||
.chars()
|
||
.take(max_length)
|
||
.collect::<String>()
|
||
.trim()
|
||
.to_string()
|
||
}
|
||
|
||
fn conditional_prompt_line(prefix: &str, value: &str) -> String {
|
||
if value.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("{prefix}:{value}。")
|
||
}
|
||
}
|