104 lines
4.1 KiB
Rust
104 lines
4.1 KiB
Rust
/// 拼图图片生成的默认反向提示词。
|
||
///
|
||
/// 这里单独收口拼图图片提示词,避免图片生成链路、候选图持久化和 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::<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("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("文字水印"));
|
||
}
|
||
}
|