fix(match3d): embed container reference image
This commit is contained in:
@@ -836,7 +836,7 @@
|
|||||||
|
|
||||||
- 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像 `pot-fused-reference.png`,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。
|
- 现象:抓大鹅结果页看似有容器生成入口,但真实生成出的局内容器不像 `pot-fused-reference.png`,或进入试玩后仍被默认圆形锅壳、金色边框和径向底色覆盖/裁切。
|
||||||
- 原因:`/v1/images/generations` 的 `image` 数组更适合弱参考文生图,难以稳定锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 `rounded-full` 锅壳和 `overflow-hidden`,生成图也会被默认视觉覆盖或裁掉。
|
- 原因:`/v1/images/generations` 的 `image` 数组更适合弱参考文生图,难以稳定锁定大尺寸轻俯视容器构图;即使生成了容器图,如果运行态继续保留默认 `rounded-full` 锅壳和 `overflow-hidden`,生成图也会被默认视觉覆盖或裁掉。
|
||||||
- 处理:抓大鹅 `1:1` 容器 UI 图必须用 VectorEngine `POST /v1/images/edits` multipart,把 `public/match3d-background-references/pot-fused-reference.png` 作为 `image` part 上传;共享 GPT-image-2 HTTP client 承载 multipart 时强制 HTTP/1.1。`Match3DRuntimeShell` 在容器图换签并成功加载后,把棋盘外壳切为透明和 `overflow-visible`,只在容器缺失或加载失败时使用默认圆形容器。
|
- 处理:抓大鹅 `1:1` 容器 UI 图必须用 VectorEngine `POST /v1/images/edits` multipart,参考 `public/match3d-background-references/pot-fused-reference.png` 的透明容器图作为 `image` part;该参考图属于后端生图协议输入,需通过 `include_bytes!` 编译进 `api-server`,不能在运行时按当前工作目录读取 `public/`。共享 GPT-image-2 HTTP client 承载 multipart 时强制 HTTP/1.1。`Match3DRuntimeShell` 在容器图换签并成功加载后,把棋盘外壳切为透明和 `overflow-visible`,只在容器缺失或加载失败时使用默认圆形容器。
|
||||||
- 验证:执行 `cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml`、`npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/match3d-result/Match3DResultView.test.tsx`;真实联调看容器生成请求是否命中 `/v1/images/edits`,局内 `match3d-container-image` 是否渲染且 `match3d-board` 不再含默认 `rounded-full`。
|
- 验证:执行 `cargo test -p api-server vector_engine --manifest-path server-rs/Cargo.toml`、`cargo test -p api-server match3d_background --manifest-path server-rs/Cargo.toml`、`npm run test -- src/components/match3d-runtime/Match3DRuntimeShell.test.tsx src/components/match3d-result/Match3DResultView.test.tsx`;真实联调看容器生成请求是否命中 `/v1/images/edits`,局内 `match3d-container-image` 是否渲染且 `match3d-board` 不再含默认 `rounded-full`。
|
||||||
- 关联:`server-rs/crates/api-server/src/openai_image_generation.rs`、`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
- 关联:`server-rs/crates/api-server/src/openai_image_generation.rs`、`server-rs/crates/api-server/src/match3d.rs`、`src/components/match3d-runtime/Match3DRuntimeShell.tsx`、`docs/technical/MATCH3D_DRAFT_ASSET_GENERATION_PIPELINE_2026-05-10.md`。
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ npm run check:server-rs-ddd
|
|||||||
- 图片生成:VectorEngine / APIMart / DashScope,密钥只在后端环境变量中。
|
- 图片生成:VectorEngine / APIMart / DashScope,密钥只在后端环境变量中。
|
||||||
- Match3D 物品 sheet:VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`;图集 prompt、切图、透明化和切片持久化走 `generated_asset_sheets` 通用模块,Match3D 只补题材 / 风格 / 五视角设定和字段映射。
|
- Match3D 物品 sheet:VectorEngine Gemini `gemini-3-pro-image-preview` 原生 `generateContent`;图集 prompt、切图、透明化和切片持久化走 `generated_asset_sheets` 通用模块,Match3D 只补题材 / 风格 / 五视角设定和字段映射。
|
||||||
- Match3D 封面和 9:16 纯背景:VectorEngine `/v1/images/generations`。
|
- Match3D 封面和 9:16 纯背景:VectorEngine `/v1/images/generations`。
|
||||||
- Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。
|
- Match3D 1:1 容器 UI:VectorEngine `/v1/images/edits` multipart 参考图。该容器参考图是后端生图协议输入,必须通过 `include_bytes!` 随 `api-server` 编译进二进制,避免 API 单独发布或运行目录缺少 `public/` 时生成失败。
|
||||||
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。
|
- Hyper3D / Rodin:只保留后端安全代理和旧数据兼容;新 Match3D 草稿和批量新增不再生成 GLB。
|
||||||
- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`。
|
- 音频:视觉小说专用音频路由保留;拼图和抓大鹅生成入口暂时关闭,通用 `/api/creation/audio/*` 对相关目标返回 `410 Gone`。
|
||||||
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。
|
- OSS:私有 generated legacy path 进入浏览器前必须通过 `/api/assets/read-url` 换签;不要裸请求 `/generated-*`。
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
6. 切图前先在整张 sheet 上做绿幕 / 近白底透明化和边缘去污染,再按格子导出独立 PNG;每个视角图再以扩大的 PNG 边界带为种子,把连通的浅绿 / 近白抗锯齿边直接改为透明,并对贴透明背景的弱绿 / 暗绿轮廓像素做去绿污染处理,最后按剩余可见主体二次收紧;不要先裁剪单格再各自去绿。
|
6. 切图前先在整张 sheet 上做绿幕 / 近白底透明化和边缘去污染,再按格子导出独立 PNG;每个视角图再以扩大的 PNG 边界带为种子,把连通的浅绿 / 近白抗锯齿边直接改为透明,并对贴透明背景的弱绿 / 暗绿轮廓像素做去绿污染处理,最后按剩余可见主体二次收紧;不要先裁剪单格再各自去绿。
|
||||||
7. `generatedItemAssets[].imageViews[]` 是新素材主字段,`imageSrc/imageObjectKey` 只兼容首张视角。
|
7. `generatedItemAssets[].imageViews[]` 是新素材主字段,`imageSrc/imageObjectKey` 只兼容首张视角。
|
||||||
8. 文本生成物品名称时必须同时生成 `itemSize`,只允许 `大`、`中`、`小`。该字段随 `generatedItemAssets[].itemSize` 持久化并下发;历史缺失字段的素材按 `大` 兼容,模型缺失或非法值按物品名本地推断。
|
8. 文本生成物品名称时必须同时生成 `itemSize`,只允许 `大`、`中`、`小`。该字段随 `generatedItemAssets[].itemSize` 持久化并下发;历史缺失字段的素材按 `大` 兼容,模型缺失或非法值按物品名本地推断。
|
||||||
9. 局内 `9:16` 纯背景图和 `1:1` 中心容器 UI 图分开生成。纯背景不得包含锅、盘、托盘、HUD、按钮、文字或物品,且入库前必须合成为全画幅不透明图片,不允许出现透明区域;容器图走 `/v1/images/edits` 参考透明容器图。
|
9. 局内 `9:16` 纯背景图和 `1:1` 中心容器 UI 图分开生成。纯背景不得包含锅、盘、托盘、HUD、按钮、文字或物品,且入库前必须合成为全画幅不透明图片,不允许出现透明区域;容器图走 `/v1/images/edits` 参考透明容器图。该容器参考图由后端内嵌到 `api-server`,不要依赖运行时当前目录下的 `public/` 文件。
|
||||||
10. 当前抓大鹅音频生成关闭:入口无 `生成音效`,草稿不生成背景音乐或点击音效,结果页不展示背景音乐 Tab 或点击音效生成入口。历史 `backgroundMusic` / `clickSound` 字段继续兼容传递。
|
10. 当前抓大鹅音频生成关闭:入口无 `生成音效`,草稿不生成背景音乐或点击音效,结果页不展示背景音乐 Tab 或点击音效生成入口。历史 `backgroundMusic` / `clickSound` 字段继续兼容传递。
|
||||||
11. UI 背景和容器资产的持久化真相仍在 `generatedItemAssets[].backgroundAsset`;Agent session、work summary/detail、结果页和运行态入口都必须把该字段提升为 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset` 读取。草稿编译后的 `draftJson` 自身也必须携带 `generatedItemAssets` 快照;HTTP facade 不能只依赖 work detail 回读补齐 UI 资产,外部回读为空时也不得清空草稿内已有的背景 / 容器图。平台壳层从作品架、广场、生成完成回调、结果页保存 / 发布 / 试玩回调进入 Match3D profile 时也要先归一化并提升,避免首次试玩、手动试玩、推荐流或公开详情运行态退回默认背景 / 默认容器。
|
11. UI 背景和容器资产的持久化真相仍在 `generatedItemAssets[].backgroundAsset`;Agent session、work summary/detail、结果页和运行态入口都必须把该字段提升为 `backgroundImageSrc/backgroundImageObjectKey/generatedBackgroundAsset` 读取。草稿编译后的 `draftJson` 自身也必须携带 `generatedItemAssets` 快照;HTTP facade 不能只依赖 work detail 回读补齐 UI 资产,外部回读为空时也不得清空草稿内已有的背景 / 容器图。平台壳层从作品架、广场、生成完成回调、结果页保存 / 发布 / 试玩回调进入 Match3D profile 时也要先归一化并提升,避免首次试玩、手动试玩、推荐流或公开详情运行态退回默认背景 / 默认容器。
|
||||||
|
|
||||||
|
|||||||
@@ -109,8 +109,10 @@ const MATCH3D_WORK_METADATA_LLM_MODEL: &str = "gpt-4o";
|
|||||||
const MATCH3D_ITEM_SIZE_LARGE: &str = "大";
|
const MATCH3D_ITEM_SIZE_LARGE: &str = "大";
|
||||||
const MATCH3D_ITEM_SIZE_MEDIUM: &str = "中";
|
const MATCH3D_ITEM_SIZE_MEDIUM: &str = "中";
|
||||||
const MATCH3D_ITEM_SIZE_SMALL: &str = "小";
|
const MATCH3D_ITEM_SIZE_SMALL: &str = "小";
|
||||||
const MATCH3D_CONTAINER_REFERENCE_IMAGE_PATH: &str =
|
const MATCH3D_CONTAINER_REFERENCE_IMAGE_BYTES: &[u8] = include_bytes!(
|
||||||
"public/match3d-background-references/pot-fused-reference.png";
|
"../../../../public/match3d-background-references/pot-fused-reference.png"
|
||||||
|
);
|
||||||
|
const MATCH3D_PUBLIC_REFERENCE_IMAGE_PATH_PREFIX: &str = "public/";
|
||||||
const MATCH3D_QUESTION_THEME: &str = "你想创作什么题材";
|
const MATCH3D_QUESTION_THEME: &str = "你想创作什么题材";
|
||||||
const MATCH3D_QUESTION_CLEAR_COUNT: &str = "需要消除多少次才能通关";
|
const MATCH3D_QUESTION_CLEAR_COUNT: &str = "需要消除多少次才能通关";
|
||||||
const MATCH3D_QUESTION_DIFFICULTY: &str = "如果难度是从1-10,你要创作的关卡是难度几";
|
const MATCH3D_QUESTION_DIFFICULTY: &str = "如果难度是从1-10,你要创作的关卡是难度几";
|
||||||
|
|||||||
@@ -1156,6 +1156,19 @@ fn match3d_public_reference_image_paths_are_limited_to_known_assets() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match3d_container_reference_image_is_embedded_for_api_only_deploy() {
|
||||||
|
let reference = load_match3d_container_reference_image()
|
||||||
|
.expect("container reference image should be compiled into api-server");
|
||||||
|
|
||||||
|
assert_eq!(reference.mime_type, "image/png");
|
||||||
|
assert_eq!(reference.file_name, "match3d-container-reference.png");
|
||||||
|
assert!(
|
||||||
|
reference.bytes.starts_with(&[137, 80, 78, 71, 13, 10, 26, 10]),
|
||||||
|
"container reference image should be PNG bytes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn match3d_cover_reference_prompt_marks_reference_images() {
|
fn match3d_cover_reference_prompt_marks_reference_images() {
|
||||||
let prompt = build_match3d_cover_reference_generation_prompt("水果封面", true);
|
let prompt = build_match3d_cover_reference_generation_prompt("水果封面", true);
|
||||||
@@ -1684,44 +1697,44 @@ fn match3d_required_item_images_require_five_views() {
|
|||||||
assert!(!has_match3d_required_item_images(&assets, 3));
|
assert!(!has_match3d_required_item_images(&assets, 3));
|
||||||
|
|
||||||
let five_view_assets = (1..=3)
|
let five_view_assets = (1..=3)
|
||||||
.map(|index| Match3DGeneratedItemAsset {
|
.map(|index| Match3DGeneratedItemAsset {
|
||||||
item_id: format!("match3d-item-{index}"),
|
item_id: format!("match3d-item-{index}"),
|
||||||
item_name: format!("物品{index}"),
|
item_name: format!("物品{index}"),
|
||||||
item_size: Some(MATCH3D_ITEM_SIZE_LARGE.to_string()),
|
item_size: Some(MATCH3D_ITEM_SIZE_LARGE.to_string()),
|
||||||
image_src: Some(format!(
|
image_src: Some(format!(
|
||||||
"/generated-match3d-assets/s/p/items/i{index}/views/view-01.png"
|
"/generated-match3d-assets/s/p/items/i{index}/views/view-01.png"
|
||||||
)),
|
)),
|
||||||
image_object_key: Some(format!(
|
image_object_key: Some(format!(
|
||||||
"generated-match3d-assets/s/p/items/i{index}/views/view-01.png"
|
"generated-match3d-assets/s/p/items/i{index}/views/view-01.png"
|
||||||
)),
|
)),
|
||||||
image_views: (1..=MATCH3D_ITEM_VIEW_COUNT)
|
image_views: (1..=MATCH3D_ITEM_VIEW_COUNT)
|
||||||
.map(|view_index| Match3DGeneratedItemImageView {
|
.map(|view_index| Match3DGeneratedItemImageView {
|
||||||
view_id: format!("view-{view_index:02}"),
|
view_id: format!("view-{view_index:02}"),
|
||||||
view_index: view_index as u32,
|
view_index: view_index as u32,
|
||||||
image_src: Some(format!(
|
image_src: Some(format!(
|
||||||
"/generated-match3d-assets/s/p/items/i{index}/views/view-{view_index:02}.png"
|
"/generated-match3d-assets/s/p/items/i{index}/views/view-{view_index:02}.png"
|
||||||
)),
|
)),
|
||||||
image_object_key: Some(format!(
|
image_object_key: Some(format!(
|
||||||
"generated-match3d-assets/s/p/items/i{index}/views/view-{view_index:02}.png"
|
"generated-match3d-assets/s/p/items/i{index}/views/view-{view_index:02}.png"
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
model_src: None,
|
model_src: None,
|
||||||
model_object_key: None,
|
model_object_key: None,
|
||||||
model_file_name: None,
|
model_file_name: None,
|
||||||
task_uuid: None,
|
task_uuid: None,
|
||||||
subscription_key: None,
|
subscription_key: None,
|
||||||
sound_prompt: None,
|
sound_prompt: None,
|
||||||
background_music_title: None,
|
background_music_title: None,
|
||||||
background_music_style: None,
|
background_music_style: None,
|
||||||
background_music_prompt: None,
|
background_music_prompt: None,
|
||||||
background_music: None,
|
background_music: None,
|
||||||
click_sound: None,
|
click_sound: None,
|
||||||
background_asset: None,
|
background_asset: None,
|
||||||
status: "image_ready".to_string(),
|
status: "image_ready".to_string(),
|
||||||
error: None,
|
error: None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
assert!(has_match3d_required_item_images(&five_view_assets, 3));
|
assert!(has_match3d_required_item_images(&five_view_assets, 3));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ pub(super) async fn generate_match3d_background_image(
|
|||||||
require_match3d_oss_client(state)?;
|
require_match3d_oss_client(state)?;
|
||||||
let settings = require_openai_image_settings(state)?;
|
let settings = require_openai_image_settings(state)?;
|
||||||
let http_client = build_openai_image_http_client(&settings)?;
|
let http_client = build_openai_image_http_client(&settings)?;
|
||||||
let reference_image = load_match3d_container_reference_image().await?;
|
let reference_image = load_match3d_container_reference_image()?;
|
||||||
let generated_background = create_openai_image_generation(
|
let generated_background = create_openai_image_generation(
|
||||||
&http_client,
|
&http_client,
|
||||||
&settings,
|
&settings,
|
||||||
@@ -486,7 +486,7 @@ pub(super) async fn generate_match3d_container_image(
|
|||||||
require_match3d_oss_client(state)?;
|
require_match3d_oss_client(state)?;
|
||||||
let settings = require_openai_image_settings(state)?;
|
let settings = require_openai_image_settings(state)?;
|
||||||
let http_client = build_openai_image_http_client(&settings)?;
|
let http_client = build_openai_image_http_client(&settings)?;
|
||||||
let reference_image = load_match3d_container_reference_image().await?;
|
let reference_image = load_match3d_container_reference_image()?;
|
||||||
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
|
let container_prompt = build_match3d_container_generation_prompt(config, prompt);
|
||||||
let generated_container = create_openai_image_edit(
|
let generated_container = create_openai_image_edit(
|
||||||
&http_client,
|
&http_client,
|
||||||
@@ -563,15 +563,10 @@ pub(super) fn merge_match3d_container_image_into_background_asset(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_match3d_container_reference_image() -> Result<OpenAiReferenceImage, AppError> {
|
pub(super) fn load_match3d_container_reference_image() -> Result<OpenAiReferenceImage, AppError> {
|
||||||
let bytes = tokio::fs::read(MATCH3D_CONTAINER_REFERENCE_IMAGE_PATH)
|
// 中文注释:生产 API 单独发布二进制,Web 静态资源可能在另一轮流水线发布。
|
||||||
.await
|
// 容器参考图属于后端生图协议输入,必须随 api-server 编译进二进制,不能依赖运行时 cwd 下存在 public/。
|
||||||
.map_err(|error| {
|
let bytes = MATCH3D_CONTAINER_REFERENCE_IMAGE_BYTES.to_vec();
|
||||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
|
||||||
"provider": MATCH3D_AGENT_PROVIDER,
|
|
||||||
"message": format!("读取抓大鹅容器参考图失败:{error}"),
|
|
||||||
}))
|
|
||||||
})?;
|
|
||||||
if bytes.is_empty() {
|
if bytes.is_empty() {
|
||||||
return Err(
|
return Err(
|
||||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||||
@@ -992,7 +987,9 @@ pub(super) fn normalize_match3d_public_reference_image_path(source: &str) -> Opt
|
|||||||
) {
|
) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(format!("public/{source}"))
|
Some(format!(
|
||||||
|
"{MATCH3D_PUBLIC_REFERENCE_IMAGE_PATH_PREFIX}{source}"
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn collect_match3d_cover_reference_image_sources(
|
pub(super) fn collect_match3d_cover_reference_image_sources(
|
||||||
|
|||||||
Reference in New Issue
Block a user