This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -0,0 +1,106 @@
/// 拼图图片生成的默认反向提示词。
///
/// 这里单独收口拼图图片提示词,避免图片生成链路、候选图持久化和 DashScope 请求编排
/// 混在同一个脚本里,后续调画风或资产约束时只需要改这一处。
pub(crate) const PUZZLE_DEFAULT_NEGATIVE_PROMPT: &str =
"低清晰度,低质量,文字水印,畸形构图,过度模糊,重复肢体,画面脏污";
/// wan2.2 / wan2.1 文生图旧协议的正向 prompt 上限。
///
/// 中文注释DashScope 旧 text2image 接口会把超长 prompt 判成请求参数不合法,
/// 所以这里先在拼图提示词模块内压缩,保证固定玩法约束不会被用户长描述挤掉。
pub(crate) const PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS: usize = 500;
const PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS: usize = 40;
const PUZZLE_IMAGE_PROMPT_FALLBACK: &str = "清晰、有辨识度的拼图画面";
/// 根据拼图关卡名和陶泥主输入构造最终发给图片模型的提示词。
pub(crate) fn build_puzzle_image_prompt(level_name: &str, prompt: &str) -> String {
let level_name =
truncate_puzzle_prompt_segment(level_name.trim(), PUZZLE_IMAGE_LEVEL_NAME_MAX_CHARS);
let prompt = prompt.trim();
let prompt = if prompt.is_empty() {
PUZZLE_IMAGE_PROMPT_FALLBACK
} else {
prompt
};
let template_chars = build_puzzle_image_prompt_text(level_name.as_str(), "")
.chars()
.count();
let prompt_max_chars = PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS.saturating_sub(template_chars);
let prompt = truncate_puzzle_prompt_segment(prompt, prompt_max_chars);
let image_prompt = build_puzzle_image_prompt_text(level_name.as_str(), prompt.as_str());
debug_assert!(
image_prompt.chars().count() <= PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS,
"puzzle image prompt should fit DashScope wan2.2 limit"
);
image_prompt
}
fn build_puzzle_image_prompt_text(level_name: &str, prompt: &str) -> String {
format!(
concat!(
"请生成一张高清插画。",
"关卡名:{level_name}。",
"画面主体:{prompt}。",
"画面要求1:1 正方形拼图关卡,适配 3x3 或 4x4 拼图切块,",
"主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,",
"避免文字、水印、边框和 UI 元素。"
),
level_name = level_name,
prompt = prompt,
)
}
fn truncate_puzzle_prompt_segment(value: &str, max_chars: usize) -> String {
if value.chars().count() <= max_chars {
return value.to_string();
}
const MARKER: &str = "...";
if max_chars <= MARKER.chars().count() {
return value.chars().take(max_chars).collect();
}
let keep_chars = max_chars - MARKER.chars().count();
format!(
"{}{MARKER}",
value.chars().take(keep_chars).collect::<String>()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_puzzle_image_prompt_keeps_puzzle_asset_constraints() {
let prompt = build_puzzle_image_prompt("雨夜神庙", "猫咪在发光遗迹前寻找线索");
assert!(prompt.contains("雨夜神庙"));
assert!(prompt.contains("猫咪在发光遗迹前寻找线索"));
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}
#[test]
fn build_puzzle_image_prompt_trims_long_user_description_for_wan22() {
let long_level_name = "雨夜神庙".repeat(20);
let long_description =
"发光遗迹、猫咪、漂浮碎片、雨水反光、远处灯塔、适合拼图切块。".repeat(50);
let prompt = build_puzzle_image_prompt(long_level_name.as_str(), long_description.as_str());
assert!(prompt.chars().count() <= PUZZLE_TEXT_TO_IMAGE_PROMPT_MAX_CHARS);
assert!(prompt.contains("1:1 正方形拼图关卡"));
assert!(prompt.contains("3x3 或 4x4"));
assert!(prompt.contains("避免文字、水印、边框和 UI 元素"));
}
#[test]
fn default_negative_prompt_blocks_text_and_low_quality_assets() {
assert!(PUZZLE_DEFAULT_NEGATIVE_PROMPT.contains("低清晰度"));
assert!(PUZZLE_DEFAULT_NEGATIVE_PROMPT.contains("文字水印"));
}
}