新增编辑器生成规范、生成角色形象、生成图标素材等功能
新增编辑器生成规范、生成角色形象、生成图标素材等功能
This commit is contained in:
@@ -16,7 +16,9 @@ pub use persist::{
|
||||
};
|
||||
pub use prompt::{GeneratedAssetSheetPromptInput, build_generated_asset_sheet_prompt};
|
||||
pub use sheet::{
|
||||
GeneratedAssetSheetSliceImage, crop_generated_asset_sheet_view_edge_matte,
|
||||
GeneratedAssetSheetConnectedIcon, GeneratedAssetSheetSliceImage,
|
||||
crop_generated_asset_sheet_view_edge_matte,
|
||||
crop_generated_asset_sheet_view_edge_matte_with_options, slice_generated_asset_sheet,
|
||||
slice_generated_asset_sheet_two_items_per_row,
|
||||
slice_generated_icon_spritesheet_by_connected_components,
|
||||
};
|
||||
|
||||
@@ -132,6 +132,25 @@ pub fn slice_generated_asset_sheet_two_items_per_row(
|
||||
Ok(slices)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GeneratedAssetSheetConnectedIcon {
|
||||
pub name: String,
|
||||
pub bytes: Vec<u8>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
pub fn slice_generated_icon_spritesheet_by_connected_components(
|
||||
image: &crate::DownloadedImage,
|
||||
icon_names: &[String],
|
||||
) -> Result<Vec<GeneratedAssetSheetConnectedIcon>, GeneratedAssetSheetError> {
|
||||
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);
|
||||
slice_generated_icon_spritesheet_rgba_by_connected_components(source, icon_names)
|
||||
}
|
||||
|
||||
pub fn crop_generated_asset_sheet_view_edge_matte(
|
||||
image: image::DynamicImage,
|
||||
) -> image::DynamicImage {
|
||||
@@ -141,6 +160,207 @@ pub fn crop_generated_asset_sheet_view_edge_matte(
|
||||
)
|
||||
}
|
||||
|
||||
fn slice_generated_icon_spritesheet_rgba_by_connected_components(
|
||||
source: image::DynamicImage,
|
||||
icon_names: &[String],
|
||||
) -> Result<Vec<GeneratedAssetSheetConnectedIcon>, GeneratedAssetSheetError> {
|
||||
let image = source.to_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let pixel_count = (width as usize).saturating_mul(height as usize);
|
||||
if pixel_count == 0 {
|
||||
return Err(GeneratedAssetSheetError::invalid_request(
|
||||
"图标 spritesheet 尺寸为空。",
|
||||
));
|
||||
}
|
||||
|
||||
let mut visited = vec![false; pixel_count];
|
||||
let mut components = Vec::<GeneratedAssetSheetCellBounds>::new();
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let pixel_index = (y as usize)
|
||||
.saturating_mul(width as usize)
|
||||
.saturating_add(x as usize);
|
||||
if visited[pixel_index] || image.get_pixel(x, y).0[3] == 0 {
|
||||
continue;
|
||||
}
|
||||
components.push(flood_fill_generated_icon_component(
|
||||
&image,
|
||||
&mut visited,
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
components.sort_by_key(|bounds| (bounds.y0, bounds.x0));
|
||||
if components.len() < icon_names.len() {
|
||||
return Err(GeneratedAssetSheetError::invalid_request(format!(
|
||||
"图标 spritesheet 连通域数量不足:需要 {} 个,实际 {} 个。",
|
||||
icon_names.len(),
|
||||
components.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut icons = Vec::with_capacity(icon_names.len());
|
||||
for (name, bounds) in icon_names.iter().zip(components.into_iter()) {
|
||||
let pad_x = (bounds.width() / 12).clamp(4, 16);
|
||||
let pad_y = (bounds.height() / 12).clamp(4, 16);
|
||||
let crop = GeneratedAssetSheetCellBounds {
|
||||
x0: bounds.x0.saturating_sub(pad_x),
|
||||
y0: bounds.y0.saturating_sub(pad_y),
|
||||
x1: bounds.x1.saturating_add(pad_x).min(width),
|
||||
y1: bounds.y1.saturating_add(pad_y).min(height),
|
||||
};
|
||||
let cropped = image::imageops::crop_imm(
|
||||
&image,
|
||||
crop.x0,
|
||||
crop.y0,
|
||||
crop.width(),
|
||||
crop.height(),
|
||||
)
|
||||
.to_image();
|
||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(cropped)
|
||||
.write_to(&mut cursor, ImageFormat::Png)
|
||||
.map_err(|error| {
|
||||
GeneratedAssetSheetError::encode_image(format!(
|
||||
"图标 spritesheet 切割失败:{error}"
|
||||
))
|
||||
})?;
|
||||
icons.push(GeneratedAssetSheetConnectedIcon {
|
||||
name: name.clone(),
|
||||
bytes: cursor.into_inner(),
|
||||
width: crop.width(),
|
||||
height: crop.height(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(icons)
|
||||
}
|
||||
|
||||
fn flood_fill_generated_icon_component(
|
||||
image: &image::RgbaImage,
|
||||
visited: &mut [bool],
|
||||
width: u32,
|
||||
height: u32,
|
||||
start_x: u32,
|
||||
start_y: u32,
|
||||
) -> GeneratedAssetSheetCellBounds {
|
||||
let mut queue = vec![(start_x, start_y)];
|
||||
let mut queue_index = 0usize;
|
||||
let start_index = (start_y as usize)
|
||||
.saturating_mul(width as usize)
|
||||
.saturating_add(start_x as usize);
|
||||
visited[start_index] = true;
|
||||
let mut bounds = GeneratedAssetSheetCellBounds {
|
||||
x0: start_x,
|
||||
y0: start_y,
|
||||
x1: start_x.saturating_add(1),
|
||||
y1: start_y.saturating_add(1),
|
||||
};
|
||||
|
||||
while queue_index < queue.len() {
|
||||
let (x, y) = queue[queue_index];
|
||||
queue_index += 1;
|
||||
bounds.x0 = bounds.x0.min(x);
|
||||
bounds.y0 = bounds.y0.min(y);
|
||||
bounds.x1 = bounds.x1.max(x.saturating_add(1));
|
||||
bounds.y1 = bounds.y1.max(y.saturating_add(1));
|
||||
|
||||
for next_y in y.saturating_sub(1)..=(y.saturating_add(1).min(height.saturating_sub(1))) {
|
||||
for next_x in x.saturating_sub(1)..=(x.saturating_add(1).min(width.saturating_sub(1))) {
|
||||
if next_x == x && next_y == y {
|
||||
continue;
|
||||
}
|
||||
let next_index = (next_y as usize)
|
||||
.saturating_mul(width as usize)
|
||||
.saturating_add(next_x as usize);
|
||||
if visited[next_index] || image.get_pixel(next_x, next_y).0[3] == 0 {
|
||||
continue;
|
||||
}
|
||||
visited[next_index] = true;
|
||||
queue.push((next_x, next_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use image::{ImageBuffer, Rgba};
|
||||
|
||||
fn encode_png(image: image::RgbaImage) -> Vec<u8> {
|
||||
let mut cursor = std::io::Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(image)
|
||||
.write_to(&mut cursor, ImageFormat::Png)
|
||||
.expect("png should encode");
|
||||
cursor.into_inner()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slices_icon_spritesheet_by_connected_components_in_reading_order() {
|
||||
let mut sheet: image::RgbaImage =
|
||||
ImageBuffer::from_pixel(96, 64, Rgba([0, 255, 0, 255]));
|
||||
for y in 10..24 {
|
||||
for x in 12..28 {
|
||||
sheet.put_pixel(x, y, Rgba([240, 80, 80, 255]));
|
||||
}
|
||||
}
|
||||
for y in 32..46 {
|
||||
for x in 52..70 {
|
||||
sheet.put_pixel(x, y, Rgba([80, 120, 240, 255]));
|
||||
}
|
||||
}
|
||||
|
||||
let source = crate::DownloadedImage {
|
||||
bytes: encode_png(sheet),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
};
|
||||
let icons = slice_generated_icon_spritesheet_by_connected_components(
|
||||
&source,
|
||||
&["返回按钮".to_string(), "设置按钮".to_string()],
|
||||
)
|
||||
.expect("icons should slice");
|
||||
|
||||
assert_eq!(icons.len(), 2);
|
||||
assert_eq!(icons[0].name, "返回按钮");
|
||||
assert_eq!(icons[1].name, "设置按钮");
|
||||
assert!(icons[0].width >= 16);
|
||||
assert!(icons[0].height >= 14);
|
||||
assert!(image::load_from_memory(icons[0].bytes.as_slice()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_when_connected_components_are_fewer_than_icon_names() {
|
||||
let mut sheet: image::RgbaImage =
|
||||
ImageBuffer::from_pixel(48, 48, Rgba([0, 255, 0, 255]));
|
||||
for y in 12..24 {
|
||||
for x in 12..24 {
|
||||
sheet.put_pixel(x, y, Rgba([240, 80, 80, 255]));
|
||||
}
|
||||
}
|
||||
let source = crate::DownloadedImage {
|
||||
bytes: encode_png(sheet),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
};
|
||||
|
||||
let error = slice_generated_icon_spritesheet_by_connected_components(
|
||||
&source,
|
||||
&["返回按钮".to_string(), "设置按钮".to_string()],
|
||||
)
|
||||
.expect_err("missing component should fail");
|
||||
|
||||
assert!(error.to_string().contains("连通域数量不足"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crop_generated_asset_sheet_view_edge_matte_with_options(
|
||||
image: image::DynamicImage,
|
||||
options: GeneratedAssetSheetAlphaOptions,
|
||||
|
||||
@@ -8,6 +8,7 @@ pub use vector_engine::{
|
||||
VECTOR_ENGINE_GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings,
|
||||
build_vector_engine_image_http_client, build_vector_engine_image_request_body,
|
||||
create_vector_engine_image_edit, create_vector_engine_image_edit_with_references,
|
||||
create_vector_engine_image_generation, download_remote_image, vector_engine_images_edit_url,
|
||||
vector_engine_images_generation_url,
|
||||
create_vector_engine_image_edit_with_references_and_model,
|
||||
create_vector_engine_image_generation, create_vector_engine_image_generation_with_model,
|
||||
download_remote_image, vector_engine_images_edit_url, vector_engine_images_generation_url,
|
||||
};
|
||||
|
||||
@@ -13,8 +13,10 @@ use super::{
|
||||
error::PlatformImageError,
|
||||
image_source::resolve_reference_images,
|
||||
request::{
|
||||
build_vector_engine_image_edit_request_log_params, build_vector_engine_image_request_body,
|
||||
normalize_image_size, vector_engine_images_edit_url, vector_engine_images_generation_url,
|
||||
build_vector_engine_image_edit_request_log_params,
|
||||
build_vector_engine_image_request_body_with_model, normalize_image_size,
|
||||
normalize_vector_engine_image_model, vector_engine_images_edit_url,
|
||||
vector_engine_images_generation_url,
|
||||
},
|
||||
response::handle_vector_engine_response,
|
||||
types::{GeneratedImages, ReferenceImage, VectorEngineImageSettings},
|
||||
@@ -31,12 +33,40 @@ pub async fn create_vector_engine_image_generation(
|
||||
reference_images: &[String],
|
||||
failure_context: &str,
|
||||
) -> Result<GeneratedImages, PlatformImageError> {
|
||||
create_vector_engine_image_generation_with_model(
|
||||
http_client,
|
||||
settings,
|
||||
GPT_IMAGE_2_MODEL,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
candidate_count,
|
||||
reference_images,
|
||||
failure_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_vector_engine_image_generation_with_model(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &VectorEngineImageSettings,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_images: &[String],
|
||||
failure_context: &str,
|
||||
) -> Result<GeneratedImages, PlatformImageError> {
|
||||
let model = normalize_vector_engine_image_model(model);
|
||||
if !reference_images.is_empty() {
|
||||
let resolved_references =
|
||||
resolve_reference_images(http_client, reference_images, failure_context).await?;
|
||||
return create_vector_engine_image_edit_with_references(
|
||||
return create_vector_engine_image_edit_with_references_and_model(
|
||||
http_client,
|
||||
settings,
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
@@ -49,7 +79,8 @@ pub async fn create_vector_engine_image_generation(
|
||||
|
||||
let request_url = vector_engine_images_generation_url(settings);
|
||||
let normalized_size = normalize_image_size(size);
|
||||
let request_body = build_vector_engine_image_request_body(
|
||||
let request_body = build_vector_engine_image_request_body_with_model(
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
normalized_size.as_str(),
|
||||
@@ -125,6 +156,7 @@ pub async fn create_vector_engine_image_generation(
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
endpoint = %request_url,
|
||||
status = response_status,
|
||||
image_model = model,
|
||||
prompt_chars = prompt.chars().count(),
|
||||
size = %normalized_size,
|
||||
reference_image_count = reference_images.len(),
|
||||
@@ -181,6 +213,33 @@ pub async fn create_vector_engine_image_edit_with_references(
|
||||
reference_images: &[ReferenceImage],
|
||||
failure_context: &str,
|
||||
) -> Result<GeneratedImages, PlatformImageError> {
|
||||
create_vector_engine_image_edit_with_references_and_model(
|
||||
http_client,
|
||||
settings,
|
||||
GPT_IMAGE_2_MODEL,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
candidate_count,
|
||||
reference_images,
|
||||
failure_context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_vector_engine_image_edit_with_references_and_model(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &VectorEngineImageSettings,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_images: &[ReferenceImage],
|
||||
failure_context: &str,
|
||||
) -> Result<GeneratedImages, PlatformImageError> {
|
||||
let model = normalize_vector_engine_image_model(model);
|
||||
if reference_images.is_empty() {
|
||||
return Err(PlatformImageError::InvalidRequest {
|
||||
provider: VECTOR_ENGINE_PROVIDER,
|
||||
@@ -191,6 +250,7 @@ pub async fn create_vector_engine_image_edit_with_references(
|
||||
let request_url = vector_engine_images_edit_url(settings);
|
||||
let normalized_size = normalize_image_size(size);
|
||||
let request_params = build_vector_engine_image_edit_request_log_params(
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
normalized_size.as_str(),
|
||||
@@ -208,7 +268,7 @@ pub async fn create_vector_engine_image_edit_with_references(
|
||||
tracing::info!(
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
endpoint = %request_url,
|
||||
image_model = GPT_IMAGE_2_MODEL,
|
||||
image_model = model,
|
||||
size = %normalized_size,
|
||||
candidate_count = candidate_count.clamp(1, 4),
|
||||
requested_candidate_count = candidate_count,
|
||||
@@ -230,6 +290,7 @@ pub async fn create_vector_engine_image_edit_with_references(
|
||||
match send_vector_engine_multipart_edit_request_with_curl(
|
||||
request_url.as_str(),
|
||||
settings.api_key.as_str(),
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
normalized_size.as_str(),
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
audit::build_failure_audit,
|
||||
constants::{GPT_IMAGE_2_MODEL, VECTOR_ENGINE_PROVIDER},
|
||||
constants::VECTOR_ENGINE_PROVIDER,
|
||||
error::PlatformImageError,
|
||||
request::build_prompt_with_negative,
|
||||
types::ReferenceImage,
|
||||
@@ -115,6 +115,7 @@ pub(crate) async fn send_vector_engine_json_request_with_curl(
|
||||
pub(crate) async fn send_vector_engine_multipart_edit_request_with_curl(
|
||||
request_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
normalized_size: &str,
|
||||
@@ -124,6 +125,7 @@ pub(crate) async fn send_vector_engine_multipart_edit_request_with_curl(
|
||||
) -> Result<VectorEngineCurlResponse, VectorEngineCurlError> {
|
||||
let request_url = request_url.to_string();
|
||||
let api_key = api_key.to_string();
|
||||
let model = model.to_string();
|
||||
let prompt = prompt.to_string();
|
||||
let negative_prompt = negative_prompt.map(str::to_string);
|
||||
let normalized_size = normalized_size.to_string();
|
||||
@@ -132,6 +134,7 @@ pub(crate) async fn send_vector_engine_multipart_edit_request_with_curl(
|
||||
send_multipart_edit_request_with_curl_blocking(
|
||||
request_url.as_str(),
|
||||
api_key.as_str(),
|
||||
model.as_str(),
|
||||
prompt.as_str(),
|
||||
negative_prompt.as_deref(),
|
||||
normalized_size.as_str(),
|
||||
@@ -230,6 +233,7 @@ fn send_json_request_with_curl_blocking(
|
||||
fn send_multipart_edit_request_with_curl_blocking(
|
||||
request_url: &str,
|
||||
api_key: &str,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
normalized_size: &str,
|
||||
@@ -239,7 +243,7 @@ fn send_multipart_edit_request_with_curl_blocking(
|
||||
) -> Result<VectorEngineCurlResponse, VectorEngineCurlError> {
|
||||
let mut form = Form::new();
|
||||
form.part("model")
|
||||
.contents(GPT_IMAGE_2_MODEL.as_bytes())
|
||||
.contents(model.as_bytes())
|
||||
.add()?;
|
||||
form.part("prompt")
|
||||
.contents(build_prompt_with_negative(prompt, negative_prompt).as_bytes())
|
||||
@@ -295,7 +299,7 @@ fn perform_curl_request(mut easy: Easy) -> Result<VectorEngineCurlResponse, curl
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::vector_engine::types::ReferenceImage;
|
||||
use crate::vector_engine::{constants::GPT_IMAGE_2_MODEL, types::ReferenceImage};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpListener,
|
||||
@@ -330,6 +334,7 @@ mod tests {
|
||||
let response = send_vector_engine_multipart_edit_request_with_curl(
|
||||
format!("{base_url}/v1/images/edits").as_str(),
|
||||
"test-key",
|
||||
GPT_IMAGE_2_MODEL,
|
||||
"测试提示词",
|
||||
None,
|
||||
"1024x1024",
|
||||
|
||||
@@ -14,14 +14,15 @@ mod util;
|
||||
pub use audit::PlatformImageFailureAudit;
|
||||
pub use client::{
|
||||
create_vector_engine_image_edit, create_vector_engine_image_edit_with_references,
|
||||
create_vector_engine_image_generation,
|
||||
create_vector_engine_image_edit_with_references_and_model,
|
||||
create_vector_engine_image_generation, create_vector_engine_image_generation_with_model,
|
||||
};
|
||||
pub use constants::{GPT_IMAGE_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 request::{
|
||||
build_vector_engine_image_request_body, normalize_image_size, vector_engine_images_edit_url,
|
||||
vector_engine_images_generation_url,
|
||||
build_vector_engine_image_request_body, build_vector_engine_image_request_body_with_model,
|
||||
normalize_image_size, vector_engine_images_edit_url, vector_engine_images_generation_url,
|
||||
};
|
||||
pub use transport::build_vector_engine_image_http_client;
|
||||
pub use types::{DownloadedImage, GeneratedImages, ReferenceImage, VectorEngineImageSettings};
|
||||
|
||||
@@ -12,10 +12,29 @@ pub fn build_vector_engine_image_request_body(
|
||||
candidate_count: u32,
|
||||
_reference_images: &[String],
|
||||
) -> Value {
|
||||
build_vector_engine_image_request_body_with_model(
|
||||
GPT_IMAGE_2_MODEL,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
candidate_count,
|
||||
_reference_images,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn build_vector_engine_image_request_body_with_model(
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
_reference_images: &[String],
|
||||
) -> Value {
|
||||
let model = normalize_vector_engine_image_model(model);
|
||||
let body = Map::from_iter([
|
||||
(
|
||||
"model".to_string(),
|
||||
Value::String(GPT_IMAGE_2_MODEL.to_string()),
|
||||
Value::String(model.to_string()),
|
||||
),
|
||||
(
|
||||
"prompt".to_string(),
|
||||
@@ -31,11 +50,20 @@ pub fn build_vector_engine_image_request_body(
|
||||
Value::Object(body)
|
||||
}
|
||||
|
||||
pub fn normalize_vector_engine_image_model(model: &str) -> &str {
|
||||
match model.trim() {
|
||||
"" => GPT_IMAGE_2_MODEL,
|
||||
value => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalize_image_size(size: &str) -> String {
|
||||
match size.trim() {
|
||||
"1024*1024" | "1024x1024" | "1:1" => "1024x1024",
|
||||
"1280*720" | "1280x720" | "1600*900" | "1600x900" | "16:9" | "1536x1024" | "2048x1152"
|
||||
| "2k" => "1536x1024",
|
||||
"1280*720" | "1280x720" | "1600*900" | "1600x900" | "16:9" | "1536x1024" | "2k" => {
|
||||
"1536x1024"
|
||||
}
|
||||
"1920*1080" | "1920x1080" | "2048*1152" | "2048x1152" | "2k-16:9" => "2048x1152",
|
||||
"1024*1536" | "1024x1536" | "9:16" => "1024x1536",
|
||||
value if !value.is_empty() => value,
|
||||
_ => "1024x1024",
|
||||
@@ -60,12 +88,14 @@ pub fn vector_engine_images_edit_url(settings: &VectorEngineImageSettings) -> St
|
||||
}
|
||||
|
||||
pub(crate) fn build_vector_engine_image_edit_request_log_params(
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
size: &str,
|
||||
candidate_count: u32,
|
||||
reference_images: &[ReferenceImage],
|
||||
) -> Value {
|
||||
let model = normalize_vector_engine_image_model(model);
|
||||
let prompt = prompt.trim();
|
||||
let negative_prompt = negative_prompt
|
||||
.map(str::trim)
|
||||
@@ -91,7 +121,7 @@ pub(crate) fn build_vector_engine_image_edit_request_log_params(
|
||||
.sum();
|
||||
|
||||
json!({
|
||||
"model": GPT_IMAGE_2_MODEL,
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"negativePrompt": negative_prompt.unwrap_or_default(),
|
||||
"promptChars": prompt.chars().count(),
|
||||
@@ -125,6 +155,7 @@ mod tests {
|
||||
#[test]
|
||||
fn edit_request_log_params_include_reference_image_sizes_without_secrets_or_bytes() {
|
||||
let params = build_vector_engine_image_edit_request_log_params(
|
||||
GPT_IMAGE_2_MODEL,
|
||||
" 拼图参考图重绘 ",
|
||||
Some(" 文字,水印 "),
|
||||
"1024x1024",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use platform_image::vector_engine::{
|
||||
GPT_IMAGE_2_MODEL, ReferenceImage, VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings,
|
||||
build_vector_engine_image_http_client, build_vector_engine_image_request_body,
|
||||
create_vector_engine_image_edit, create_vector_engine_image_generation,
|
||||
build_vector_engine_image_request_body_with_model, create_vector_engine_image_edit,
|
||||
create_vector_engine_image_generation,
|
||||
vector_engine_images_edit_url, vector_engine_images_generation_url,
|
||||
};
|
||||
use std::{
|
||||
@@ -43,6 +44,31 @@ fn vector_engine_module_exposes_provider_protocol_helpers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_engine_normalizes_2k_landscape_spec_size() {
|
||||
let body = build_vector_engine_image_request_body("生成规范图", None, "2048x1152", 1, &[]);
|
||||
|
||||
assert_eq!(body["model"], GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(body["size"], "2048x1152");
|
||||
assert_eq!(body["n"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_engine_request_body_can_use_nanobanana2_model() {
|
||||
let body = build_vector_engine_image_request_body_with_model(
|
||||
"gemini-3.1-flash-image-preview",
|
||||
"生成图标 spritesheet",
|
||||
None,
|
||||
"512x512",
|
||||
1,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], "gemini-3.1-flash-image-preview");
|
||||
assert_eq!(body["size"], "512x512");
|
||||
assert_eq!(body["n"], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn vector_engine_image_edit_retries_send_timeout_once_and_succeeds() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
|
||||
Reference in New Issue
Block a user