Merge remote-tracking branch 'origin/codex/wooden-fish-template'

This commit is contained in:
kdletters
2026-05-22 08:09:58 +08:00
617 changed files with 31612 additions and 237 deletions

View File

@@ -1539,111 +1539,11 @@ pub(super) fn slice_match3d_material_sheet(
image: &DownloadedOpenAiImage,
item_names: &[String],
) -> Result<Vec<Vec<Match3DSlicedItemImage>>, AppError> {
// 中文注释:素材图提示词固定要求 5*5 均匀排布;切图也固定按 5 行 5 列定位格子。
// 每个格子内再基于前景像素二次校准,避免固定内缩裁断物品边缘。
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "match3d-assets",
"message": format!("抓大鹅素材图解码失败:{error}"),
}))
})?;
// 中文注释:素材图按绿幕背景生成;先把整张 sheet 的绿幕转成 alpha再进入格子裁切。
let source = apply_match3d_material_green_screen_alpha(source);
let (width, height) = source.dimensions();
let row_count = MATCH3D_MATERIAL_GRID_SIZE;
let cell_width = width / MATCH3D_MATERIAL_GRID_SIZE;
let cell_height = height / row_count;
if cell_width == 0 || cell_height == 0 {
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "match3d-assets",
"message": "抓大鹅素材图尺寸过小,无法切割",
})),
);
}
let mut slices = Vec::with_capacity(item_names.len());
for item_index in 0..item_names.len().min(MATCH3D_MATERIAL_ITEM_BATCH_SIZE) {
let row = item_index as u32;
let mut views = Vec::with_capacity(MATCH3D_ITEM_VIEW_COUNT);
for view_index in 0..MATCH3D_ITEM_VIEW_COUNT {
let col = view_index as u32;
let (crop_x, crop_y, crop_width, crop_height) =
resolve_match3d_material_cell_crop(&source, row_count, row, col);
let cropped = source.crop_imm(crop_x, crop_y, crop_width, crop_height);
let cleaned = crop_match3d_material_view_edge_matte(cropped);
let mut cursor = std::io::Cursor::new(Vec::new());
cleaned
.write_to(&mut cursor, ImageFormat::Png)
.map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "match3d-assets",
"message": format!("抓大鹅素材图切割失败:{error}"),
}))
})?;
views.push(Match3DSlicedItemImage {
bytes: cursor.into_inner(),
});
}
slices.push(views);
}
Ok(slices)
}
fn resolve_match3d_material_cell_crop(
source: &image::DynamicImage,
row_count: u32,
row: u32,
col: u32,
) -> (u32, u32, u32, u32) {
let (image_width, image_height) = source.dimensions();
let cell = resolve_match3d_material_cell_bounds(image_width, image_height, row_count, row, col);
let Some(foreground) = detect_match3d_material_foreground_bounds(source, cell) else {
return cell.to_crop_tuple();
};
let cell_width = cell.width();
let cell_height = cell.height();
let pad_x = (cell_width / 16).clamp(4, 16);
let pad_y = (cell_height / 16).clamp(4, 16);
let crop = Match3DMaterialCellBounds {
x0: foreground.x0.saturating_sub(pad_x).max(cell.x0),
y0: foreground.y0.saturating_sub(pad_y).max(cell.y0),
x1: foreground.x1.saturating_add(pad_x).min(cell.x1),
y1: foreground.y1.saturating_add(pad_y).min(cell.y1),
};
crop.to_crop_tuple()
}
pub(super) fn crop_match3d_material_view_edge_matte(
image: image::DynamicImage,
) -> image::DynamicImage {
let mut image = image.to_rgba8();
let (width, height) = image.dimensions();
remove_match3d_material_view_edge_matte(image.as_mut(), width as usize, height as usize);
let bounds = detect_match3d_material_visible_bounds(&image).unwrap_or_else(|| {
Match3DMaterialCellBounds {
x0: 0,
y0: 0,
x1: width,
y1: height,
}
});
if bounds.x0 == 0 && bounds.y0 == 0 && bounds.x1 == width && bounds.y1 == height {
return image::DynamicImage::ImageRgba8(image);
}
image::DynamicImage::ImageRgba8(
image::imageops::crop_imm(
&image,
bounds.x0,
bounds.y0,
bounds.width(),
bounds.height(),
)
.to_image(),
slice_generated_asset_sheet_two_items_per_row(
image,
item_names,
MATCH3D_MATERIAL_GRID_SIZE as usize,
MATCH3D_ITEM_VIEW_COUNT,
)
.map(|rows| {
rows.into_iter()

View File

@@ -716,6 +716,40 @@ pub(super) fn load_match3d_container_reference_image() -> Result<OpenAiReference
})
}
pub(super) fn build_match3d_level_scene_generation_prompt(config: &Match3DConfigJson) -> String {
let theme = config.theme_text.trim();
let theme = if theme.is_empty() {
MATCH3D_DEFAULT_THEME
} else {
theme
};
let style_clause = resolve_match3d_asset_style_prompt(config)
.map(|style| format!("\n整体美术风格要求:{style}"))
.unwrap_or_default();
format!(
concat!(
"生成抓大鹅游戏关卡画面要求画面中所有元素精致且风格高度一致画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n",
"抓大鹅主题描述:\n",
"{theme}{style_clause}\n\n",
"画面元素:\n",
"返回按钮位于顶部左上角顶部中间显示关卡标题“第1关 重庆火锅”和倒计时时间,右上角显示设置按钮\n",
"画面中间是一个和主题匹配的容器,宽度与画面宽度同宽,紧贴画面横向边缘\n",
"底部还有三个道具按钮分别为“移出”、“凑齐”、“打乱”"
),
theme = theme,
style_clause = style_clause,
)
}
pub(super) fn build_match3d_ui_spritesheet_prompt() -> String {
"提取画面中的UI元素将返回按钮、设置按钮、方格素材不含边框仅保留一个、移出按钮、凑齐按钮、打乱按钮的顺序从上到下从左到右整理成纯绿色绿幕背景spritesheet。背景必须是统一纯绿色绿幕高饱和亮绿色接近 #00FF00背景平整无纹理、无渐变、无阴影、无场景内容后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。UI 素材自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。".to_string()
}
pub(super) fn build_match3d_background_from_scene_prompt() -> String {
"移除画面中的所有UI组件和容器中的内含物完整保留容器和背景补全被UI覆盖的背景内容".to_string()
}
pub(super) fn build_match3d_background_generation_prompt(
config: &Match3DConfigJson,
prompt: &str,