From d05440dcca209ed18f8297ebfd1b878398e20fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 12 Sep 2026 11:13:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BA=20Raw=20=E7=BC=96=E8=BE=91=E5=BB=BA?= =?UTF-8?q?=E7=AB=8B=E7=8B=AC=E7=AB=8B=E5=AD=97=E8=8A=82=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 恢复旧图片链路的 Vec 字节契约 让新增 raw_edit 输入和结果类型独立于既有 ReferenceImage --- .../api-server/src/editor_project_icon.rs | 4 +- server-rs/crates/api-server/src/raw_image.rs | 11 ++++-- .../src/generated_asset_sheets/sheet.rs | 28 +++++++------- server-rs/crates/platform-image/src/lib.rs | 6 +-- .../platform-image/src/pixel_art_snapper.rs | 6 +-- .../platform-image/src/vector_engine/mod.rs | 13 +++---- .../src/vector_engine/raw_edit.rs | 37 ++++++++++++++----- .../tests/generated_asset_sheets.rs | 2 +- .../platform-image/tests/vector_engine.rs | 4 +- 9 files changed, 65 insertions(+), 46 deletions(-) diff --git a/server-rs/crates/api-server/src/editor_project_icon.rs b/server-rs/crates/api-server/src/editor_project_icon.rs index 39deb0bd7..f7f318b86 100644 --- a/server-rs/crates/api-server/src/editor_project_icon.rs +++ b/server-rs/crates/api-server/src/editor_project_icon.rs @@ -2653,7 +2653,7 @@ pub(crate) fn editor_icon_spritesheet_warning_after_persist_error( } fn validate_editor_icon_spritesheet_source(source: &DownloadedImage) -> Result<(), AppError> { - let reader = image::ImageReader::new(Cursor::new(source.bytes.as_ref())) + let reader = image::ImageReader::new(Cursor::new(source.bytes.as_slice())) .with_guessed_format() .map_err(|error| { AppError::from_status(StatusCode::UNPROCESSABLE_ENTITY).with_details(json!({ @@ -2931,7 +2931,7 @@ mod tests { .write_to(&mut std::io::Cursor::new(&mut bytes), ImageFormat::Png) .expect("fixture png should encode"); let source = DownloadedImage { - bytes: bytes.into(), + bytes, mime_type: "image/png".to_string(), extension: "png".to_string(), }; diff --git a/server-rs/crates/api-server/src/raw_image.rs b/server-rs/crates/api-server/src/raw_image.rs index 6d549c4c0..1af3d4c91 100644 --- a/server-rs/crates/api-server/src/raw_image.rs +++ b/server-rs/crates/api-server/src/raw_image.rs @@ -7,7 +7,7 @@ use bytes::Bytes; use image::{GenericImageView, ImageFormat, ImageReader}; use platform_image::{ GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, - RawImageEditOptions, ReferenceImage, create_vector_engine_raw_image_edit, + RawImageEditImage, RawImageEditOptions, create_vector_engine_raw_image_edit, validate_raw_image_edit_dimensions, }; use serde::Serialize; @@ -162,7 +162,7 @@ pub(crate) async fn edit_raw_image( } struct PreparedRawImageEdit { - image: ReferenceImage, + image: RawImageEditImage, prompt: String, options: RawImageEditOptions, width: u32, @@ -367,7 +367,10 @@ fn validate_optional_value( Err(bad_request(format!("{field} 值无效"))) } -fn decode_image(value: RawImageData, field: &str) -> Result<(ReferenceImage, u32, u32), AppError> { +fn decode_image( + value: RawImageData, + field: &str, +) -> Result<(RawImageEditImage, u32, u32), AppError> { let mime_type = value.mime_type.trim().to_string(); if !mime_type.eq_ignore_ascii_case("image/png") { return Err(bad_request(format!( @@ -405,7 +408,7 @@ fn decode_image(value: RawImageData, field: &str) -> Result<(ReferenceImage, u32 } let (width, height) = decoded.dimensions(); Ok(( - ReferenceImage { + RawImageEditImage { bytes, file_name: value.file_name, mime_type, diff --git a/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs b/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs index 496bb9824..b0d7ec81b 100644 --- a/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs +++ b/server-rs/crates/platform-image/src/generated_asset_sheets/sheet.rs @@ -34,7 +34,7 @@ pub fn slice_generated_asset_sheet( let grid_size_u32 = u32::try_from(grid_size).map_err(|_| { GeneratedAssetSheetError::invalid_request("系列素材图集的 n 超出可支持范围。") })?; - let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { GeneratedAssetSheetError::decode_image(format!("系列素材图集解码失败:{error}")) })?; let source = apply_generated_asset_sheet_green_screen_alpha(source); @@ -94,7 +94,7 @@ pub fn slice_generated_asset_sheet_two_items_per_row( let grid_size_u32 = u32::try_from(grid_size).map_err(|_| { GeneratedAssetSheetError::invalid_request("系列素材图集的 n 超出可支持范围。") })?; - let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { GeneratedAssetSheetError::decode_image(format!("系列素材图集解码失败:{error}")) })?; let source = apply_generated_asset_sheet_green_screen_alpha(source); @@ -243,7 +243,7 @@ pub fn prepare_generated_icon_spritesheet_all_by_connected_components( "图标 spritesheet 累计裁剪像素上限必须大于 0。", )); } - let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { GeneratedAssetSheetError::decode_image(format!("图标 spritesheet 解码失败:{error}")) })?; let source = apply_generated_asset_sheet_green_screen_alpha(source); @@ -263,7 +263,7 @@ pub fn prepare_generated_icon_spritesheet_all_by_connected_components( pub fn prepare_generated_icon_spritesheet_grid_2x2( image: &crate::DownloadedImage, ) -> Result { - let source = image::load_from_memory(image.bytes.as_ref()).map_err(|error| { + let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| { GeneratedAssetSheetError::decode_image(format!("图标 spritesheet 解码失败:{error}")) })?; let source = apply_generated_asset_sheet_green_screen_alpha(source).into_rgba8(); @@ -942,7 +942,7 @@ mod tests { } } crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), } @@ -1031,7 +1031,7 @@ mod tests { sheet.put_pixel(x0 + 40, y0 + 8, Rgba(color)); } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1063,7 +1063,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1095,7 +1095,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1137,7 +1137,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1165,7 +1165,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1206,7 +1206,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1240,7 +1240,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1272,7 +1272,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; @@ -1303,7 +1303,7 @@ mod tests { } let source = crate::DownloadedImage { - bytes: encode_png(sheet).into(), + bytes: encode_png(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), }; diff --git a/server-rs/crates/platform-image/src/lib.rs b/server-rs/crates/platform-image/src/lib.rs index 6c7d480fb..9ef40103c 100644 --- a/server-rs/crates/platform-image/src/lib.rs +++ b/server-rs/crates/platform-image/src/lib.rs @@ -12,9 +12,9 @@ pub use vector_engine::{ DownloadedImage, GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages, NANOBANANA_2_MODEL, PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, - RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditOptions, - RawImageEditResult, ReferenceImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER, - VectorEngineImageSettings, build_vector_engine_image_http_client, + RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage, + RawImageEditOptions, RawImageEditResult, ReferenceImage, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, + VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client, build_vector_engine_image_request_body, build_vector_engine_nanobanana_generate_content_request_body, create_vector_engine_image_edit, create_vector_engine_image_edit_with_references, diff --git a/server-rs/crates/platform-image/src/pixel_art_snapper.rs b/server-rs/crates/platform-image/src/pixel_art_snapper.rs index 9f7ac5c33..589a26a6f 100644 --- a/server-rs/crates/platform-image/src/pixel_art_snapper.rs +++ b/server-rs/crates/platform-image/src/pixel_art_snapper.rs @@ -266,7 +266,7 @@ fn decode_rgba_source( return Err(PixelArtSnapError::InvalidInput(format!("{input} 为空"))); } - let dimension_reader = image::ImageReader::new(Cursor::new(source.bytes.as_ref())) + let dimension_reader = image::ImageReader::new(Cursor::new(source.bytes.as_slice())) .with_guessed_format() .map_err(|error| PixelArtSnapError::Decode { input, @@ -281,7 +281,7 @@ fn decode_rgba_source( })?; validate_dimensions(width, height, input)?; - let mut reader = image::ImageReader::new(Cursor::new(source.bytes.as_ref())) + let mut reader = image::ImageReader::new(Cursor::new(source.bytes.as_slice())) .with_guessed_format() .map_err(|error| PixelArtSnapError::Decode { input, @@ -465,7 +465,7 @@ fn encode_png(image: RgbaImage) -> Result { .write_to(&mut cursor, ImageFormat::Png) .map_err(|error| PixelArtSnapError::Encode(error.to_string()))?; Ok(DownloadedImage { - bytes: cursor.into_inner().into(), + bytes: cursor.into_inner(), mime_type: "image/png".to_string(), extension: "png".to_string(), }) diff --git a/server-rs/crates/platform-image/src/vector_engine/mod.rs b/server-rs/crates/platform-image/src/vector_engine/mod.rs index 44941cefb..25e984180 100644 --- a/server-rs/crates/platform-image/src/vector_engine/mod.rs +++ b/server-rs/crates/platform-image/src/vector_engine/mod.rs @@ -21,14 +21,15 @@ pub use client::{ create_vector_engine_nanobanana_generate_content, }; pub use constants::{ - GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, - VECTOR_ENGINE_GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER, + GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_GPT_IMAGE_2_MODEL, + VECTOR_ENGINE_PROVIDER, }; pub use error::{PlatformImageError, PlatformImageStatusHint}; pub use image_source::download_remote_image; pub use raw_edit::{ - RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, - RawImageEditDimensionError, RawImageEditOptions, create_vector_engine_raw_image_edit, + GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_DIMENSION_ALIGNMENT, RAW_IMAGE_MAX_EDGE, + RAW_IMAGE_MAX_PIXELS, RAW_IMAGE_MIN_PIXELS, RawImageEditDimensionError, RawImageEditImage, + RawImageEditOptions, RawImageEditResult, create_vector_engine_raw_image_edit, validate_raw_image_edit_dimensions, }; pub use request::{ @@ -38,6 +39,4 @@ pub use request::{ vector_engine_nanobanana_generate_content_url, }; pub use transport::build_vector_engine_image_http_client; -pub use types::{ - DownloadedImage, GeneratedImages, RawImageEditResult, ReferenceImage, VectorEngineImageSettings, -}; +pub use types::{DownloadedImage, GeneratedImages, ReferenceImage, VectorEngineImageSettings}; diff --git a/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs b/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs index d15340b07..c4399f3de 100644 --- a/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs +++ b/server-rs/crates/platform-image/src/vector_engine/raw_edit.rs @@ -1,18 +1,16 @@ use std::time::{Duration, Instant}; +use bytes::Bytes; use reqwest::multipart::{Form, Part}; use serde::Deserialize; use super::{ audit::build_failure_audit, budget::{effective_request_timeout_ms, request_budget_exhausted_error}, - constants::{ - GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE, GPT_IMAGE_2_MAX_PIXELS, - GPT_IMAGE_2_MIN_PIXELS, GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER, - }, + constants::{GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER}, error::PlatformImageError, request::vector_engine_images_edit_url, - types::{RawImageEditResult, ReferenceImage, VectorEngineImageSettings}, + types::VectorEngineImageSettings, util::truncate_raw, }; @@ -23,9 +21,27 @@ pub struct RawImageEditOptions { pub output_format: Option, pub width: u32, pub height: u32, - pub mask: Option, + pub mask: Option, } +#[derive(Clone, Debug)] +pub struct RawImageEditImage { + pub bytes: Bytes, + pub mime_type: String, + pub file_name: String, +} + +#[derive(Clone, Debug)] +pub struct RawImageEditResult { + pub b64_images: Vec, + pub recovered_failure_audits: Vec, +} + +const GPT_IMAGE_2_MIN_PIXELS: u64 = 655_360; +const GPT_IMAGE_2_MAX_PIXELS: u64 = 8_294_400; +const GPT_IMAGE_2_MAX_EDGE: u32 = 3_840; +const GPT_IMAGE_2_DIMENSION_ALIGNMENT: u32 = 16; + #[derive(Debug, Deserialize)] struct RawImageEditResponsePayload { data: Vec, @@ -40,6 +56,7 @@ pub const RAW_IMAGE_MAX_EDGE: u32 = GPT_IMAGE_2_MAX_EDGE; pub const RAW_IMAGE_DIMENSION_ALIGNMENT: u32 = GPT_IMAGE_2_DIMENSION_ALIGNMENT; pub const RAW_IMAGE_MIN_PIXELS: u64 = GPT_IMAGE_2_MIN_PIXELS; pub const RAW_IMAGE_MAX_PIXELS: u64 = GPT_IMAGE_2_MAX_PIXELS; +pub const GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD: u32 = 1536; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum RawImageEditDimensionError { @@ -104,7 +121,7 @@ pub fn validate_raw_image_edit_dimensions( pub async fn create_vector_engine_raw_image_edit( settings: &VectorEngineImageSettings, prompt: &str, - image: ReferenceImage, + image: RawImageEditImage, options: RawImageEditOptions, failure_context: &str, ) -> Result { @@ -126,7 +143,7 @@ pub async fn create_vector_engine_raw_image_edit( reference_image_count, )); }; - let ReferenceImage { + let RawImageEditImage { bytes: image_bytes, file_name: image_file_name, mime_type: image_mime_type, @@ -139,7 +156,7 @@ pub async fn create_vector_engine_raw_image_edit( // GPT-Image-2 原生只返回 b64_json;不要添加 URL 下载或 response_format 兼容分支。 .part( "image", - Part::bytes(image_bytes.to_vec()) + Part::stream(reqwest::Body::from(image_bytes)) .file_name(image_file_name) .mime_str(image_mime_type.as_str()) .map_err(|error| invalid_request(failure_context, error.to_string()))?, @@ -156,7 +173,7 @@ pub async fn create_vector_engine_raw_image_edit( if let Some(mask) = options.mask { form = form.part( "mask", - Part::bytes(mask.bytes.to_vec()) + Part::stream(reqwest::Body::from(mask.bytes)) .file_name(mask.file_name) .mime_str(mask.mime_type.as_str()) .map_err(|error| invalid_request(failure_context, error.to_string()))?, diff --git a/server-rs/crates/platform-image/tests/generated_asset_sheets.rs b/server-rs/crates/platform-image/tests/generated_asset_sheets.rs index b3c860528..d3bc61705 100644 --- a/server-rs/crates/platform-image/tests/generated_asset_sheets.rs +++ b/server-rs/crates/platform-image/tests/generated_asset_sheets.rs @@ -42,7 +42,7 @@ fn build_test_sheet(width: u32, height: u32) -> DownloadedImage { } DownloadedImage { - bytes: encode_image(sheet).into(), + bytes: encode_image(sheet), mime_type: "image/png".to_string(), extension: "png".to_string(), } diff --git a/server-rs/crates/platform-image/tests/vector_engine.rs b/server-rs/crates/platform-image/tests/vector_engine.rs index c764d3e67..f1bd4470b 100644 --- a/server-rs/crates/platform-image/tests/vector_engine.rs +++ b/server-rs/crates/platform-image/tests/vector_engine.rs @@ -259,7 +259,7 @@ async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() { let http_client = build_vector_engine_image_http_client(&settings).expect("client should build"); let reference_image = ReferenceImage { - bytes: bytes::Bytes::from_static(b"reference"), + bytes: b"reference".to_vec(), mime_type: "image/png".to_string(), file_name: "reference.png".to_string(), }; @@ -598,7 +598,7 @@ async fn vector_engine_image_edit_falls_back_when_preferred_model_is_unsupported let http_client = build_vector_engine_image_http_client(&settings).expect("client should build"); let reference = ReferenceImage { - bytes: bytes::Bytes::from_static(b"reference"), + bytes: b"reference".to_vec(), mime_type: "image/png".to_string(), file_name: "reference.png".to_string(), };