From f1aa8abbd0346a79d9eb9c9d0ed7c876aca7dc78 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 20:03:28 +0800 Subject: [PATCH] dive fallback for gpt image2 to platform-image --- .../src/vector_engine/client.rs | 11 +- .../platform-image/src/vector_engine/mod.rs | 2 +- .../src/vector_engine/request.rs | 100 +++++++++++++++++- .../platform-image/tests/vector_engine.rs | 63 ++++++++++- 4 files changed, 165 insertions(+), 11 deletions(-) diff --git a/server-rs/crates/platform-image/src/vector_engine/client.rs b/server-rs/crates/platform-image/src/vector_engine/client.rs index e5024124a..d8ee9271d 100644 --- a/server-rs/crates/platform-image/src/vector_engine/client.rs +++ b/server-rs/crates/platform-image/src/vector_engine/client.rs @@ -15,9 +15,10 @@ use super::{ request::{ build_vector_engine_image_edit_request_log_params, build_vector_engine_image_request_body_with_model, - build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size, - normalize_vector_engine_image_model, vector_engine_images_edit_url, - vector_engine_images_generation_url, vector_engine_nanobanana_generate_content_url, + build_vector_engine_nanobanana_generate_content_request_body, + normalize_image_size_for_model, normalize_vector_engine_image_model, + vector_engine_images_edit_url, vector_engine_images_generation_url, + vector_engine_nanobanana_generate_content_url, }, response::handle_vector_engine_response, types::{GeneratedImages, ReferenceImage, VectorEngineImageSettings}, @@ -79,7 +80,7 @@ pub async fn create_vector_engine_image_generation_with_model( } let request_url = vector_engine_images_generation_url(settings); - let normalized_size = normalize_image_size(size); + let normalized_size = normalize_image_size_for_model(model, size); let request_body = build_vector_engine_image_request_body_with_model( model, prompt, @@ -387,7 +388,7 @@ pub async fn create_vector_engine_image_edit_with_references_and_model( } let request_url = vector_engine_images_edit_url(settings); - let normalized_size = normalize_image_size(size); + let normalized_size = normalize_image_size_for_model(model, size); let request_params = build_vector_engine_image_edit_request_log_params( model, prompt, 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 f8d953e8a..1422ecb3a 100644 --- a/server-rs/crates/platform-image/src/vector_engine/mod.rs +++ b/server-rs/crates/platform-image/src/vector_engine/mod.rs @@ -23,7 +23,7 @@ pub use error::{PlatformImageError, PlatformImageStatusHint}; pub use image_source::download_remote_image; pub use request::{ build_vector_engine_image_request_body, build_vector_engine_image_request_body_with_model, - build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size, + build_vector_engine_nanobanana_generate_content_request_body, normalize_image_size_for_model, vector_engine_images_edit_url, vector_engine_images_generation_url, vector_engine_nanobanana_generate_content_url, }; diff --git a/server-rs/crates/platform-image/src/vector_engine/request.rs b/server-rs/crates/platform-image/src/vector_engine/request.rs index 869f4e374..e67771f15 100644 --- a/server-rs/crates/platform-image/src/vector_engine/request.rs +++ b/server-rs/crates/platform-image/src/vector_engine/request.rs @@ -40,7 +40,7 @@ pub fn build_vector_engine_image_request_body_with_model( ("n".to_string(), json!(candidate_count.clamp(1, 4))), ( "size".to_string(), - Value::String(normalize_image_size(size)), + Value::String(normalize_image_size_for_model(model, size)), ), ]); @@ -90,9 +90,9 @@ pub fn normalize_vector_engine_image_model(model: &str) -> &str { } } -pub fn normalize_image_size(size: &str) -> String { +pub fn normalize_image_size_for_model(model: &str, size: &str) -> String { let size = size.trim(); - match size { + let normalized_size = match size { "1:1" => "1024x1024".to_string(), "16:9" | "2k" => "1536x1024".to_string(), "2k-16:9" => "2048x1152".to_string(), @@ -100,6 +100,12 @@ pub fn normalize_image_size(size: &str) -> String { value if is_explicit_pixel_size(value) => normalize_explicit_pixel_size(value), value if !value.is_empty() => value.to_string(), _ => "1024x1024".to_string(), + }; + + if normalize_vector_engine_image_model(model) == GPT_IMAGE_2_MODEL { + clamp_gpt_image_2_pixel_size(normalized_size.as_str()) + } else { + normalized_size } } @@ -115,6 +121,94 @@ fn normalize_explicit_pixel_size(value: &str) -> String { value.replace('*', "x") } +fn clamp_gpt_image_2_pixel_size(size: &str) -> String { + const MIN_PIXELS: u64 = 655_360; + const MAX_PIXELS: u64 = 8_294_400; + const MAX_EDGE: u32 = 3_840; + const DIMENSION_ALIGNMENT: u32 = 16; + const MAX_ASPECT_RATIO: f64 = 3.0; + + // 中文注释:这里是 VectorEngine 的共享发送边界,只处理 gpt-image-2 的显式像素尺寸。 + let Some((width, height)) = parse_explicit_pixel_size(size) else { + return size.to_string(); + }; + // 中文注释:零尺寸无法计算比例或像素数,直接使用已知合法的默认尺寸。 + if width == 0 || height == 0 { + return "1024x1024".to_string(); + } + + let (mut width, mut height) = (f64::from(width), f64::from(height)); + // 中文注释:先把短边补至长边的三分之一,确保长短边比不超过 3:1。 + if width / height > MAX_ASPECT_RATIO { + height = width / MAX_ASPECT_RATIO; + } else if height / width > MAX_ASPECT_RATIO { + width = height / MAX_ASPECT_RATIO; + } + + let pixels = width * height; + // 中文注释:等比缩小,优先保留已收紧后的画面比例,同时满足最大边和最大总像素。 + let scale = (MAX_EDGE as f64 / width.max(height)) + .min((MAX_PIXELS as f64 / pixels).sqrt()) + .min(1.0); + width *= scale; + height *= scale; + + let pixels = width * height; + // 中文注释:小于最小总像素时等比放大;此前已处理比例,放大不会重新突破 3:1。 + if pixels < MIN_PIXELS as f64 { + let scale = (MIN_PIXELS as f64 / pixels).sqrt(); + width *= scale; + height *= scale; + } + + // 中文注释:provider 要求两边均为 16px 倍数,向上取整避免对齐后落到最小像素以下。 + let width = align_dimension_up(width, DIMENSION_ALIGNMENT); + let height = align_dimension_up(height, DIMENSION_ALIGNMENT); + // TODO unlikely but can improve + // 中文注释:对齐可能触碰最大边或最大像素,因此发送前重新核验全部约束。 + if is_valid_gpt_image_2_size( + width, + height, + MIN_PIXELS, + MAX_PIXELS, + MAX_EDGE, + MAX_ASPECT_RATIO, + ) { + return format!("{width}x{height}"); + } + + // 中文注释:无法同时满足全部约束时,回退为已知会被 provider 接受的默认尺寸。 + "1024x1024".to_string() +} + +fn align_dimension_up(value: f64, alignment: u32) -> u32 { + ((value.ceil() as u32).saturating_add(alignment - 1) / alignment) * alignment +} + +fn is_valid_gpt_image_2_size( + width: u32, + height: u32, + min_pixels: u64, + max_pixels: u64, + max_edge: u32, + max_aspect_ratio: f64, +) -> bool { + let pixels = u64::from(width) * u64::from(height); + width > 0 + && height > 0 + && width <= max_edge + && height <= max_edge + && width.is_multiple_of(16) + && height.is_multiple_of(16) + && (min_pixels..=max_pixels).contains(&pixels) + && f64::from(width.max(height)) / f64::from(width.min(height)) <= max_aspect_ratio +} + +fn parse_explicit_pixel_size(value: &str) -> Option<(u32, u32)> { + let (width, height) = value.split_once('x')?; + Some((width.parse().ok()?, height.parse().ok()?)) +} + fn normalize_nanobanana_aspect_ratio(aspect_ratio: &str) -> &str { match aspect_ratio.trim() { "2:3" => "2:3", diff --git a/server-rs/crates/platform-image/tests/vector_engine.rs b/server-rs/crates/platform-image/tests/vector_engine.rs index 66c6dc0b0..0ec6d9c43 100644 --- a/server-rs/crates/platform-image/tests/vector_engine.rs +++ b/server-rs/crates/platform-image/tests/vector_engine.rs @@ -47,12 +47,12 @@ fn vector_engine_module_exposes_provider_protocol_helpers() { } #[test] -fn vector_engine_keeps_explicit_publication_material_pixel_sizes() { +fn vector_engine_clamps_gpt_image_2_explicit_pixel_sizes_to_its_supported_pixel_budget() { let cover = build_vector_engine_image_request_body("宣发首图", None, "720x540", 1, &[]); let detail = build_vector_engine_image_request_body("详情单图", None, "720x1280", 1, &[]); let poster = build_vector_engine_image_request_body("运营海报", None, "1280x720", 1, &[]); - assert_eq!(cover["size"], "720x540"); + assert_eq!(cover["size"], "944x704"); assert_eq!(detail["size"], "720x1280"); assert_eq!(poster["size"], "1280x720"); } @@ -82,6 +82,65 @@ fn vector_engine_request_body_can_use_nanobanana2_model() { assert_eq!(body["n"], 1); } +#[test] +fn vector_engine_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() { + let gpt_body = build_vector_engine_image_request_body_with_model( + GPT_IMAGE_2_MODEL, + "小尺寸图", + None, + "640x640", + 1, + &[], + ); + let nanobanana_body = build_vector_engine_image_request_body_with_model( + "gemini-3.1-flash-image-preview", + "小尺寸图", + None, + "640x640", + 1, + &[], + ); + let oversized_gpt_body = build_vector_engine_image_request_body_with_model( + GPT_IMAGE_2_MODEL, + "大尺寸图", + None, + "4096x4096", + 1, + &[], + ); + + assert_eq!(gpt_body["size"], "816x816"); + assert_eq!(nanobanana_body["size"], "640x640"); + assert_eq!(oversized_gpt_body["size"], "2880x2880"); +} + +#[test] +fn vector_engine_gpt_image_2_sizes_always_meet_the_full_provider_envelope() { + for size in [ + "1x1", + "720x540", + "3841x1280", + "4096x4096", + "3200x400", + "16x4096", + "3840x3840", + ] { + let body = build_vector_engine_image_request_body("约束测试", None, size, 1, &[]); + let normalized = body["size"].as_str().expect("size should be a string"); + let (width, height) = normalized + .split_once('x') + .expect("gpt-image-2 size should be explicit pixels"); + let width = width.parse::().expect("width should be numeric"); + let height = height.parse::().expect("height should be numeric"); + let pixels = u64::from(width) * u64::from(height); + + assert!(width <= 3_840 && height <= 3_840, "{size} -> {normalized}"); + assert!(width.is_multiple_of(16) && height.is_multiple_of(16)); + assert!((655_360..=8_294_400).contains(&pixels)); + assert!(width.max(height) <= width.min(height) * 3); + } +} + #[test] fn vector_engine_request_body_can_use_nanobanana2_half_k() { let body = build_vector_engine_image_request_body_with_model(