为 Raw 编辑建立独立字节类型
恢复旧图片链路的 Vec 字节契约 让新增 raw_edit 输入和结果类型独立于既有 ReferenceImage
This commit is contained in:
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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<const N: usize>(
|
||||
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,
|
||||
|
||||
@@ -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<GeneratedAssetSheetConnectedIconPlan, GeneratedAssetSheetError> {
|
||||
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(),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<DownloadedImage, PixelArtSnapError> {
|
||||
.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(),
|
||||
})
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<String>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub mask: Option<ReferenceImage>,
|
||||
pub mask: Option<RawImageEditImage>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub recovered_failure_audits: Vec<super::audit::PlatformImageFailureAudit>,
|
||||
}
|
||||
|
||||
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<RawImageEditResponseEntry>,
|
||||
@@ -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<RawImageEditResult, PlatformImageError> {
|
||||
@@ -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()))?,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user