Switch to VectorEngine gpt-image-2 and edits
Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
This commit is contained in:
@@ -212,7 +212,7 @@ pub(super) async fn ensure_match3d_background_asset(
|
||||
}
|
||||
}
|
||||
|
||||
let generated_background = generate_match3d_background_image(
|
||||
let generated_background = generate_match3d_level_asset_bundle(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
@@ -236,6 +236,40 @@ pub(super) async fn ensure_match3d_background_asset(
|
||||
Ok(assets)
|
||||
}
|
||||
|
||||
pub(super) async fn resolve_or_generate_match3d_level_asset_bundle(
|
||||
state: &AppState,
|
||||
request_context: &RequestContext,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
profile_id: &str,
|
||||
config: &Match3DConfigJson,
|
||||
background_prompt: &str,
|
||||
assets: &[Match3DGeneratedItemAsset],
|
||||
) -> Result<Match3DGeneratedBackgroundAsset, Response> {
|
||||
if let Some(existing_background) = find_match3d_generated_background_asset(assets) {
|
||||
if is_match3d_background_asset_ready(&existing_background) {
|
||||
return Ok(existing_background);
|
||||
}
|
||||
}
|
||||
|
||||
let normalized_prompt = normalize_match3d_background_prompt(background_prompt);
|
||||
let resolved_prompt = if normalized_prompt.is_empty() {
|
||||
build_fallback_match3d_background_prompt(config)
|
||||
} else {
|
||||
normalized_prompt
|
||||
};
|
||||
generate_match3d_level_asset_bundle(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
config,
|
||||
resolved_prompt.as_str(),
|
||||
)
|
||||
.await
|
||||
.map_err(|error| match3d_error_response(request_context, MATCH3D_AGENT_PROVIDER, error))
|
||||
}
|
||||
|
||||
pub(super) fn attach_match3d_background_asset_to_assets(
|
||||
assets: &mut Vec<Match3DGeneratedItemAsset>,
|
||||
background_asset: Match3DGeneratedBackgroundAsset,
|
||||
@@ -281,7 +315,7 @@ pub(super) async fn generate_match3d_cover_image_asset(
|
||||
create_openai_image_edit(
|
||||
&http_client,
|
||||
&settings,
|
||||
build_match3d_cover_edit_prompt(cover_prompt.as_str()).as_str(),
|
||||
build_match3d_cover_uploaded_reference_prompt(cover_prompt.as_str()).as_str(),
|
||||
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
|
||||
"1:1",
|
||||
&uploaded_image,
|
||||
@@ -289,27 +323,38 @@ pub(super) async fn generate_match3d_cover_image_asset(
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
let reference_images = resolve_match3d_cover_reference_image_data_urls(
|
||||
let reference_images = resolve_match3d_cover_reference_images_for_edit(
|
||||
state,
|
||||
reference_image_srcs,
|
||||
MATCH3D_ITEM_IMAGE_MAX_BYTES,
|
||||
)
|
||||
.await?;
|
||||
create_openai_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
build_match3d_cover_reference_generation_prompt(
|
||||
if reference_images.is_empty() {
|
||||
create_openai_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
cover_prompt.as_str(),
|
||||
!reference_images.is_empty(),
|
||||
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
|
||||
"1:1",
|
||||
1,
|
||||
&[],
|
||||
"抓大鹅封面图生成失败",
|
||||
)
|
||||
.as_str(),
|
||||
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
|
||||
"1:1",
|
||||
1,
|
||||
reference_images.as_slice(),
|
||||
"抓大鹅封面图生成失败",
|
||||
)
|
||||
.await?
|
||||
.await?
|
||||
} else {
|
||||
create_openai_image_edit_with_references(
|
||||
&http_client,
|
||||
&settings,
|
||||
build_match3d_cover_reference_generation_prompt(cover_prompt.as_str(), true)
|
||||
.as_str(),
|
||||
Some("文字、水印、UI、按钮、倒计时、分数、教程浮层、菜单、边框"),
|
||||
"1:1",
|
||||
1,
|
||||
reference_images.as_slice(),
|
||||
"抓大鹅封面图生成失败",
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
let image = generated.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
@@ -347,7 +392,7 @@ fn build_match3d_cover_generation_prompt(config: &Match3DConfigJson, prompt: &st
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_cover_edit_prompt(prompt: &str) -> String {
|
||||
pub(super) fn build_match3d_cover_uploaded_reference_prompt(prompt: &str) -> String {
|
||||
format!(
|
||||
concat!(
|
||||
"请以随请求上传的封面图作为第一优先级重绘依据,保留主图的主体、构图、视角和主要配色;",
|
||||
@@ -382,24 +427,113 @@ pub(super) async fn generate_match3d_background_image(
|
||||
profile_id: &str,
|
||||
config: &Match3DConfigJson,
|
||||
prompt: &str,
|
||||
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
|
||||
generate_match3d_level_asset_bundle(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
config,
|
||||
prompt,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn generate_match3d_level_asset_bundle(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
session_id: &str,
|
||||
profile_id: &str,
|
||||
config: &Match3DConfigJson,
|
||||
prompt: &str,
|
||||
) -> Result<Match3DGeneratedBackgroundAsset, AppError> {
|
||||
require_match3d_oss_client(state)?;
|
||||
let settings = require_openai_image_settings(state)?;
|
||||
let http_client = build_openai_image_http_client(&settings)?;
|
||||
let reference_image = load_match3d_container_reference_image()?;
|
||||
let generated_background = create_openai_image_generation(
|
||||
|
||||
let level_scene_prompt = build_match3d_level_scene_generation_prompt(config);
|
||||
let generated_scene = create_openai_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
build_match3d_background_generation_prompt(config, prompt).as_str(),
|
||||
Some(
|
||||
"文字、水印、UI、按钮、倒计时、分数、物品、角色、手、边框、教程浮层、菜单、透明区域、透明 alpha、镂空、棋盘格透明底",
|
||||
),
|
||||
level_scene_prompt.as_str(),
|
||||
Some("水印、教程浮层、菜单、广告、真实手机外框、浏览器 UI"),
|
||||
"9:16",
|
||||
1,
|
||||
&[],
|
||||
"抓大鹅背景图生成失败",
|
||||
"抓大鹅关卡画面生成失败",
|
||||
)
|
||||
.await?;
|
||||
let level_scene_image = generated_scene.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "抓大鹅关卡画面生成失败:未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let level_scene_reference = OpenAiReferenceImage {
|
||||
bytes: level_scene_image.bytes.clone(),
|
||||
mime_type: level_scene_image.mime_type.clone(),
|
||||
file_name: "match3d-level-scene.png".to_string(),
|
||||
};
|
||||
let level_scene_upload = persist_match3d_generated_bytes(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
&["level-scene", generated_scene.task_id.as_str()],
|
||||
"scene.png",
|
||||
level_scene_image.mime_type.as_str(),
|
||||
level_scene_image.bytes,
|
||||
"match3d_level_scene_image",
|
||||
Some(generated_scene.task_id.as_str()),
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ui_prompt = build_match3d_ui_spritesheet_prompt();
|
||||
let background_extract_prompt = build_match3d_background_from_scene_prompt();
|
||||
let generated_ui_future = create_openai_image_edit(
|
||||
&http_client,
|
||||
&settings,
|
||||
ui_prompt.as_str(),
|
||||
Some("整页背景、中心物品、容器内物品、重复按钮、文字说明、白底、纯色底、网格线"),
|
||||
"1:1",
|
||||
&level_scene_reference,
|
||||
"抓大鹅 UI spritesheet 生成失败",
|
||||
);
|
||||
let generated_background_future = create_openai_image_edit(
|
||||
&http_client,
|
||||
&settings,
|
||||
background_extract_prompt.as_str(),
|
||||
Some("返回按钮、设置按钮、倒计时、标题文字、道具按钮、物品、容器内含物、菜单、教程浮层"),
|
||||
"9:16",
|
||||
&level_scene_reference,
|
||||
"抓大鹅背景图生成失败",
|
||||
);
|
||||
let (generated_ui, generated_background) =
|
||||
tokio::try_join!(generated_ui_future, generated_background_future)?;
|
||||
|
||||
let ui_image = generated_ui.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "抓大鹅 UI spritesheet 生成失败:未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let ui_image = make_match3d_spritesheet_image_transparent(ui_image)?;
|
||||
let ui_upload = persist_match3d_generated_bytes(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
&["ui-spritesheet", generated_ui.task_id.as_str()],
|
||||
"ui-spritesheet.png",
|
||||
ui_image.mime_type.as_str(),
|
||||
ui_image.bytes,
|
||||
"match3d_ui_spritesheet_image",
|
||||
Some(generated_ui.task_id.as_str()),
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let background_image = generated_background
|
||||
.images
|
||||
.into_iter()
|
||||
@@ -426,50 +560,22 @@ pub(super) async fn generate_match3d_background_image(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
|
||||
let generated_container = create_openai_image_edit(
|
||||
&http_client,
|
||||
&settings,
|
||||
container_prompt.as_str(),
|
||||
Some("文字、水印、按钮、倒计时、分数、物品、角色、手、教程浮层、菜单、整页背景、小容器、正俯视圆盘、侧视碗、餐盘、托盘、画布大留白"),
|
||||
"1:1",
|
||||
&reference_image,
|
||||
"抓大鹅容器 UI 图生成失败",
|
||||
)
|
||||
.await?;
|
||||
let container_image = generated_container
|
||||
.images
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "vector-engine",
|
||||
"message": "抓大鹅容器 UI 图生成失败:未返回图片",
|
||||
}))
|
||||
})?;
|
||||
let container_image = make_match3d_container_image_transparent(container_image)?;
|
||||
let container_upload = persist_match3d_generated_bytes(
|
||||
state,
|
||||
owner_user_id,
|
||||
session_id,
|
||||
profile_id,
|
||||
&["ui-container", generated_container.task_id.as_str()],
|
||||
"container.png",
|
||||
container_image.mime_type.as_str(),
|
||||
container_image.bytes,
|
||||
"match3d_ui_container_image",
|
||||
Some(generated_container.task_id.as_str()),
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Match3DGeneratedBackgroundAsset {
|
||||
prompt: prompt.to_string(),
|
||||
level_scene_prompt: Some(level_scene_prompt),
|
||||
level_scene_image_src: Some(level_scene_upload.src),
|
||||
level_scene_image_object_key: Some(level_scene_upload.object_key),
|
||||
image_src: Some(background_upload.src),
|
||||
image_object_key: Some(background_upload.object_key),
|
||||
container_prompt: Some(container_prompt),
|
||||
container_image_src: Some(container_upload.src),
|
||||
container_image_object_key: Some(container_upload.object_key),
|
||||
ui_spritesheet_prompt: Some(ui_prompt.clone()),
|
||||
ui_spritesheet_image_src: Some(ui_upload.src.clone()),
|
||||
ui_spritesheet_image_object_key: Some(ui_upload.object_key.clone()),
|
||||
item_spritesheet_prompt: None,
|
||||
item_spritesheet_image_src: None,
|
||||
item_spritesheet_image_object_key: None,
|
||||
container_prompt: Some(ui_prompt),
|
||||
container_image_src: Some(ui_upload.src),
|
||||
container_image_object_key: Some(ui_upload.object_key),
|
||||
status: "image_ready".to_string(),
|
||||
error: None,
|
||||
})
|
||||
@@ -533,6 +639,7 @@ pub(super) async fn generate_match3d_container_image(
|
||||
container_image_object_key: Some(container_upload.object_key),
|
||||
status: "image_ready".to_string(),
|
||||
error: None,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -549,12 +656,39 @@ pub(super) fn merge_match3d_container_image_into_background_asset(
|
||||
.unwrap_or_else(|| container_asset.prompt.clone());
|
||||
Match3DGeneratedBackgroundAsset {
|
||||
prompt,
|
||||
level_scene_prompt: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.level_scene_prompt.clone()),
|
||||
level_scene_image_src: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.level_scene_image_src.clone()),
|
||||
level_scene_image_object_key: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.level_scene_image_object_key.clone()),
|
||||
image_src: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.image_src.clone()),
|
||||
image_object_key: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.image_object_key.clone()),
|
||||
ui_spritesheet_prompt: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.ui_spritesheet_prompt.clone()),
|
||||
ui_spritesheet_image_src: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.ui_spritesheet_image_src.clone()),
|
||||
ui_spritesheet_image_object_key: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.ui_spritesheet_image_object_key.clone()),
|
||||
item_spritesheet_prompt: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.item_spritesheet_prompt.clone()),
|
||||
item_spritesheet_image_src: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.item_spritesheet_image_src.clone()),
|
||||
item_spritesheet_image_object_key: existing_background
|
||||
.as_ref()
|
||||
.and_then(|asset| asset.item_spritesheet_image_object_key.clone()),
|
||||
container_prompt: container_asset.container_prompt,
|
||||
container_image_src: container_asset.container_image_src,
|
||||
container_image_object_key: container_asset.container_image_object_key,
|
||||
@@ -582,6 +716,44 @@ pub(super) fn load_match3d_container_reference_image() -> Result<OpenAiReference
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_level_scene_generation_prompt(config: &Match3DConfigJson) -> String {
|
||||
let theme = config.theme_text.trim();
|
||||
let theme = if theme.is_empty() {
|
||||
MATCH3D_DEFAULT_THEME
|
||||
} else {
|
||||
theme
|
||||
};
|
||||
let style_clause = resolve_match3d_asset_style_prompt(config)
|
||||
.map(|style| format!("\n整体美术风格要求:{style}"))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
concat!(
|
||||
"生成抓大鹅游戏关卡画面,要求画面中所有元素精致且风格高度一致,画面中所有UI细节饱满精致、完成度高、顶级游戏品质\n\n",
|
||||
"抓大鹅主题描述:\n",
|
||||
"{theme}{style_clause}\n\n",
|
||||
"画面元素:\n",
|
||||
"返回按钮位于顶部左上角,顶部中间显示关卡标题“第1关 重庆火锅”和倒计时时间,右上角显示设置按钮\n",
|
||||
"画面中间是一个和主题匹配的容器,宽度与画面宽度同宽,紧贴画面横向边缘\n",
|
||||
"底部还有三个道具按钮分别为“移出”、“凑齐”、“打乱”"
|
||||
),
|
||||
theme = theme,
|
||||
style_clause = style_clause,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_ui_spritesheet_prompt() -> String {
|
||||
"提取画面中的UI元素,将返回按钮、设置按钮、方格素材(不含边框,仅保留一个)、移出按钮、凑齐按钮、打乱按钮的顺序从上到下从左到右整理成纯绿色绿幕背景spritesheet。背景必须是统一纯绿色绿幕(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。UI 素材自身不得使用接近 #00FF00 的高饱和纯绿;绿色题材只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。".to_string()
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_background_from_scene_prompt() -> String {
|
||||
"移除画面中的所有UI组件和容器中的内含物,完整保留容器和背景,补全被UI覆盖的背景内容".to_string()
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_item_spritesheet_prompt() -> String {
|
||||
"固定生成10行*10列spritesheet图,统一纯绿色绿幕背景(高饱和亮绿色,接近 #00FF00),背景平整无纹理、无渐变、无阴影、无场景内容,后端会在生图后将绿幕扣成透明并把透明背景 PNG 存到 OSS。素材间距严格均匀分布,任意两个素材间距相同,物品来自参考图中画面中心容器中的2D素材。每一行包含两种物品,每种物品的五个不同形态。物品素材自身不得使用接近 #00FF00 的高饱和纯绿;绿色物品只能使用深绿、橄榄绿、金绿或蓝绿,并用清晰描边与绿幕区分。严禁出现两种高相似度的物品".to_string()
|
||||
}
|
||||
|
||||
pub(super) fn build_match3d_background_generation_prompt(
|
||||
config: &Match3DConfigJson,
|
||||
prompt: &str,
|
||||
@@ -761,6 +933,32 @@ pub(super) fn make_match3d_container_image_transparent(
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn make_match3d_spritesheet_image_transparent(
|
||||
image: DownloadedOpenAiImage,
|
||||
) -> Result<DownloadedOpenAiImage, AppError> {
|
||||
let source = image::load_from_memory(image.bytes.as_slice()).map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "match3d-assets",
|
||||
"message": format!("抓大鹅 spritesheet 图解码失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
let mut encoded = std::io::Cursor::new(Vec::new());
|
||||
apply_generated_asset_sheet_green_screen_alpha(source)
|
||||
.write_to(&mut encoded, ImageFormat::Png)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "match3d-assets",
|
||||
"message": format!("抓大鹅 spritesheet 图透明化失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(DownloadedOpenAiImage {
|
||||
bytes: encoded.into_inner(),
|
||||
mime_type: "image/png".to_string(),
|
||||
extension: "png".to_string(),
|
||||
})
|
||||
}
|
||||
pub(super) async fn download_match3d_legacy_model(
|
||||
file: &hyper3d_contract::Hyper3dDownloadFilePayload,
|
||||
) -> Result<Match3DDownloadedModel, AppError> {
|
||||
@@ -864,7 +1062,7 @@ pub(super) fn is_match3d_glb_binary_payload(bytes: &[u8]) -> bool {
|
||||
magic == 0x4654_6c67 && version == 2 && declared_length == bytes.len()
|
||||
}
|
||||
|
||||
async fn read_match3d_generated_object_bytes(
|
||||
pub(super) async fn read_match3d_generated_object_bytes(
|
||||
state: &AppState,
|
||||
object_key: &str,
|
||||
message_prefix: &str,
|
||||
@@ -915,57 +1113,6 @@ async fn read_match3d_generated_object_bytes(
|
||||
Ok(bytes.to_vec())
|
||||
}
|
||||
|
||||
async fn resolve_match3d_reference_image_data_url(
|
||||
state: &AppState,
|
||||
source: Option<&str>,
|
||||
max_size_bytes: usize,
|
||||
) -> Result<Option<String>, AppError> {
|
||||
let Some(source) = source.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if source.starts_with("data:image/") {
|
||||
return Ok(Some(source.to_string()));
|
||||
}
|
||||
if let Some(public_path) = normalize_match3d_public_reference_image_path(source) {
|
||||
let bytes = tokio::fs::read(public_path.as_str())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": MATCH3D_WORKS_PROVIDER,
|
||||
"message": format!("读取抓大鹅本地参考图失败:{error}"),
|
||||
"path": public_path,
|
||||
}))
|
||||
})?;
|
||||
if bytes.is_empty() || bytes.len() > max_size_bytes {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": MATCH3D_WORKS_PROVIDER,
|
||||
"field": "referenceImageSrcs",
|
||||
"message": "封面参考图过大,请压缩后重试。",
|
||||
"maxBytes": max_size_bytes,
|
||||
"actualBytes": bytes.len(),
|
||||
})),
|
||||
);
|
||||
}
|
||||
return Ok(Some(format!(
|
||||
"data:{};base64,{}",
|
||||
infer_match3d_image_mime_type(bytes.as_slice()),
|
||||
BASE64_STANDARD.encode(bytes)
|
||||
)));
|
||||
}
|
||||
if !source.trim_start_matches('/').starts_with("generated-") {
|
||||
return Ok(Some(source.to_string()));
|
||||
}
|
||||
let bytes =
|
||||
read_match3d_generated_object_bytes(state, source, "读取抓大鹅参考图失败", max_size_bytes)
|
||||
.await?;
|
||||
Ok(Some(format!(
|
||||
"data:{};base64,{}",
|
||||
infer_match3d_image_mime_type(bytes.as_slice()),
|
||||
BASE64_STANDARD.encode(bytes)
|
||||
)))
|
||||
}
|
||||
|
||||
pub(super) fn normalize_match3d_public_reference_image_path(source: &str) -> Option<String> {
|
||||
let source = source
|
||||
.trim()
|
||||
@@ -1018,18 +1165,22 @@ pub(super) fn collect_match3d_cover_reference_image_sources(
|
||||
sources
|
||||
}
|
||||
|
||||
async fn resolve_match3d_cover_reference_image_data_urls(
|
||||
async fn resolve_match3d_cover_reference_images_for_edit(
|
||||
state: &AppState,
|
||||
sources: Vec<String>,
|
||||
max_size_bytes: usize,
|
||||
) -> Result<Vec<String>, AppError> {
|
||||
) -> Result<Vec<OpenAiReferenceImage>, AppError> {
|
||||
let mut resolved = Vec::new();
|
||||
for source in sources {
|
||||
if let Some(data_url) =
|
||||
resolve_match3d_reference_image_data_url(state, Some(source.as_str()), max_size_bytes)
|
||||
.await?
|
||||
for (index, source) in sources.into_iter().enumerate() {
|
||||
if let Some(image) = resolve_match3d_reference_image_for_edit(
|
||||
state,
|
||||
Some(source.as_str()),
|
||||
max_size_bytes,
|
||||
format!("match3d-cover-reference-{index}").as_str(),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
resolved.push(data_url);
|
||||
resolved.push(image);
|
||||
}
|
||||
}
|
||||
Ok(resolved)
|
||||
@@ -1046,6 +1197,16 @@ async fn resolve_match3d_reference_image_for_edit(
|
||||
};
|
||||
let bytes = if source.starts_with("data:image/") {
|
||||
decode_match3d_data_url_bytes(source)?
|
||||
} else if let Some(public_path) = normalize_match3d_public_reference_image_path(source) {
|
||||
tokio::fs::read(public_path.as_str())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
"provider": MATCH3D_WORKS_PROVIDER,
|
||||
"message": format!("读取抓大鹅本地参考图失败:{error}"),
|
||||
"path": public_path,
|
||||
}))
|
||||
})?
|
||||
} else if source.trim_start_matches('/').starts_with("generated-") {
|
||||
read_match3d_generated_object_bytes(
|
||||
state,
|
||||
@@ -1059,7 +1220,7 @@ async fn resolve_match3d_reference_image_for_edit(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": MATCH3D_WORKS_PROVIDER,
|
||||
"field": "uploadedImageSrc",
|
||||
"message": "封面上传图必须是图片 Data URL 或 /generated-* 路径。",
|
||||
"message": "封面参考图必须是图片 Data URL、本地 public 参考图或 /generated-* 路径。",
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -1086,7 +1247,7 @@ async fn resolve_match3d_reference_image_for_edit(
|
||||
}))
|
||||
}
|
||||
|
||||
fn decode_match3d_data_url_bytes(source: &str) -> Result<Vec<u8>, AppError> {
|
||||
pub(super) fn decode_match3d_data_url_bytes(source: &str) -> Result<Vec<u8>, AppError> {
|
||||
let Some((header, data)) = source.split_once(',') else {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
|
||||
Reference in New Issue
Block a user