fix: 提升拼消消素材生成质量门禁

This commit is contained in:
2026-06-04 22:32:46 +08:00
parent b9de2f2a43
commit 0c7fc0b26f
6 changed files with 724 additions and 49 deletions
+11
View File
@@ -1832,6 +1832,17 @@
- 处理:只要移动后棋盘存在空位,就立即走补位和可解性修复;这样源位会从顶部准备区补卡,不会留下不可交互空洞。
- 验证:`npm run test -- src/services/puzzle-clear/puzzleClearLocalRuntime.test.ts``cargo test -p module-puzzle-clear --manifest-path server-rs/Cargo.toml player_move_can_drop_card_into_empty_target_cell -- --nocapture`
- 关联:`src/services/puzzle-clear/puzzleClearLocalRuntime.ts``server-rs/crates/module-puzzle-clear/src/application.rs`
## 拼消消素材错位先查 sheet 质量门禁
- 现象:一张卡牌切片里同时出现两个或多个错位图案,或空白格、相邻编号区域里混入其他图案碎片。
- 原因:provider 生成的 `1024x1536 / 4x6` 工作表可能违反视觉契约;旧流程只校验布局元数据和切片数量,无法发现图像内容已经主体缺失或污染空白格。边界贴边检测容易把正常铺满主体误判成跨格污染,不能作为高可靠硬门禁。
- 处理:先强化 atlas prompt,要求每个 `256x256` 单元独立查看时只能包含一个主体或同一主体单一局部;服务端在 sheet 切片前做像素级质量门禁,硬拦截非空格前景占比过低和空白格污染,严重多边非同组边界贴边只记录 warning 供排查,不直接让创作失败。硬门禁失败的 sheet 最多尝试 4 次,仍失败则拒绝持久化脏 atlas。
- 追加处理:照片式微场景素材必须把每个 `256x256` 单元收束为一张完整的单场景照片裁片;同编号连续格只表示玩法分组,不要暗示 provider 生成横跨多格的大图或照片拼贴。禁止单格内部出现两张照片、两个不同场景、拼接线、内部竖切、内部横切或左右 / 上下两块不同背景;质量门禁需要扫描单格内部贯穿大部分高度或宽度的强色差直线,命中时按“单格内部疑似拼接线”硬失败并重试 sheet。
- 追加处理:sheet 生成时如果 VectorEngine 返回 `retryable=true``502``504``429` 或请求超时,例如 nginx HTML `502 Bad Gateway`,不要立刻把草稿置为 failed,应消耗同一 sheet 的下一次 attempt;仍失败再回写失败状态。
- 验证:`cargo test -p api-server puzzle_clear --manifest-path server-rs/Cargo.toml -- --nocapture``cargo check -p api-server --manifest-path server-rs/Cargo.toml`
- 关联:`server-rs/crates/api-server/src/puzzle_clear.rs``docs/technical/【玩法创作】拼消消玩法模板技术方案-2026-05-30.md`
## 拼消消锁定组覆盖层必须锚定在棋盘本身
- 现象:消除或补牌过程中,局部完成的组图偶尔会看起来从格子里“飘出去”,并且大小会随着窗口和外层面板变化而异常拉伸。
@@ -46,9 +46,16 @@
验证策略:
- 生图 prompt 明确禁止文字、水印、UI、边框标签、切分线、网格线、裁切参考线和跨格主体
- 复合图案组本身不画任何可见分割辅助线,但 prompt 必须说明每个 `1x2``1x3``2x2``2x3` 图案都能被服务端按等大的 1x1 方形单元切分;纵向 `1x2` 按横向切线分成两个 1x1,小图案内不显示切线;横向 `1x2` 按纵向切线分成两个 1x1,小图案内不显示切线;其他形状同理
- 服务端保留 `PuzzleClearPatternGroup` 坐标清单,切片前校验每个 sheet 编号出现次数等于领域图案组 `width * height`,并要求同编号区域是完整连续矩形;切片后还应对尺寸、非空像素比例和重复 hash 做校验
- 生图 prompt 明确要求照片式构图 / 绘本式渲染的主题微场景拼图卡,每个 256x256 单元本身就是一张完整的单场景照片裁片,单元内部只能有一个连续画面,禁止出现两张照片、两个不同场景、拼接线、分割线、内部竖切、内部横切、左右 / 上下两块不同背景,场景变化只能发生在 256 单元边界上
- 同编号连续格只表示玩法上的同组关系,不再暗示 provider 把连续区域画成一张横跨多格的大图;同组格子用色调、道具和背景线索呼应,但每个 256x256 单元独立查看时都必须完整成图
- 同一张 sheet 内不同编号必须发散成不同视觉概念;以水果为例,应扩展为果园、集市摊位、野餐布、果汁杯、厨房案板、甜品盘、篮筐、玻璃罐、窗边餐桌、花园背景等微场景,禁止同品种主体换角度、换大小或换姿势后重复出现
- 每个 256x256 小卡切片独立查看时也要有可辨识的背景纹理、桌面、草地、天空、建筑、布料、器皿、叶片、阴影或装饰元素,避免“孤立主体 + 纯色背景”导致运行态难区分。
- 生图 prompt 明确禁止文字、水印、UI、边框标签、切分线、网格线、裁切参考线、纯色背景、白底商品图、孤立主体、同品种重复和同一物体多角度。
- 复合图案组本身不画任何可见分割辅助线,但 prompt 必须说明每个 `1x2``1x3``2x2``2x3` 图案都能被服务端按等大的 1x1 方形单元切分;纵向 `1x2` 按横向切线分成两个 1x1,横向 `1x2` 按纵向切线分成两个 1x1,其他形状同理。图案组可以在语义上成组,但不能把一张大图的照片边界或拼贴边界落在单个 1x1 单元内部。
- 服务端保留 `PuzzleClearPatternGroup` 坐标清单,切片前校验每个 sheet 编号出现次数等于领域图案组 `width * height`,并要求同编号区域是完整连续矩形。
- 每张 sheet 生成后、正式切片前执行像素级质量门禁:非空格必须达到最低前景占比,空白格前景占比不得超阈值,单格内部不得出现贯穿大部分高度或宽度的强色差拼接线;非同组边界前景贴边仅记录为质量提示,不作为硬失败,避免把模型正常铺满主体的图集误杀。
- 每张 sheet 生成最多尝试 4 次;除质量门禁失败外,VectorEngine 返回 `retryable=true``502``504``429` 或请求超时也应消耗下一次 sheet attempt,避免上游 nginx 偶发 502 或单次拼贴式坏图直接把草稿置为 failed。
- sheet 多次生成仍未通过硬质量门禁时,生成任务进入 `failed` 并写入错误原因;不得把明显空白格污染或主体缺失的工作表切成正式卡牌资产。
- 首版若当前 provider 无法稳定产出可切 atlas,生成任务进入 `failed`,错误写入审计;不得退回前端假素材或绕过平台资产底座。
- 草稿编译和作品发布都必须拒绝缺失 atlas、缺失卡牌切片、空 `assetObjectId` / `imageObjectKey``placeholder` 占位资产;`spacetime-client` 不再为编译请求合成默认 atlas / card assets。
- 技术回退需要用户确认后才能改成更多 sheet、降低切片规格或改为逐图生成;当前需求固定为 4 张 `1024x1536` sheet 与最终 `2560x2560` atlas。
+1 -2
View File
@@ -4080,8 +4080,7 @@ mod tests {
.await
.expect("banners body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("banners payload should be json");
let payload: Value = serde_json::from_slice(&body).expect("banners payload should be json");
assert_eq!(payload["eventBanners"][0]["title"], "后台表单公告");
assert_eq!(payload["eventBanners"][0]["renderMode"], "html");
@@ -77,17 +77,13 @@ pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
{
return Some("puzzle");
}
if normalized.starts_with("/api/runtime/puzzle/gallery/")
&& normalized.ends_with("/remix")
{
if normalized.starts_with("/api/runtime/puzzle/gallery/") && normalized.ends_with("/remix") {
return Some("puzzle");
}
if normalized == "/api/runtime/big-fish/agent/sessions" {
return Some("big-fish");
}
if normalized.starts_with("/api/runtime/big-fish/gallery/")
&& normalized.ends_with("/remix")
{
if normalized.starts_with("/api/runtime/big-fish/gallery/") && normalized.ends_with("/remix") {
return Some("big-fish");
}
if normalized == "/api/runtime/custom-world/agent/sessions"
@@ -42,6 +42,10 @@ impl AppError {
&self.message
}
pub fn details(&self) -> Option<&Value> {
self.details.as_ref()
}
pub fn body_text(&self) -> String {
// 批处理任务不能只读 HTTP 状态文案,否则 DashScope 返回的真实失败原因会被压成“上游服务请求失败”。
self.details
File diff suppressed because it is too large Load Diff