自动扩缩edit图片的大小适应gptimage2的生成图片大小参数要求 (#82)
尺寸严格限制规则 (Strict Size Constraints): 图片最大边长 ≤ 3840px <- by hardcoded length 宽高两边像素均为 16px 的倍数 <- by backend do crop 长边 / 短边 比值 ≤ 3:1 <- by hardcoded aspect ratio 总像素范围:最小 655360 ~ 最大 8294400 <- by auto scale in this pr logic impl in the backend --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/82 Co-authored-by: kvtodev <kvtodev@outlook.com> Co-committed-by: kvtodev <kvtodev@outlook.com>
This commit was merged in pull request #82.
This commit is contained in:
@@ -144,7 +144,7 @@ spacetime sql <database> "SELECT * FROM puzzle_gallery_card_view LIMIT 1" --serv
|
||||
|
||||
本地 `spacetime` CLI / standalone 版本必须和 `server-rs/Cargo.toml` 里锁定的 `spacetimedb` 版本一致;当前统一版本为 `2.6.0`。若版本错配,procedure 返回值可能在宿主侧触发 `Failed to BSATN deserialize procedure return value`,api-server 最终表现为敲木鱼等创作动作的 `SpacetimeDB procedure 调用超时`。排障时先运行 `spacetime --version`,再对照 `server-rs/Cargo.toml` 的 `spacetimedb = "..."`;遇到版本不匹配时不要继续深挖业务超时,直接执行 `spacetime version install <version> && spacetime version use <version>`,或在目标就是最新版本时执行 `spacetime version upgrade`,升级后重启 `npm run dev:spacetime` 再重试。当前 `scripts/dev.mjs` 会在启动和复用本地 SpacetimeDB 前写入并校验 `dev-spacetime-tool-version`,避免把旧 standalone 继续带进新一轮创作。
|
||||
|
||||
本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine `gpt-image-2` 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`;`api-server` 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若 VectorEngine 在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 `request_id` 的 provider 日志字段 `source`、`source_chain`、`source_chain_depth`,再查 `external_api_call_failure.metadata_json.errorSource`;当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。拼图关卡资产按 `level_scene -> ui_spritesheet -> level_background` 顺序生成,日志会带 `slot`、`asset_kind` 和 `elapsed_ms`。
|
||||
本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查 RPG / 拼图 / 抓大鹅等 VectorEngine 生图链路时,确认 `VECTOR_ENGINE_BASE_URL`、`VECTOR_ENGINE_API_KEY` 和 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git。VectorEngine `gpt-image-2` 图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`;`api-server` 只做配置、玩法编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前,按原比例尽量收敛 `gpt-image-2` 的显式像素尺寸:最大边长不超过 `3840`、宽高均为 `16` 的倍数、长短边比不超过 `3:1`,总像素范围为 `655360` 至 `8294400`。无法满足全部条件时回退为 `1024x1024`;其他模型保留其传入尺寸。开局 CG 故事板、首图、背景和图集都属于长耗时图片请求;后端默认会把 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 下限收口到 `1000000`,旧进程仍可能沿用重启前的短超时。若 VectorEngine 在 `send()` 阶段失败且日志显示 `SendRequest`,先看同一 `request_id` 的 provider 日志字段 `source`、`source_chain`、`source_chain_depth`,再查 `external_api_call_failure.metadata_json.errorSource`;当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。拼图关卡资产按 `level_scene -> ui_spritesheet -> level_background` 顺序生成,日志会带 `slot`、`asset_kind` 和 `elapsed_ms`。
|
||||
|
||||
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout`、`connect`、libcurl 35 SSL connect reset、libcurl 56 receive error / `unexpected eof while reading`、recv failure 等临时传输错误,或在 `upstream_status` 阶段收到 408 / 429 / 5xx(例如 Nginx HTML `502 Bad Gateway`)时,`platform-image` 会对同一请求最多发送 5 次;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。日志中 `VectorEngine 图片请求发送失败,准备重试` 或 `VectorEngine 图片上游状态可重试,准备重试` 表示本次失败已进入下一次尝试;最终仍失败时才会写入 `external_api_call_failure` 并返回 504 / 502。排查生产失败时应同时统计 retry 前的尝试日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ Prompt 输入摘要与 Prompt 约束只作为内部生成契约维护,不在 U
|
||||
- 前端生成请求统一通过 `/api/editor/images/generations`。
|
||||
- 宣发素材请求携带 `kind: publication-material`,前端提交和后端 handler 都固定归一为 `gpt-image-2`;即使旧前端或外部请求传入 `nanobanana2`,后端也按 `gpt-image-2` 生成和计费,但价格仍来自运行时模型定价配置,不写死数值。
|
||||
- `publication-detail-gallery` 不再携带 `candidateCount: 5`;后端仍兼容多候选请求,但当前宣发素材入口不主动批量生成。
|
||||
- 尺寸请求必须使用明确像素值:游戏首图 `720x540`、详情图 `720x1280`、运营海报 `1280x720`。VectorEngine 适配层对明确像素值保持原样透传;只有 `16:9`、`9:16`、`2k` 等比例 / 档位别名才走 provider 预设映射。
|
||||
- 尺寸请求和成品交付规格必须使用明确像素值:游戏首图 `720x540`、详情图 `720x1280`、运营海报 `1280x720`。这些值是画布图层与成品的业务规格;由于三种规格并非都满足 `gpt-image-2` 的上游尺寸约束,VectorEngine 适配层会在发送前等比归一到合法请求尺寸(最大边 `3840`、两边为 `16` 的倍数、长短边比不超过 `3:1`、总像素 `655360..8294400`),不能将上游归一后的尺寸当作宣发成品规格。只有 `16:9`、`9:16`、`2k` 等比例 / 档位别名才走 provider 预设映射。
|
||||
- 参考图在提交 `/api/editor/images/generations` 前由前端压缩成适合生成理解的图片 Data URL,避免原图 Data URL 撑爆 JSON 请求体;后端该路由保留 `12MB` body limit 作为兼容兜底。
|
||||
- 扣费通过现有钱包资产操作封装执行;上游生成失败或未返回图片时按现有补偿逻辑退款。
|
||||
- 生成成功后,成品作为图片画布生成图层加入画布,并保留游戏输入和参考图摘要供图层信息使用。
|
||||
|
||||
@@ -1662,6 +1662,9 @@ pub(crate) async fn generate_editor_image_for_owner(
|
||||
None
|
||||
};
|
||||
|
||||
// TODO the image size passed to api is already normalized, should remove those normalize
|
||||
// and let resize here to get the proper size
|
||||
image = restore_editor_generated_image_output_dimensions(image, image_size.as_ref())?;
|
||||
let (width, height) = image::load_from_memory(image.bytes.as_slice())
|
||||
.map(|image| (image.width(), image.height()))
|
||||
.unwrap_or((1024, 1024));
|
||||
@@ -2219,6 +2222,54 @@ fn restore_editor_image_edit_output_dimensions(
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
fn restore_editor_generated_image_output_dimensions(
|
||||
output: DownloadedOpenAiImage,
|
||||
// TODO primitive obsession
|
||||
target_size: &str,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
let (target_width, target_height) = target_size.split_once('x').ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "尺寸无效",
|
||||
}))
|
||||
})?;
|
||||
let target_width = target_width.parse::<u32>().map_err(|_| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "宽度无效",
|
||||
}))
|
||||
})?;
|
||||
let target_height = target_height.parse::<u32>().map_err(|_| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "高度无效",
|
||||
}))
|
||||
})?;
|
||||
let decoded = image::load_from_memory(output.bytes.as_slice()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": format!("素材生成结果不是有效图片:{error}"),
|
||||
}))
|
||||
})?;
|
||||
if decoded.width() == target_width && decoded.height() == target_height {
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
let restored = decoded.resize_to_fill(
|
||||
target_width,
|
||||
target_height,
|
||||
image::imageops::FilterType::Lanczos3,
|
||||
);
|
||||
Ok(DownloadedOpenAiImage {
|
||||
bytes: encode_editor_image_edit_png(
|
||||
restored,
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"恢复宣发素材交付尺寸失败",
|
||||
)?,
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn edit_editor_image(
|
||||
State(state): State<AppState>,
|
||||
@@ -7497,6 +7548,30 @@ mod tests {
|
||||
assert_eq!(restored.extension, "png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn publication_material_generation_restores_provider_output_to_workflow_dimensions() {
|
||||
let image = image::DynamicImage::new_rgba8(944, 704);
|
||||
let mut bytes = Cursor::new(Vec::new());
|
||||
image
|
||||
.write_to(&mut bytes, image::ImageFormat::Png)
|
||||
.expect("test image should encode");
|
||||
|
||||
let restored = restore_editor_generated_image_output_dimensions(
|
||||
DownloadedOpenAiImage {
|
||||
bytes: bytes.into_inner(),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
},
|
||||
"720x540",
|
||||
)
|
||||
.expect("provider output should restore delivery dimensions");
|
||||
let restored_image = image::load_from_memory(restored.bytes.as_slice()).unwrap();
|
||||
|
||||
assert_eq!((restored_image.width(), restored_image.height()), (720, 540));
|
||||
assert_eq!(restored.mime_type, "image/png");
|
||||
assert_eq!(restored.extension, "png");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_generation_dimensions_follow_model_options() {
|
||||
let default_generation = normalize_editor_generation_options(None, Some("1:1"), Some("1K"));
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user