/// 拼图图片生成的默认反向提示词。 /// /// 这里单独收口拼图图片提示词,避免图片生成链路、候选图持久化和 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!( "请生成一张高清插画。", "画面主体:{prompt}。", "画面要求:1:1", "主体要清晰集中,前中后景层次明确,局部细节丰富但不要杂乱,", "避免文字、水印、边框和 UI 元素。" ), 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::() ) } #[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("1:1")); assert!(prompt.contains("主体要清晰集中")); 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("主体要清晰集中")); 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("文字水印")); } }