统一 GPT 图片尺寸计费阈值
提取共享的 1536 长边 tier 常量并复用到 raw 与编辑器计费。 同步 Raw 图片代理技术合同中的阈值来源。
This commit is contained in:
@@ -38,7 +38,7 @@ height: 1024
|
||||
|
||||
校验通过后按整数尺寸发送给 provider,不静默 clamp 或改写调用者尺寸。
|
||||
|
||||
Raw 路由的 multipart body limit 为 `64 MiB`,覆盖图片和文本字段;图片字段由 multipart 解析器收集为可共享字节缓冲,文本字段按 chunk 流式读取并在达到 `16 KiB` 时立即拒绝,随后在阻塞线程中完成 PNG 解码。PNG 解码使用与输出合同一致的资源上限:宽高各不超过 `3840`,解码分配不超过 `8294400 × 4` 字节;不再执行 base64 入站解码。
|
||||
Raw 路由的 multipart body limit 为 `64 MiB`,覆盖图片和文本字段;图片字段由 multipart 解析器收集为可共享字节缓冲,文本字段按 chunk 流式读取并在达到 `16 KiB` 时立即拒绝,随后在阻塞线程中完成 PNG 解码。PNG 仅接受 8-bit/channel(`png::BitDepth::Eight`),其它位深在解码前以 400 返回“`{field} 必须为 8-bit PNG(每通道 8 位)`”;PNG 解码使用与输出合同一致的资源上限:宽高各不超过 `3840`,解码分配不超过 `8294400 × 4` 字节;不再执行 base64 入站解码。
|
||||
|
||||
服务端发送给 `platform-image` 时固定注入:
|
||||
|
||||
@@ -72,7 +72,7 @@ response_format = b64_json
|
||||
|
||||
检查通过后,api-server 进入现有资产操作计费边界,通过 SpacetimeDB 钱包事务 procedure 原子完成:
|
||||
|
||||
1. 按现有图片编辑算法解析价格:GPT Image 2 长边不超过 1536 使用 1K 价格,否则使用 2K 价格;当前默认价格为 3 / 5 泥点;
|
||||
1. 按共享常量 `platform-image::GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD`(当前值 `1536`)解析图片价格:长边不超过阈值使用 1K 价格,否则使用 2K 价格;当前默认价格为 3 / 5 泥点;
|
||||
2. 以认证后的用户、`raw-image-edit` 命名空间和请求 ID 组成幂等扣费流水 ID;
|
||||
3. 原子扣除用户泥点并写入 `asset_operation_consume` 流水。
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use module_assets::{
|
||||
AssetObjectAccessPolicy, AssetObjectFieldError, AssetObjectUpsertInput,
|
||||
build_asset_object_upsert_input, generate_asset_object_id,
|
||||
};
|
||||
use platform_image::DownloadedImage;
|
||||
use platform_image::{DownloadedImage, GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD};
|
||||
use platform_oss::{
|
||||
LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssSignedGetObjectUrlRequest,
|
||||
};
|
||||
@@ -3841,7 +3841,11 @@ fn editor_image_price_size_from_pixels(size: &str) -> &'static str {
|
||||
let Ok(height) = height.parse::<u32>() else {
|
||||
return "1K";
|
||||
};
|
||||
if width.max(height) > 1536 { "2K" } else { "1K" }
|
||||
if width.max(height) > GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD {
|
||||
"2K"
|
||||
} else {
|
||||
"1K"
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_image_edit_uses_nanobanana_generate_content(model: &str) -> bool {
|
||||
@@ -3890,7 +3894,11 @@ fn infer_editor_image_edit_size_tier(model: &str, size: Option<&str>) -> Option<
|
||||
if model == EDITOR_IMAGE_MODEL_NANOBANANA2 && long_edge <= 768 {
|
||||
return Some("0.5K");
|
||||
}
|
||||
Some(if long_edge > 1536 { "2K" } else { "1K" })
|
||||
Some(if long_edge > GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD {
|
||||
"2K"
|
||||
} else {
|
||||
"1K"
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_editor_image_edit_pixel_size(size: &str) -> Option<(u32, u32)> {
|
||||
|
||||
@@ -6,7 +6,8 @@ use axum::{
|
||||
use bytes::Bytes;
|
||||
use image::{GenericImageView, ImageFormat, ImageReader};
|
||||
use platform_image::{
|
||||
RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RawImageEditOptions, ReferenceImage,
|
||||
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS,
|
||||
RawImageEditOptions, ReferenceImage,
|
||||
create_vector_engine_raw_image_edit, validate_raw_image_edit_dimensions,
|
||||
};
|
||||
use serde::Serialize;
|
||||
@@ -380,6 +381,15 @@ fn decode_image(value: RawImageData, field: &str) -> Result<(ReferenceImage, u32
|
||||
if reader.format() != Some(ImageFormat::Png) {
|
||||
return Err(bad_request(format!("{field} 文件必须是有效 PNG 文件")));
|
||||
}
|
||||
let png_decoder = png::Decoder::new(Cursor::new(bytes.as_ref()));
|
||||
let png_reader = png_decoder
|
||||
.read_info()
|
||||
.map_err(|_| bad_request(format!("{field} 文件必须是有效 PNG 文件")))?;
|
||||
if png_reader.info().bit_depth != png::BitDepth::Eight {
|
||||
return Err(bad_request(format!(
|
||||
"{field} 必须为 8-bit PNG(每通道 8 位)"
|
||||
)));
|
||||
}
|
||||
let decoded = reader
|
||||
.decode()
|
||||
.map_err(|error| map_decode_image_error(field, error))?;
|
||||
@@ -406,7 +416,11 @@ fn map_decode_image_error(field: &str, error: image::ImageError) -> AppError {
|
||||
}
|
||||
|
||||
async fn raw_image_edit_price(state: &AppState, width: u32, height: u32) -> Result<u32, AppError> {
|
||||
let tier = if width.max(height) > 1536 { "2K" } else { "1K" };
|
||||
let tier = if width.max(height) > GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD {
|
||||
"2K"
|
||||
} else {
|
||||
"1K"
|
||||
};
|
||||
state
|
||||
.editor_generation_pricing()
|
||||
.await
|
||||
@@ -561,6 +575,22 @@ mod tests {
|
||||
assert!(prepare_request(invalid_mime).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_rejects_non_eight_bit_png() {
|
||||
let mut bytes = Vec::new();
|
||||
{
|
||||
let mut encoder = png::Encoder::new(&mut bytes, 1, 1);
|
||||
encoder.set_color(png::ColorType::Rgba);
|
||||
encoder.set_depth(png::BitDepth::Sixteen);
|
||||
let mut writer = encoder.write_header().expect("PNG header");
|
||||
writer
|
||||
.write_image_data(&[0, 0, 0, 0, 0, 0, 0, 0])
|
||||
.expect("PNG body");
|
||||
}
|
||||
let error = prepare_request(request(bytes, None)).expect_err("16-bit PNG should fail");
|
||||
assert!(format!("{error:?}").contains("image 必须为 8-bit PNG(每通道 8 位)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_valid_png_reports_resource_limit() {
|
||||
let error = match prepare_request(request(png_bytes(RAW_IMAGE_MAX_EDGE + 1, 1), None)) {
|
||||
|
||||
@@ -5,6 +5,8 @@ pub const VECTOR_ENGINE_GPT_IMAGE_2_MODEL: &str = GPT_IMAGE_2_MODEL;
|
||||
pub const VECTOR_ENGINE_PROVIDER: &str = "vector-engine";
|
||||
pub const VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES: usize = 5;
|
||||
pub const VECTOR_ENGINE_NANOBANANA_MAX_REFERENCE_IMAGES: usize = 14;
|
||||
/// GPT-Image-2 pricing uses 1K through this inclusive long-edge threshold.
|
||||
pub const GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD: u32 = 1536;
|
||||
|
||||
pub(crate) const GPT_IMAGE_2_MIN_PIXELS: u64 = 655_360;
|
||||
pub(crate) const GPT_IMAGE_2_MAX_PIXELS: u64 = 8_294_400;
|
||||
|
||||
@@ -21,8 +21,8 @@ pub use client::{
|
||||
create_vector_engine_nanobanana_generate_content,
|
||||
};
|
||||
pub use constants::{
|
||||
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_GPT_IMAGE_2_MODEL,
|
||||
VECTOR_ENGINE_PROVIDER,
|
||||
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,
|
||||
};
|
||||
pub use error::{PlatformImageError, PlatformImageStatusHint};
|
||||
pub use image_source::download_remote_image;
|
||||
|
||||
Reference in New Issue
Block a user