Sheet04/Sheet05 改为横版 1536x1024 + 多画少取策略

- Sheet04: 竖版 4x6 → 横版 6x4,3组粗切 → 4组多画少取取3组,消除右列歧义

- Sheet05: 竖版 4x6 → 横版 6x4,6组 → 8组多画少取取3组,消除右列歧义

- PuzzleClearAtlasSheetSpec: layout 从固定数组改为 Vec<Vec>,新增 cols/rows,移除 Copy

- build_puzzle_clear_atlas_prompt: 画布尺寸动态计算(orientation w×h)

- 生成函数: per-sheet 独立 generation_size 传入 API

- slice_puzzle_clear_sheet: detect_cell_grid_seed 改用 sheet 自身 rows/cols

- 提示词全部统一为 x行y列 表述

- 移除未使用的 PUZZLE_CLEAR_SHEET_*_USIZE 常量

- puzzle_clear_full_pipeline 测试适配 per-sheet 维度
This commit is contained in:
2026-06-16 16:37:10 +08:00
parent 5795115c20
commit c827dc2d18

View File

@@ -56,8 +56,6 @@ const PUZZLE_CLEAR_RUNTIME_RUNS_ROUTE: &str = "/api/runtime/puzzle-clear/runs";
const PUZZLE_CLEAR_ATLAS_CELL_SIZE: u32 = 256;
const PUZZLE_CLEAR_SHEET_COLUMNS: u32 = 4;
const PUZZLE_CLEAR_SHEET_ROWS: u32 = 6;
const PUZZLE_CLEAR_SHEET_COLUMNS_USIZE: usize = 4;
const PUZZLE_CLEAR_SHEET_ROWS_USIZE: usize = 6;
const PUZZLE_CLEAR_FINAL_ATLAS_COLUMNS: u32 = 10;
const PUZZLE_CLEAR_FINAL_ATLAS_ROWS: u32 = 10;
const PUZZLE_CLEAR_SHEET_UNUSED_CELL: &str = ".";
@@ -577,11 +575,13 @@ struct PuzzleClearAtlasCardSlice {
bytes: Vec<u8>,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
struct PuzzleClearAtlasSheetSpec {
sheet_id: &'static str,
layout: [[&'static str; PUZZLE_CLEAR_SHEET_COLUMNS_USIZE]; PUZZLE_CLEAR_SHEET_ROWS_USIZE],
layout: Vec<Vec<&'static str>>,
layout_prompt: &'static str,
cols: u32,
rows: u32,
}
#[derive(Clone, Debug)]
@@ -1196,6 +1196,11 @@ async fn maybe_prepare_puzzle_clear_assets_inner(
.iter()
.map(|sheet_spec| {
let sheet_prompt = build_puzzle_clear_atlas_prompt(theme_prompt, sheet_spec);
let generation_size = format!(
"{}x{}",
sheet_spec.cols * PUZZLE_CLEAR_ATLAS_CELL_SIZE,
sheet_spec.rows * PUZZLE_CLEAR_ATLAS_CELL_SIZE,
);
let client = http_client.clone();
let settings = settings.clone();
let debug_run = image_debug_run.clone();
@@ -1218,7 +1223,7 @@ async fn maybe_prepare_puzzle_clear_assets_inner(
&settings,
sheet_prompt.as_str(),
Some(PUZZLE_CLEAR_ATLAS_NEGATIVE_PROMPT),
PUZZLE_CLEAR_ATLAS_GENERATION_SIZE,
&generation_size,
1,
&[],
failure_context.as_str(),
@@ -1274,7 +1279,7 @@ async fn maybe_prepare_puzzle_clear_assets_inner(
);
}
return Ok(PuzzleClearGeneratedSheet {
spec: *sheet_spec,
spec: sheet_spec.clone(),
prompt: sheet_prompt.clone(),
task_id,
image,
@@ -1453,13 +1458,15 @@ fn puzzle_clear_atlas_sheet_specs() -> Vec<PuzzleClearAtlasSheetSpec> {
// Sheet 1: 1×2 横向 12 组, 满画布 24cells
PuzzleClearAtlasSheetSpec {
sheet_id: "sheet-01",
layout: [
["A01","A01", "A03","A03"],
["A05","A05", "A07","A07"],
["A09","A09", "A11","A11"],
["A13","A13", "A15","A15"],
["A17","A17", "A19","A19"],
["A21","A21", "A23","A23"],
cols: 4,
rows: 6,
layout: vec![
vec!["A01","A01", "A03","A03"],
vec!["A05","A05", "A07","A07"],
vec!["A09","A09", "A11","A11"],
vec!["A13","A13", "A15","A15"],
vec!["A17","A17", "A19","A19"],
vec!["A21","A21", "A23","A23"],
],
layout_prompt: concat!(
"画面等分为 6 行 4 列,每格 256×256。",
@@ -1472,17 +1479,19 @@ fn puzzle_clear_atlas_sheet_specs() -> Vec<PuzzleClearAtlasSheetSpec> {
// Sheet 2: 1×2 纵向 12 组 (取11), 满画布 24cells
PuzzleClearAtlasSheetSpec {
sheet_id: "sheet-02",
layout: [
["A02","A04", "A06","A08"],
["A02","A04", "A06","A08"],
["A10","A12", "A14","A16"],
["A10","A12", "A14","A16"],
["A18","A20", "A22",buf],
["A18","A20", "A22",buf],
cols: 4,
rows: 6,
layout: vec![
vec!["A02","A04", "A06","A08"],
vec!["A02","A04", "A06","A08"],
vec!["A10","A12", "A14","A16"],
vec!["A10","A12", "A14","A16"],
vec!["A18","A20", "A22",buf],
vec!["A18","A20", "A22",buf],
],
layout_prompt: concat!(
"画面等分为 6 行 4 列,每格 256×256各行高度严格相等。",
"共 12 个纵向裁片,每行排 4 个,两两成组上下排列,每裁片严格占 1 列 2 行。",
"共 12 个纵向裁片,每行排 4 个,两两成组上下排列,每裁片严格占 2 行 1 列",
"裁片内部画面连续无间断,不可拆成上下两格独立小图。",
"不同裁片之间用洋红细线分隔。画面四周留洋红边距。",
"绝对不要画网格线、边框、编号、文字或水印。",
@@ -1491,55 +1500,57 @@ fn puzzle_clear_atlas_sheet_specs() -> Vec<PuzzleClearAtlasSheetSpec> {
// Sheet 3: 2×2 正方形 6 组 (取4), 满画布 24cells
PuzzleClearAtlasSheetSpec {
sheet_id: "sheet-03",
layout: [
["C01","C01", "C02","C02"],
["C01","C01", "C02","C02"],
["C03","C03", "C04","C04"],
["C03","C03", "C04","C04"],
[buf,buf, buf,buf],
[buf,buf, buf,buf],
cols: 4,
rows: 6,
layout: vec![
vec!["C01","C01", "C02","C02"],
vec!["C01","C01", "C02","C02"],
vec!["C03","C03", "C04","C04"],
vec!["C03","C03", "C04","C04"],
vec![buf,buf, buf,buf],
vec![buf,buf, buf,buf],
],
layout_prompt: concat!(
"画面等分为 6 行 4 列,每格 256×256各行高度严格相等。",
"共 6 个正方形裁片,每行左右各一个,每裁片严格占 2 2 ",
"共 6 个正方形裁片,每行左右各一个,每裁片严格占 2 2 ",
"裁片内部画面连续无间断,不可拆成四格独立小图。",
"不同裁片之间用洋红细线分隔。画面四周留洋红边距。",
"绝对不要画网格线、边框、编号、文字或水印。",
),
},
// Sheet 4: 2×3 横向 3, 满画布 + 右列缓冲
// Sheet 4: 2×3 横向 4 (取3), 横版满画布 6×4=24cells
PuzzleClearAtlasSheetSpec {
sheet_id: "sheet-04",
layout: [
["D01","D01","D01",buf],
["D01","D01","D01",buf],
["D02","D02","D02",buf],
["D02","D02","D02",buf],
["D03","D03","D03",buf],
["D03","D03","D03",buf],
cols: 6,
rows: 4,
layout: vec![
vec!["D01","D01","D01", "D02","D02","D02"],
vec!["D01","D01","D01", "D02","D02","D02"],
vec!["D03","D03","D03", buf,buf,buf],
vec!["D03","D03","D03", buf,buf,buf],
],
layout_prompt: concat!(
"画面等分为 64 列,每格 256×256行高度严格相等。",
"3 个横向宽裁片,每行一个,每裁片严格占 3 列 2 行(宽 3 格高 2 格)。",
"画面等分为 46 列,每格 256×256列宽度严格相等。",
"4 个横向宽裁片,每行左右各一个,每裁片严格占 2 行 3 列(宽 3 格高 2 格)。",
"裁片内部画面连续无间断,不可拆成独立格子。",
"不同裁片之间用洋红细线分隔。画面四周留洋红边距。",
"绝对不要画网格线、边框、编号、文字或水印。",
),
},
// Sheet 5: 1×3 横向 6 组 (取3), 满画布 18cells + 右列缓冲
// Sheet 5: 1×3 横向 8 组 (取3), 横版满画布 6×4=24cells
PuzzleClearAtlasSheetSpec {
sheet_id: "sheet-05",
layout: [
["B01","B01","B01",buf],
["B03","B03","B03",buf],
["B05","B05","B05",buf],
[buf,buf,buf,buf],
[buf,buf,buf,buf],
[buf,buf,buf,buf],
cols: 6,
rows: 4,
layout: vec![
vec!["B01","B01","B01", "B03","B03","B03"],
vec!["B05","B05","B05", buf,buf,buf],
vec![buf,buf,buf, buf,buf,buf],
vec![buf,buf,buf, buf,buf,buf],
],
layout_prompt: concat!(
"画面等分为 64 列,每格 256×256行高度严格相等。",
"6 个横向宽裁片,每行一个,每裁片严格占 3 列 1 行(宽 3 格高 1 格)。",
"画面等分为 46 列,每格 256×256列宽度严格相等。",
"8 个横向宽裁片,每行左右各一个,每裁片严格占 1 行 3 列(宽 3 格高 1 格)。",
"裁片内部画面连续无间断,不可拆成三格独立小图。",
"不同裁片之间用洋红细线分隔。画面四周留洋红边距。",
"绝对不要画网格线、边框、编号、文字或水印。",
@@ -1548,17 +1559,19 @@ fn puzzle_clear_atlas_sheet_specs() -> Vec<PuzzleClearAtlasSheetSpec> {
// Sheet 6: 1×3 纵向 8 组 (取2), 满画布 24cells
PuzzleClearAtlasSheetSpec {
sheet_id: "sheet-06",
layout: [
["B02",buf,buf,buf],
["B02",buf,buf,buf],
["B02",buf,buf,buf],
["B04",buf,buf,buf],
["B04",buf,buf,buf],
["B04",buf,buf,buf],
cols: 4,
rows: 6,
layout: vec![
vec!["B02",buf,buf,buf],
vec!["B02",buf,buf,buf],
vec!["B02",buf,buf,buf],
vec!["B04",buf,buf,buf],
vec!["B04",buf,buf,buf],
vec!["B04",buf,buf,buf],
],
layout_prompt: concat!(
"画面等分为 6 行 4 列,每格 256×256各列宽度严格相等。",
"共 8 个纵向裁片,每列排 2 组上下相邻,每裁片严格占 1 列 3 行(宽 1 格高 3 格)。",
"共 8 个纵向裁片,每列排 2 组上下相邻,每裁片严格占 3 行 1 列(宽 1 格高 3 格)。",
"裁片内部画面连续无间断,不可拆成三格独立小图。",
"不同裁片之间用洋红细线分隔。画面四周留洋红边距。",
"绝对不要画网格线、边框、编号、文字或水印。",
@@ -1572,16 +1585,20 @@ fn build_puzzle_clear_atlas_prompt(
sheet_spec: &PuzzleClearAtlasSheetSpec,
) -> String {
let subject = normalize_non_empty_str(theme_prompt).unwrap_or_else(|| "梦幻物件".to_string());
let w = sheet_spec.cols * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
let h = sheet_spec.rows * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
let orientation = if h > w { "竖版" } else { "横版" };
format!(
concat!(
"生成一张拼消消卡牌图集,主题是「{subject}」,竖版 1024x1536。\n",
"照片式构图、绘本式渲染。画面由若干场景裁片组成,",
"每个裁片是完整连续画面,内部无接缝、无分隔线、无网格。",
"不同裁片之间用纯洋红(#FF00FF)细线分隔。",
"不要文字、Logo、水印、UI、网格线、边框、编号、纯色背景或孤立主体。\n",
"{layout_prompt}"
),
"生成一张拼消消卡牌图集,主题是「{subject}」,{orientation} {w}x{h}\n\
照片式构图、绘本式渲染。画面由若干场景裁片组成,\n\
每个裁片是完整连续画面,内部无接缝、无分隔线、无网格。\n\
不同裁片之间用纯洋红(#FF00FF)细线分隔。\n\
不要文字、Logo、水印、UI、网格线、边框、编号、纯色背景或孤立主体。\n\
{layout_prompt}",
subject = subject,
orientation = orientation,
w = w,
h = h,
layout_prompt = sheet_spec.layout_prompt
)
}
@@ -1791,8 +1808,8 @@ fn slice_puzzle_clear_sheet(
source_rgba.as_raw(),
source_width,
source_height,
PUZZLE_CLEAR_SHEET_ROWS,
PUZZLE_CLEAR_SHEET_COLUMNS,
sheet_spec.rows,
sheet_spec.cols,
);
let mut slices = Vec::new();
let mut cells_by_group: BTreeMap<&str, Vec<(u32, u32)>> = BTreeMap::new();
@@ -2585,7 +2602,6 @@ mod tests {
fn puzzle_clear_full_pipeline_saves_intermediate_results() {
use super::{
compose_puzzle_clear_final_atlas, slice_puzzle_clear_sheet, PUZZLE_CLEAR_ATLAS_CELL_SIZE,
PUZZLE_CLEAR_SHEET_COLUMNS, PUZZLE_CLEAR_SHEET_ROWS,
PUZZLE_CLEAR_FINAL_ATLAS_COLUMNS, PUZZLE_CLEAR_FINAL_ATLAS_ROWS,
};
use crate::openai_image_generation::DownloadedOpenAiImage;
@@ -2609,16 +2625,15 @@ mod tests {
.map(|g| (g.group_id.clone(), g))
.collect();
let w = PUZZLE_CLEAR_SHEET_COLUMNS * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
let h = PUZZLE_CLEAR_SHEET_ROWS * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
let mut all_slices = Vec::new();
for sheet in &sheets {
// 中文注释:为每张 sheet 生成合成测试图,按 group_id 分配颜色
// 中文注释:为每张 sheet 生成合成测试图,按 sheet 自身 cols/rows 分配颜色
let w = sheet.cols * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
let h = sheet.rows * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
let mut img = RgbaImage::from_pixel(w, h, Rgba([248, 246, 240, 255]));
for row in 0..PUZZLE_CLEAR_SHEET_ROWS {
for col in 0..PUZZLE_CLEAR_SHEET_COLUMNS {
for row in 0..sheet.rows {
for col in 0..sheet.cols {
let gid = sheet.layout[row as usize][col as usize];
let (r, g, b) = group_color(gid);
let x0 = col * PUZZLE_CLEAR_ATLAS_CELL_SIZE;
@@ -2717,7 +2732,7 @@ mod tests {
use super::{
compose_puzzle_clear_final_atlas, slice_puzzle_clear_sheet,
build_puzzle_clear_atlas_prompt, prepare_puzzle_clear_magenta_cleanup,
PUZZLE_CLEAR_ATLAS_NEGATIVE_PROMPT, PUZZLE_CLEAR_ATLAS_GENERATION_SIZE,
PUZZLE_CLEAR_ATLAS_NEGATIVE_PROMPT,
};
use crate::openai_image_generation::DownloadedOpenAiImage;
use platform_image::{
@@ -2762,7 +2777,6 @@ mod tests {
let theme = "梦幻幻想";
let neg = PUZZLE_CLEAR_ATLAS_NEGATIVE_PROMPT;
let size = PUZZLE_CLEAR_ATLAS_GENERATION_SIZE;
// 中文注释5 张 sheet 并行生成
println!("开始并行生成 {} 张 sheet...", sheets.len());
@@ -2770,13 +2784,18 @@ mod tests {
.iter()
.map(|sheet| {
let prompt = build_puzzle_clear_atlas_prompt(theme, sheet);
let size = format!(
"{}x{}",
sheet.cols * PUZZLE_CLEAR_ATLAS_CELL_SIZE,
sheet.rows * PUZZLE_CLEAR_ATLAS_CELL_SIZE,
);
let failure = format!("拼消消测试 {} 生成", sheet.sheet_id);
let client = http_client.clone();
let settings = settings.clone();
println!(" -> {} prompt={}chars", sheet.sheet_id, prompt.len());
async move {
let result = create_vector_engine_image_generation(
&client, &settings, &prompt, Some(neg), size, 1, &[], &failure,
&client, &settings, &prompt, Some(neg), &size, 1, &[], &failure,
)
.await
.unwrap_or_else(|e| panic!("{} failed: {:?}", sheet.sheet_id, e));