Merge remote-tracking branch 'origin/master' into editor-agent-refactored
# Conflicts: # server-rs/crates/api-server/src/editor_agent.rs
This commit is contained in:
@@ -267,6 +267,8 @@ pub struct EditorImageEditRequest {
|
||||
pub(crate) source_image_src: String,
|
||||
pub(crate) size: Option<String>,
|
||||
pub(crate) model: Option<String>,
|
||||
pub(crate) aspect_ratio: Option<String>,
|
||||
pub(crate) image_size: Option<String>,
|
||||
pub(crate) reference_image_srcs: Option<Vec<String>>,
|
||||
pub(crate) project_id: Option<String>,
|
||||
pub(crate) asset_kind: Option<String>,
|
||||
@@ -2073,7 +2075,15 @@ async fn resolve_editor_image_edit_price(
|
||||
state: &AppState,
|
||||
model: &str,
|
||||
size: &str,
|
||||
image_size: Option<&str>,
|
||||
) -> Result<u32, AppError> {
|
||||
let price_size = if model == EDITOR_IMAGE_MODEL_NANOBANANA2 {
|
||||
image_size
|
||||
.map(|value| normalize_editor_generation_image_size(model, Some(value)))
|
||||
.unwrap_or_else(|| editor_image_price_size_from_pixels(size))
|
||||
} else {
|
||||
editor_image_price_size_from_pixels(size)
|
||||
};
|
||||
let expected_price_mud_points = state
|
||||
.editor_generation_pricing()
|
||||
.await
|
||||
@@ -2083,11 +2093,7 @@ async fn resolve_editor_image_edit_price(
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
})?
|
||||
.image_generation_mud_points(
|
||||
Some("quick-edit"),
|
||||
Some(model),
|
||||
Some(editor_image_price_size_from_pixels(size)),
|
||||
);
|
||||
.image_generation_mud_points(Some("quick-edit"), Some(model), Some(price_size));
|
||||
Ok(expected_price_mud_points)
|
||||
}
|
||||
|
||||
@@ -2104,6 +2110,63 @@ fn editor_image_price_size_from_pixels(size: &str) -> &'static str {
|
||||
if width.max(height) > 1536 { "2K" } else { "1K" }
|
||||
}
|
||||
|
||||
fn editor_image_edit_uses_nanobanana_generate_content(model: &str) -> bool {
|
||||
model == EDITOR_IMAGE_MODEL_NANOBANANA2
|
||||
}
|
||||
|
||||
fn normalize_editor_image_edit_generation_options(
|
||||
model: Option<&str>,
|
||||
aspect_ratio: Option<&str>,
|
||||
image_size: Option<&str>,
|
||||
legacy_size: Option<&str>,
|
||||
) -> EditorGenerationOptions {
|
||||
let model_options = normalize_editor_generation_options(model, None, None);
|
||||
let inferred_aspect_ratio = aspect_ratio
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.or_else(|| infer_editor_image_edit_aspect_ratio(legacy_size));
|
||||
let inferred_image_size = image_size
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.or_else(|| infer_editor_image_edit_size_tier(model_options.model, legacy_size));
|
||||
normalize_editor_generation_options(model, inferred_aspect_ratio, inferred_image_size)
|
||||
}
|
||||
|
||||
fn infer_editor_image_edit_aspect_ratio(size: Option<&str>) -> Option<&'static str> {
|
||||
let (width, height) = parse_editor_image_edit_pixel_size(size?)?;
|
||||
let source_ratio = f64::from(width) / f64::from(height);
|
||||
[
|
||||
("1:1", 1.0),
|
||||
("4:3", 4.0 / 3.0),
|
||||
("3:2", 3.0 / 2.0),
|
||||
("2:3", 2.0 / 3.0),
|
||||
("9:16", 9.0 / 16.0),
|
||||
("16:9", 16.0 / 9.0),
|
||||
]
|
||||
.into_iter()
|
||||
.min_by(|(_, left), (_, right)| {
|
||||
(left - source_ratio)
|
||||
.abs()
|
||||
.total_cmp(&(right - source_ratio).abs())
|
||||
})
|
||||
.map(|(label, _)| label)
|
||||
}
|
||||
|
||||
fn infer_editor_image_edit_size_tier(model: &str, size: Option<&str>) -> Option<&'static str> {
|
||||
let (width, height) = parse_editor_image_edit_pixel_size(size?)?;
|
||||
let long_edge = width.max(height);
|
||||
if model == EDITOR_IMAGE_MODEL_NANOBANANA2 && long_edge <= 768 {
|
||||
return Some("0.5K");
|
||||
}
|
||||
Some(if long_edge > 1536 { "2K" } else { "1K" })
|
||||
}
|
||||
|
||||
fn parse_editor_image_edit_pixel_size(size: &str) -> Option<(u32, u32)> {
|
||||
let normalized = size.trim().replace('*', "x");
|
||||
let (width, height) = normalized.split_once('x')?;
|
||||
let width = width.parse::<u32>().ok()?;
|
||||
let height = height.parse::<u32>().ok()?;
|
||||
(width > 0 && height > 0).then_some((width, height))
|
||||
}
|
||||
|
||||
fn normalize_editor_image_generation_size(size: Option<&str>) -> Cow<'static, str> {
|
||||
match size.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some("1024x1024") | Some("1024*1024") | Some("1:1") => Cow::Borrowed("1024x1024"),
|
||||
@@ -2561,12 +2624,21 @@ pub async fn edit_editor_image(
|
||||
if !state.config.external_generation_mode.is_inline() {
|
||||
ensure_editor_image_edit_source_allowed(&state, caller.owner_user_id.as_str(), &payload)
|
||||
.await?;
|
||||
let generation_options =
|
||||
normalize_editor_generation_options(payload.model.as_deref(), None, None);
|
||||
let generation_options = normalize_editor_image_edit_generation_options(
|
||||
payload.model.as_deref(),
|
||||
payload.aspect_ratio.as_deref(),
|
||||
payload.image_size.as_deref(),
|
||||
payload.size.as_deref(),
|
||||
);
|
||||
let image_size = normalize_editor_image_generation_size(payload.size.as_deref());
|
||||
let price_mud_points = u64::from(
|
||||
resolve_editor_image_edit_price(&state, generation_options.model, image_size.as_ref())
|
||||
.await?,
|
||||
resolve_editor_image_edit_price(
|
||||
&state,
|
||||
generation_options.model,
|
||||
image_size.as_ref(),
|
||||
Some(generation_options.image_size),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
let source_entity_id =
|
||||
editor_generation_source_entity_id(payload.project_id.as_deref(), "editor-image-edit");
|
||||
@@ -2607,8 +2679,12 @@ pub(crate) async fn edit_editor_image_for_owner(
|
||||
})),
|
||||
);
|
||||
}
|
||||
let generation_options =
|
||||
normalize_editor_generation_options(payload.model.as_deref(), None, None);
|
||||
let generation_options = normalize_editor_image_edit_generation_options(
|
||||
payload.model.as_deref(),
|
||||
payload.aspect_ratio.as_deref(),
|
||||
payload.image_size.as_deref(),
|
||||
payload.size.as_deref(),
|
||||
);
|
||||
let requested_image_size = normalize_editor_image_generation_size(payload.size.as_deref());
|
||||
let mut reference_images =
|
||||
Vec::with_capacity(1 + payload.reference_image_srcs.as_ref().map_or(0, Vec::len));
|
||||
@@ -2637,33 +2713,71 @@ pub(crate) async fn edit_editor_image_for_owner(
|
||||
);
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let edit_dimensions =
|
||||
prepare_editor_image_edit_references(&mut reference_images, requested_image_size.as_ref())?;
|
||||
let image_size = format!(
|
||||
"{}x{}",
|
||||
edit_dimensions.provider_width, edit_dimensions.provider_height
|
||||
);
|
||||
let expected_price_mud_points =
|
||||
resolve_editor_image_edit_price(state, generation_options.model, image_size.as_ref())
|
||||
.await?;
|
||||
let generated = execute_billable_asset_operation_with_cost(
|
||||
if editor_image_edit_uses_nanobanana_generate_content(generation_options.model) {
|
||||
None
|
||||
} else {
|
||||
Some(prepare_editor_image_edit_references(
|
||||
&mut reference_images,
|
||||
requested_image_size.as_ref(),
|
||||
)?)
|
||||
};
|
||||
let provider_size = edit_dimensions
|
||||
.map(|dimensions| {
|
||||
format!(
|
||||
"{}x{}",
|
||||
dimensions.provider_width, dimensions.provider_height
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| generation_options.provider_image_size.to_string());
|
||||
let expected_price_mud_points = resolve_editor_image_edit_price(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
"editor_image_edit",
|
||||
request_context.request_id(),
|
||||
u64::from(expected_price_mud_points),
|
||||
create_openai_image_edit_with_references_and_model(
|
||||
&http_client,
|
||||
&settings,
|
||||
generation_options.model,
|
||||
prompt.as_str(),
|
||||
Some("文字、水印、边框、按钮、UI 控件、低清晰度、变形主体"),
|
||||
image_size.as_ref(),
|
||||
1,
|
||||
reference_images.as_slice(),
|
||||
"图片画布修改图片",
|
||||
),
|
||||
generation_options.model,
|
||||
requested_image_size.as_ref(),
|
||||
Some(generation_options.image_size),
|
||||
)
|
||||
.await?;
|
||||
let generated = if editor_image_edit_uses_nanobanana_generate_content(generation_options.model)
|
||||
{
|
||||
execute_billable_asset_operation_with_cost(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
"editor_image_edit",
|
||||
request_context.request_id(),
|
||||
u64::from(expected_price_mud_points),
|
||||
create_openai_nanobanana_generate_content(
|
||||
&http_client,
|
||||
&settings,
|
||||
generation_options.model,
|
||||
prompt.as_str(),
|
||||
Some("文字、水印、边框、按钮、UI 控件、低清晰度、变形主体"),
|
||||
generation_options.aspect_ratio,
|
||||
provider_size.as_str(),
|
||||
reference_images.as_slice(),
|
||||
"图片画布修改图片",
|
||||
),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
execute_billable_asset_operation_with_cost(
|
||||
state,
|
||||
caller.owner_user_id.as_str(),
|
||||
"editor_image_edit",
|
||||
request_context.request_id(),
|
||||
u64::from(expected_price_mud_points),
|
||||
create_openai_image_edit_with_references_and_model(
|
||||
&http_client,
|
||||
&settings,
|
||||
generation_options.model,
|
||||
prompt.as_str(),
|
||||
Some("文字、水印、边框、按钮、UI 控件、低清晰度、变形主体"),
|
||||
provider_size.as_str(),
|
||||
1,
|
||||
reference_images.as_slice(),
|
||||
"图片画布修改图片",
|
||||
),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
let provider_image = generated.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
@@ -2681,15 +2795,18 @@ pub(crate) async fn edit_editor_image_for_owner(
|
||||
})?;
|
||||
let asset_label =
|
||||
resolve_editor_generated_asset_label(payload.asset_label.clone(), "图片修改结果");
|
||||
let (image, dimension_restore_error) =
|
||||
restore_editor_image_edit_output_dimensions_or_original(provider_image, &edit_dimensions);
|
||||
let (image, dimension_restore_error) = if let Some(edit_dimensions) = edit_dimensions.as_ref() {
|
||||
restore_editor_image_edit_output_dimensions_or_original(provider_image, edit_dimensions)
|
||||
} else {
|
||||
(provider_image, None)
|
||||
};
|
||||
if let Some(error) = dimension_restore_error {
|
||||
tracing::warn!(
|
||||
task_id = %generated.task_id,
|
||||
provider_width,
|
||||
provider_height,
|
||||
target_width = edit_dimensions.target_width,
|
||||
target_height = edit_dimensions.target_height,
|
||||
target_width = edit_dimensions.map(|dimensions| dimensions.target_width),
|
||||
target_height = edit_dimensions.map(|dimensions| dimensions.target_height),
|
||||
error = ?error,
|
||||
"画板快速编辑尺寸恢复失败,保留并上传 provider 原图"
|
||||
);
|
||||
@@ -8159,23 +8276,51 @@ mod tests {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
|
||||
assert_eq!(
|
||||
resolve_editor_image_edit_price(&state, GPT_IMAGE_2_MODEL, "1024x1024")
|
||||
resolve_editor_image_edit_price(&state, GPT_IMAGE_2_MODEL, "1024x1024", None)
|
||||
.await
|
||||
.unwrap(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_image_edit_price(&state, GPT_IMAGE_2_MODEL, "2048x1152")
|
||||
resolve_editor_image_edit_price(&state, GPT_IMAGE_2_MODEL, "2048x1152", None)
|
||||
.await
|
||||
.unwrap(),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_image_edit_price(&state, EDITOR_IMAGE_MODEL_NANOBANANA2, "2048x2048")
|
||||
resolve_editor_image_edit_price(&state, GPT_IMAGE_2_MODEL, "2048x1152", Some("1K"),)
|
||||
.await
|
||||
.unwrap(),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_image_edit_price(
|
||||
&state,
|
||||
EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
"2048x2048",
|
||||
Some("2K"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
24
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_editor_image_edit_price(
|
||||
&state,
|
||||
EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
"512x512",
|
||||
Some("0.5K"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
8
|
||||
);
|
||||
assert!(editor_image_edit_uses_nanobanana_generate_content(
|
||||
EDITOR_IMAGE_MODEL_NANOBANANA2
|
||||
));
|
||||
assert!(!editor_image_edit_uses_nanobanana_generate_content(
|
||||
GPT_IMAGE_2_MODEL
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -9708,7 +9853,9 @@ mod tests {
|
||||
"prompt": "修改图片",
|
||||
"sourceImageSrc": "data:image/png;base64,AAAA",
|
||||
"size": "2048x1152",
|
||||
"model": "gpt-image-2",
|
||||
"model": "gemini-3.1-flash-image-preview",
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "2K",
|
||||
"referenceImageSrcs": ["data:image/png;base64,BBBB"],
|
||||
"targetLayerId": "layer-source"
|
||||
}))
|
||||
@@ -9716,7 +9863,20 @@ mod tests {
|
||||
|
||||
assert_eq!(request.prompt, "修改图片");
|
||||
assert_eq!(request.size.as_deref(), Some("2048x1152"));
|
||||
assert_eq!(request.model.as_deref(), Some("gpt-image-2"));
|
||||
assert_eq!(
|
||||
request.model.as_deref(),
|
||||
Some("gemini-3.1-flash-image-preview")
|
||||
);
|
||||
assert_eq!(request.aspect_ratio.as_deref(), Some("16:9"));
|
||||
assert_eq!(request.image_size.as_deref(), Some("2K"));
|
||||
let legacy_options = normalize_editor_image_edit_generation_options(
|
||||
request.model.as_deref(),
|
||||
None,
|
||||
None,
|
||||
Some("2048x1365"),
|
||||
);
|
||||
assert_eq!(legacy_options.aspect_ratio, "3:2");
|
||||
assert_eq!(legacy_options.image_size, "2K");
|
||||
assert_eq!(
|
||||
request.reference_image_srcs,
|
||||
Some(vec!["data:image/png;base64,BBBB".to_string()])
|
||||
|
||||
@@ -885,8 +885,8 @@ mod tests {
|
||||
"#/components/schemas/EditorIconSpritesheetSliceWarning"
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["components"]["schemas"]["EditorImageGenerationResponse"]["properties"]
|
||||
["warning"]["anyOf"][0]["$ref"],
|
||||
parsed["components"]["schemas"]["EditorImageGenerationResponse"]["properties"]["warning"]
|
||||
["anyOf"][0]["$ref"],
|
||||
"#/components/schemas/EditorGenerationWarning"
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user