Switch to VectorEngine gpt-image-2 and edits

Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
2026-05-22 03:06:41 +08:00
parent 321e1ea33a
commit ae014ac881
90 changed files with 7078 additions and 3389 deletions

View File

@@ -197,6 +197,86 @@ pub(crate) fn slice_generated_asset_sheet(
Ok(slices)
}
pub(crate) fn slice_generated_asset_sheet_two_items_per_row(
image: &DownloadedOpenAiImage,
item_names: &[String],
grid_size: usize,
views_per_item: usize,
) -> Result<Vec<Vec<GeneratedAssetSheetSliceImage>>, AppError> {
if grid_size == 0 || views_per_item == 0 {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": GENERATED_ASSET_SHEET_PROVIDER,
"message": "系列素材图集的 n 和每物品视图数必须大于 0。",
})),
);
}
if !grid_size.is_multiple_of(views_per_item) {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": GENERATED_ASSET_SHEET_PROVIDER,
"message": "系列素材图集每行必须能均分为若干物品。",
"gridSize": grid_size,
"viewsPerItem": views_per_item,
})),
);
}
let grid_size_u32 = u32::try_from(grid_size).map_err(|_| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": GENERATED_ASSET_SHEET_PROVIDER,
"message": "系列素材图集的 n 超出可支持范围。",
}))
})?;
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": GENERATED_ASSET_SHEET_PROVIDER,
"message": format!("系列素材图集解码失败:{error}"),
}))
})?;
let source = apply_generated_asset_sheet_green_screen_alpha(source);
let (width, height) = source.dimensions();
if width / grid_size_u32 == 0 || height / grid_size_u32 == 0 {
return Err(
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": GENERATED_ASSET_SHEET_PROVIDER,
"message": "系列素材图集尺寸过小,无法切割。",
})),
);
}
let items_per_row = grid_size / views_per_item;
let max_item_count = grid_size.saturating_mul(items_per_row);
let mut slices = Vec::with_capacity(item_names.len().min(max_item_count));
for item_index in 0..item_names.len().min(max_item_count) {
let row = (item_index / items_per_row) as u32;
let start_col = ((item_index % items_per_row) * views_per_item) as u32;
let mut views = Vec::with_capacity(views_per_item);
for view_offset in 0..views_per_item {
let col = start_col + view_offset as u32;
let (crop_x, crop_y, crop_width, crop_height) =
resolve_generated_asset_sheet_cell_crop(&source, grid_size_u32, row, col);
let cropped = source.crop_imm(crop_x, crop_y, crop_width, crop_height);
let cleaned = crop_generated_asset_sheet_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": GENERATED_ASSET_SHEET_PROVIDER,
"message": format!("系列素材图集切割失败:{error}"),
}))
})?;
views.push(GeneratedAssetSheetSliceImage {
bytes: cursor.into_inner(),
});
}
slices.push(views);
}
Ok(slices)
}
pub(crate) fn crop_generated_asset_sheet_view_edge_matte(
image: image::DynamicImage,
) -> image::DynamicImage {
@@ -958,7 +1038,7 @@ fn collect_generated_asset_sheet_visible_neighbor_color(
))
}
fn apply_generated_asset_sheet_green_screen_alpha(
pub(crate) fn apply_generated_asset_sheet_green_screen_alpha(
source: image::DynamicImage,
) -> image::DynamicImage {
let mut image = source.to_rgba8();