diff --git a/server-rs/crates/api-server/src/puzzle_clear.rs b/server-rs/crates/api-server/src/puzzle_clear.rs index e4be25cc..3bf984f1 100644 --- a/server-rs/crates/api-server/src/puzzle_clear.rs +++ b/server-rs/crates/api-server/src/puzzle_clear.rs @@ -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, } -#[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>, 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 { // 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 { // 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 { // 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!( - "画面等分为 6 行 4 列,每格 256×256,各行高度严格相等。", - "共 3 个横向宽裁片,每两行一个,每裁片严格占 3 列 2 行(宽 3 格高 2 格)。", + "画面等分为 4 行 6 列,每格 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!( - "画面等分为 6 行 4 列,每格 256×256,各行高度严格相等。", - "共 6 个横向宽裁片,每行一个,每裁片严格占 3 列 1 行(宽 3 格高 1 格)。", + "画面等分为 4 行 6 列,每格 256×256,各列宽度严格相等。", + "共 8 个横向宽裁片,每行左右各一个,每裁片严格占 1 行 3 列(宽 3 格高 1 格)。", "裁片内部画面连续无间断,不可拆成三格独立小图。", "不同裁片之间用洋红细线分隔。画面四周留洋红边距。", "绝对不要画网格线、边框、编号、文字或水印。", @@ -1548,17 +1559,19 @@ fn puzzle_clear_atlas_sheet_specs() -> Vec { // 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));