优化原始图片请求缓冲

将 ReferenceImage 与 multipart 输入改用 bytes::Bytes。

通过流式 multipart part 避免 reqwest 路径的额外整图复制。
This commit is contained in:
2026-09-11 17:25:25 +08:00
parent 9acc23b848
commit 4bdbab29c0
9 changed files with 23 additions and 19 deletions
+7 -6
View File
@@ -3,6 +3,7 @@ use axum::{
extract::{Extension, Multipart, State},
http::StatusCode,
};
use bytes::Bytes;
use image::{GenericImageView, ImageFormat, ImageReader};
use platform_image::{
RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS, RawImageEditOptions, ReferenceImage,
@@ -30,7 +31,7 @@ use time::OffsetDateTime;
#[derive(Debug)]
struct RawImageData {
pub(crate) bytes: Vec<u8>,
pub(crate) bytes: Bytes,
pub(crate) mime_type: String,
pub(crate) file_name: String,
}
@@ -229,7 +230,7 @@ async fn read_multipart_image(
return Err(bad_request(format!("{name} 文件不能为空")));
}
Ok(RawImageData {
bytes: bytes.to_vec(),
bytes,
mime_type: "image/png".to_string(),
file_name: format!("{name}.png"),
})
@@ -425,12 +426,12 @@ mod tests {
fn request(image: Vec<u8>, mask: Option<Vec<u8>>) -> RawImageEditRequest {
RawImageEditRequest {
image: RawImageData {
bytes: image,
bytes: Bytes::from(image),
mime_type: "image/png".to_string(),
file_name: "image.png".to_string(),
},
mask: mask.map(|bytes| RawImageData {
bytes,
bytes: Bytes::from(bytes),
mime_type: "image/png".to_string(),
file_name: "mask.png".to_string(),
}),
@@ -520,7 +521,7 @@ mod tests {
let invalid_bytes = RawImageEditRequest {
image: RawImageData {
bytes: b"hello".to_vec(),
bytes: Bytes::from_static(b"hello"),
mime_type: "image/png".to_string(),
file_name: "image.png".to_string(),
},
@@ -530,7 +531,7 @@ mod tests {
let invalid_mime = RawImageEditRequest {
image: RawImageData {
bytes: png_bytes(1, 1),
bytes: Bytes::from(png_bytes(1, 1)),
mime_type: "image/jpeg".to_string(),
file_name: "image.jpg".to_string(),
},
@@ -6,6 +6,7 @@ license.workspace = true
[dependencies]
base64 = { workspace = true }
bytes = { workspace = true }
curl = { workspace = true }
image = { workspace = true, features = ["jpeg", "png", "webp"] }
reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] }
@@ -1107,7 +1107,7 @@ mod tests {
fn reference_image(index: usize) -> ReferenceImage {
ReferenceImage {
bytes: vec![index as u8],
bytes: bytes::Bytes::from(vec![index as u8]),
mime_type: "image/png".to_string(),
file_name: format!("reference-{index}.png"),
}
@@ -257,7 +257,7 @@ fn send_multipart_edit_request_with_curl_blocking(
form.part("image")
.buffer(
reference_image.file_name.as_str(),
reference_image.bytes.clone(),
reference_image.bytes.to_vec(),
)
.content_type(reference_image.mime_type.as_str())
.add()?;
@@ -338,7 +338,7 @@ mod tests {
"1024x1024",
1,
&[ReferenceImage {
bytes: b"reference".to_vec(),
bytes: bytes::Bytes::from_static(b"reference"),
mime_type: "image/png".to_string(),
file_name: "reference.png".to_string(),
}],
@@ -1,4 +1,5 @@
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use bytes::Bytes;
use reqwest::header;
use std::time::Instant;
@@ -161,7 +162,7 @@ pub(crate) async fn resolve_reference_images(
.await
.map_err(|error| contextualize_reference_download_error(error, failure_context))?;
resolved.push(ReferenceImage {
bytes: downloaded.bytes,
bytes: Bytes::from(downloaded.bytes),
mime_type: downloaded.mime_type.clone(),
file_name: format!(
"reference-{index}.{}",
@@ -213,7 +214,7 @@ pub(crate) fn parse_reference_image_data_url(
})?;
let mime_type = normalize_downloaded_image_mime_type(mime_type);
Ok(Some(ReferenceImage {
bytes,
bytes: Bytes::from(bytes),
file_name: format!(
"reference-{index}.{}",
mime_to_extension(mime_type.as_str())
@@ -138,7 +138,7 @@ pub async fn create_vector_engine_raw_image_edit(
.text("size", format!("{}x{}", options.width, options.height))
.part(
"image",
Part::bytes(image_bytes)
Part::stream_with_length(image_bytes.clone(), image_bytes.len() as u64)
.file_name(image_file_name)
.mime_str(image_mime_type.as_str())
.map_err(|error| invalid_request(failure_context, error.to_string()))?,
@@ -155,7 +155,7 @@ pub async fn create_vector_engine_raw_image_edit(
if let Some(mask) = options.mask {
form = form.part(
"mask",
Part::bytes(mask.bytes)
Part::stream_with_length(mask.bytes.clone(), mask.bytes.len() as u64)
.file_name(mask.file_name)
.mime_str(mask.mime_type.as_str())
.map_err(|error| invalid_request(failure_context, error.to_string()))?,
@@ -65,7 +65,7 @@ pub fn build_vector_engine_nanobanana_generate_content_request_body(
"mime_type": reference_image.mime_type,
"data": base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
reference_image.bytes.as_slice()
reference_image.bytes.as_ref()
),
},
}));
@@ -346,12 +346,12 @@ mod tests {
9,
&[
ReferenceImage {
bytes: vec![1, 2, 3, 4, 5],
bytes: bytes::Bytes::from_static(&[1, 2, 3, 4, 5]),
mime_type: "image/png".to_string(),
file_name: "reference-a.png".to_string(),
},
ReferenceImage {
bytes: vec![8; 7],
bytes: bytes::Bytes::from(vec![8; 7]),
mime_type: "image/jpeg".to_string(),
file_name: "reference-b.jpg".to_string(),
},
@@ -1,4 +1,5 @@
use super::audit::PlatformImageFailureAudit;
use bytes::Bytes;
#[derive(Clone, Debug)]
pub struct VectorEngineImageSettings {
@@ -31,7 +32,7 @@ pub struct DownloadedImage {
#[derive(Clone, Debug)]
pub struct ReferenceImage {
pub bytes: Vec<u8>,
pub bytes: Bytes,
pub mime_type: String,
pub file_name: 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: b"reference".to_vec(),
bytes: bytes::Bytes::from_static(b"reference"),
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: b"reference".to_vec(),
bytes: bytes::Bytes::from_static(b"reference"),
mime_type: "image/png".to_string(),
file_name: "reference.png".to_string(),
};