dive fallback for gpt image2 to platform-image

This commit is contained in:
2026-07-14 20:03:28 +08:00
parent de3d7db309
commit f1aa8abbd0
4 changed files with 165 additions and 11 deletions
@@ -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,
@@ -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,
};
@@ -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",
@@ -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::<u32>().expect("width should be numeric");
let height = height.parse::<u32>().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(