Files
Genarrative/server-rs/crates/api-server/src/prompt/puzzle/image.rs
2026-05-14 14:21:17 +08:00

104 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 拼图图片生成的默认反向提示词。
///
/// 这里单独收口拼图图片提示词,避免图片生成链路、候选图持久化和 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("文字水印"));
}
}