From 9cc36ea99ccd59c21b247016ea34565716ee8825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=94=E9=A6=99=E4=B8=B8=E5=AD=90?= <15518898337@163.com> Date: Wed, 27 May 2026 21:56:19 +0800 Subject: [PATCH] Refine creation progress and wooden fish runtime --- .hermes/shared-memory/decision-log.md | 16 +++ .hermes/shared-memory/pitfalls.md | 12 +- ...€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md | 6 +- ...玩法创作】生æˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md | 6 +- .../crates/api-server/src/wooden_fish.rs | 11 +- .../spacetime-module/src/wooden_fish.rs | 135 +++++++++++++++++- .../CustomWorldGenerationView.test.tsx | 15 +- src/components/GenerationProgressHero.tsx | 25 +++- .../BarkBattleGeneratingView.test.tsx | 15 +- 9 files changed, 209 insertions(+), 32 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 1495b21d..f1e7957c 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-05-27 生æˆé¡µæ€»è¿›åº¦åœ†å¼§é”定固定画布 + +- 背景:多轮圆环角度微调åŽï¼Œ`GenerationProgressHero` çš„ SVG 圆弧ä»ä¼šå‡ºçŽ°åº•éƒ¨å¼€å£å斜的问题,且圆环还会éšç€å®¹å™¨å®½åº¦ä¼¸ç¼©ï¼Œå¯¼è‡´ UI çœ‹èµ·æ¥æ—¶å¤§æ—¶å°ã€ä½ç½®æ¼‚移。 +- 决策:共用 `GenerationProgressHero` çš„ SVG 圆弧起始角固定为 `135deg`,轨é“和橘黄色填充都从åŒä¸€ä¸ªå¯¹ç§°èµ·ç‚¹ `rotate(135 200 200)` 出å‘ï¼›`270deg` 扫æè§’é…åˆæ­£ä¸‹æ–¹ `90deg` 留空,圆环本体改为固定 `400x400` 画布,ä¸å†è·Ÿéšé¡µé¢å®½åº¦ç¼©æ”¾ï¼Œå¤–层布局åªè´Ÿè´£å®šä½ï¼Œä¸è´Ÿè´£æ”¹åŠ¨åœ†çŽ¯æ ·å¼ã€‚ +- å½±å“范围:`src/components/GenerationProgressHero.tsx`ã€å…±ç”¨ `CustomWorldGenerationView`ã€æ±ªæ±ªå£°æµª `BarkBattleGeneratingView` 以åŠç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€æ–‡æ¡£ã€‚ +- éªŒè¯æ–¹å¼ï¼š`CustomWorldGenerationView` å’Œ `BarkBattleGeneratingView` 测试断言 `data-ring-start-degrees=135`ã€`data-ring-fill-start-degrees=135`,且圆环容器固定为 `h-[400px] w-[400px]`,track / fill transform 都是 `rotate(135 200 200)`。 +- å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md`。 + ## 2026-05-26 å¹³å°è·¨æµç¨‹é”™è¯¯ç»Ÿä¸€ç”¨å¯å¤åˆ¶æ¥æºå¼¹çª—展示 - 背景:拼图等生æˆé“¾è·¯å¯èƒ½åŒæ—¶å­˜åœ¨å¤šä¸ªè‰ç¨¿æˆ–游玩实例,页é¢å†…裸错误 banner å®¹æ˜“è®©ç”¨æˆ·è¯¯ä»¥ä¸ºå½“å‰æ­£åœ¨çœ‹çš„æ‹¼å›¾å¤±è´¥ï¼Œä¹Ÿä¸æ–¹ä¾¿å¤åˆ¶å®Œæ•´é”™è¯¯ç»™å¼€å‘排查。 @@ -201,6 +209,14 @@ - éªŒè¯æ–¹å¼ï¼šèƒŒæ™¯ prompt 啿µ‹åº”包å«ä¸­å¤®ç¦åŒºç¡¬çº¦æŸï¼Œè¯•玩图中央ä¸å†å‡ºçŽ°è‹¹æžœæˆ–å…¶å®ƒä¸»é¢˜ä¸»ä½“ã€‚ - å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 +## 2026-05-27 敲木鱼背景 prompt ä¸å†å†™ä¸­å¤®æœ¨é±¼é¢„设 + +- 背景:背景 prompt 曾写入“木鱼预设在å±å¹•中央ä½ç½®â€ï¼Œä¸Žâ€œèƒŒæ™¯å›¾ä¸­ä¸åŒ…嫿–°æœ¨é±¼ç‰©å“â€â€œä¸­å¤® 40% ç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“â€ç›´æŽ¥å†²çªï¼Œå¯¼è‡´ image2 å¶å‘æŠŠé™æ€æœ¨é±¼ç”»å›žèƒŒæ™¯ä¸­å¿ƒã€‚ +- 决策:背景 prompt åªèƒ½å†™â€œä¸­å¤®ä¸»ä½“预留区â€â€œè¿è¡Œæ€å æ”¾æ•²å‡»ç‰©çš„留白区域â€â€œåªç”ŸæˆèƒŒæ™¯çŽ¯å¢ƒå›¾â€ï¼Œä¸å¾—å†å‡ºçŽ°â€œæœ¨é±¼é¢„è®¾åœ¨å±å¹•中央ä½ç½®â€æˆ–ä»»ä½•ç­‰ä»·çš„ä¸­å¿ƒä¸»ä½“æ­£å‘æè¿°ã€‚ +- å½±å“范围:`server-rs/crates/api-server/src/wooden_fish.rs`ã€æ•²æœ¨é±¼ PRDã€å¹³å°é“¾è·¯æ–‡æ¡£ã€èƒŒæ™¯ prompt 啿µ‹ã€‚ +- éªŒè¯æ–¹å¼ï¼š`wooden_fish_background_prompt_uses_hidden_image2_flow` 必须断言旧冲çªå¥å­ä¸å­˜åœ¨ï¼Œå¹¶æ–­è¨€æ–°çš„中央留白表述存在。 +- å…³è”æ–‡æ¡£ï¼š`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## 2026-05-21 外部 API 失败必须 OTLP 上报并è½åº“ - 背景:图片生æˆç­‰å¤–部供应商调用失败时,仅返回 502/504 或普通日志无法支æŒåŽç»­æŒ‰ providerã€é˜¶æ®µå’Œé‡è¯•属性èšåˆæŽ’障。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 4529c30c..89db8320 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -170,7 +170,7 @@ - 现象:苹果等主题试玩时,中央敲击物图带明显黑底;背景图中央还å¯èƒ½å‡ºçŽ°è‹¹æžœä¸»ä½“ï¼Œæˆ–èƒŒæ™¯çŽ¯å¢ƒå›¾å¶å‘å˜æˆçº¯ç»¿è‰²åº•,和“中央åªå åŠ  hitObjectAssetâ€çš„è¿è¡Œæ€è®¾å®šå†²çªã€‚ - 原因:gpt-image-2 å¯¹â€œé€æ˜Žåº•â€å’Œâ€œèƒŒæ™¯åªåšå¤–围氛围â€çš„éµå¾ªä¸ç¨³å®šã€‚è‹¥ hit object 直接入库,黑底会被当æˆçœŸå®žåƒç´ å±•示;若背景 prompt åªæœ‰è½¯æè¿°ï¼Œæ¨¡åž‹ä¼šæŠŠä¸»é¢˜ä¸»ä½“画进中央。第一步为了去背刻æ„è¦æ±‚绿幕图时,如果第二步å‚考图或 prompt 没有切断绿幕语义,背景图也å¯èƒ½ç»§æ‰¿çº¯ç»¿è‰²ç”»å¸ƒã€‚ -- 处ç†ï¼šæ•²æœ¨é±¼ hit object prompt å›ºå®šè¦æ±‚先输出 `1:1` 绿色背景主体图(纯绿色绿幕ã€å•一 `#00FF00` 背景),å†ç”± `api-server` åªå¯¹ç»¿å¹•背景åšåŽ»ç»¿é€æ˜ŽåŒ–ï¼›ä¸è¦å›žåˆ°é»‘底 / 白底 / 逿˜Žåº• prompt åŽå†åšæ³›æŠ å›¾ã€‚èƒŒæ™¯ç”Ÿæˆå¿…须使用第一步抠图完æˆåŽçš„逿˜Žå›¾ä½œä¸ºå‚考图,并在 prompt 中显å¼ç¦æ­¢ç»§æ‰¿ç»¿è‰²åº•色ã€ç»¿å¹•底色或纯绿色画布;背景 prompt 还è¦å›ºå®šè¦æ±‚中央 40% ä¸»ä½“é¢„ç•™åŒºå¹²å‡€ï¼Œç¦æ­¢ä¸»é¢˜ä¸»ä½“ã€å±€éƒ¨ç‰¹å†™ã€è½®å»“å½±å­ã€é‡å¤å…ƒç´ å’Œä¸»é¢˜ç¢Žç‰‡ï¼Œåªå…许外围氛围。 +- 处ç†ï¼šæ•²æœ¨é±¼ hit object prompt å›ºå®šè¦æ±‚先输出 `1:1` 绿色背景主体图(纯绿色绿幕ã€å•一 `#00FF00` 背景),å†ç”± `api-server` åªå¯¹ç»¿å¹•背景åšåŽ»ç»¿é€æ˜ŽåŒ–ï¼›ä¸è¦å›žåˆ°é»‘底 / 白底 / 逿˜Žåº• prompt åŽå†åšæ³›æŠ å›¾ã€‚èƒŒæ™¯ç”Ÿæˆå¿…须使用第一步抠图完æˆåŽçš„逿˜Žå›¾ä½œä¸ºå‚考图,并在 prompt 中显å¼ç¦æ­¢ç»§æ‰¿ç»¿è‰²åº•色ã€ç»¿å¹•底色或纯绿色画布;背景 prompt 还è¦å›ºå®šè¦æ±‚中央 40% ä¸»ä½“é¢„ç•™åŒºå¹²å‡€ï¼Œç¦æ­¢ä¸»é¢˜ä¸»ä½“ã€å±€éƒ¨ç‰¹å†™ã€è½®å»“å½±å­ã€é‡å¤å…ƒç´ å’Œä¸»é¢˜ç¢Žç‰‡ï¼Œåªå…许外围氛围。ä¸è¦åœ¨èƒŒæ™¯ prompt 写“木鱼预设在å±å¹•中央ä½ç½®â€æˆ–ç±»ä¼¼ä¸­å¿ƒä¸»ä½“æ­£å‘æè¿°ï¼Œè¿è¡Œæ€æ•²å‡»ç‰©åªèƒ½ç”±å‰ç«¯å æ”¾ã€‚ - 验è¯ï¼š`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`,并用花朵 / 苹果 / 玉米主题跑试玩图确认绿幕被去除ã€ä¸»ä½“未被抠除ã€èƒŒæ™¯ä¸­å¤®ä¸å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ï¼ŒèƒŒæ™¯çŽ¯å¢ƒå›¾ä¸å†å‡ºçŽ°çº¯ç»¿è‰²åº•ã€‚ - å…³è”:`server-rs/crates/api-server/src/wooden_fish.rs`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 @@ -182,6 +182,14 @@ - 验è¯ï¼š`cargo test -p api-server wooden_fish --manifest-path server-rs\Cargo.toml`ï¼Œå¹¶é‡æ–°è¯•玩确认返回按钮åªå‰©åœ†å½¢åº•色和中央左箭头。 - å…³è”:`server-rs/crates/api-server/src/wooden_fish.rs`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`. +## 敲木鱼历å²å·²å‘布作å“缺返回按钮è¦è¡¥é½ï¼Œä¸è¦é æŽ¨è过滤 + +- 现象:推èé¡µæˆ–å…¬å¼€åˆ—è¡¨ä¸­çš„åŽ†å²æ•²æœ¨é±¼ä½œå“点击è¿è¡Œæ€æ—¶æŠ¥ `敲木鱼è¿è¡Œæ€éœ€è¦å®Œæ•´ä½œå“é…ç½®`,但这类作å“的敲击物ã€èƒŒæ™¯ã€éŸ³æ•ˆå’Œé£˜å­—éƒ½å·²å®Œæ•´ï¼Œåªæ˜¯ `backButtonAsset` 为空。 +- 原因:早期已å‘布作å“缺少统一的默认返回按钮快照;è¿è¡Œæ€å¯åŠ¨æ—¶å¦‚æžœä»ç›´æŽ¥æŒ‰å®Œæ•´é…置校验,就会把å¯çŽ©çš„åŽ†å²ä½œå“拒掉。这个问题ä¸åº”é€šè¿‡æŽ¨èæµæˆ–å…¬å¼€åˆ—è¡¨è¿‡æ»¤è§£å†³ã€‚ +- 处ç†ï¼š`spacetime-module` 在 `start_wooden_fish_run_tx` å’Œ work snapshot 构建时,若作å“å·²å‘布且 `generationStatus=ready`,但仅缺 `backButtonAsset`,就补写内置默认返回按钮 `/UI/11_left_arrow.png`,å†ç»§ç»­è¿›å…¥è¿è¡Œæ€ã€‚默认返回按钮以 `bundled-default` 资产快照写回 work profileï¼Œå­—æ®µä¿æŒ `assetId=wooden-fish-default-back-button`ã€`imageObjectKey=public/UI/11_left_arrow.png`。 +- 验è¯ï¼šåކ岿œ¨é±¼ä½œå“点击è¿è¡Œæ€ä¸å†æŠ¥å®Œæ•´ä½œå“é…置缺失;第一次进入åŽï¼Œwork profile 里应补出 `backButtonAsset`。 +- å…³è”:`server-rs/crates/spacetime-module/src/wooden_fish.rs`ã€`docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 + ## 敲木鱼创作生æˆä¸è¦æ²¿ç”¨ 15 秒会è¯è¶…æ—¶ - 现象:敲木鱼工作å°ç‚¹å‡»â€œç”Ÿæˆâ€åŽï¼Œå‰ç«¯ç›´æŽ¥æç¤º `请求超时:15000ms`,但åŽç«¯å’Œ VectorEngine 未必已ç»å¤±è´¥ã€‚ @@ -1567,6 +1575,8 @@ 2026-05-24 补充:生æˆé¡µâ€œé¢„计等待 / 已耗时â€å¡ç‰‡æœ¬èº«å·²ç»æœ‰æ ‡ç­¾ï¼Œä¼ ç»™ `GenerationProgressHero` 的值åªèƒ½æ˜¯çº¯æ—¶é—´ï¼Œä¾‹å¦‚ `4 分钟`ã€`1 分 15 ç§’`,ä¸è¦å†æ‹¼æŽ¥â€œé¢„è®¡è¿˜éœ€â€æˆ–“已耗时â€ï¼›ä¸¤å¼ æ—¶é—´å¡ä¹Ÿè¦å’Œå½“剿­¥éª¤å¡ä¸€æ ·ä¿æŒåŠé€æ˜Žã€‚拼图总进度åˆå§‹å¸§å¿…é¡»å…许显示 `0%`,ä¸è¦å†ç”¨ `Math.max(1, nextProgress)` ä¹‹ç±»çš„ä¿æŠ¤æŠŠå¯åŠ¨æ€æŠ¬åˆ° `1%`。 +2026-05-27 补充:`generation-hero-progress-ring-fill` 里那个橘黄色å°ç‚¹ä¸æ˜¯èƒŒæ™¯å™ªç‚¹ï¼Œè€Œæ˜¯ `strokeLinecap="round"` 在短弧段上的端点;当å‰åœ†çޝå£å¾„è¦æ±‚底部 `90deg` å¼€å£å±…中对称,因此轨é“和填充都应使用 `135deg` 起点。圆环本体现在固定为 `400x400`,排查时先看 `data-ring-start-degrees`ã€`data-ring-fill-start-degrees` 和容器尺寸,ä¸è¦æŠŠå°ºå¯¸ä¼¸ç¼©è¯¯è®¤æˆç´ ææ¸²æŸ“问题。 + ## `dev:spacetime` å¯åŠ¨åŽ 3101 åˆæ–­å¼€å…ˆæŸ¥ publish 是å¦è¢« spacetime.json 干扰 - 现象:æµè§ˆå™¨æŠ¥ `Failed to initiate WebSocket connection`,目标为 `ws://127.0.0.1:3101/v1/database//subscribe`ï¼Œç«¯å£æ£€æŸ¥å‘现 `3101` 没有长期监å¬ï¼›æ‰‹åЍè¿è¡Œ `npm run dev:spacetime` å¯çœ‹åˆ° standalone 短暂å¯åЍåŽé€€å‡ºï¼Œå‘布阶段报 `No database target matches ''`。 diff --git a/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md b/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md index fa2c56f6..6bf23f8e 100644 --- a/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md +++ b/docs/prd/ã€çŽ©æ³•åˆ›ä½œã€‘æ•²æœ¨é±¼çŽ©æ³•æ¨¡æ¿PRD-2026-05-20.md @@ -166,11 +166,11 @@ WF-* 1. 调用 VectorEngine `/v1/images/edits`,模型固定为 `gpt-image-2`ï¼› 2. multipart å‚考图固定为第一步敲击物图案抠图完æˆåŽçš„逿˜Žå›¾ï¼›é»˜è®¤æœªç”Ÿæˆæ–°æ•²å‡»ç‰©æ—¶ä½¿ç”¨å†…ç½®é»˜è®¤æ•²å‡»ç‰©å›¾æ¡ˆçš„é€æ˜Žå…œåº•图; 3. å°ºå¯¸å›ºå®šç«–å± `9:16`ï¼› -4. 背景环境图åªé€‚é…æ–°æ•²å‡»ç‰©ä¸»é¢˜å’Œç”»é£Žï¼ŒèƒŒæ™¯ä¸­ä¸å¾—åŒ…å«æ–°æ•²å‡»ç‰©æœ¬ä½“,也ä¸å¾—增加木槌互动物å“ï¼›ä¸­å¤®ä¸»ä½“é¢„ç•™åŒºå¿…é¡»ä¿æŒå¹²å‡€ï¼Œç”»é¢ä¸­å¤® 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€ä¸»ä½“轮廓影å­ã€é‡å¤å…ƒç´ æˆ–主题主体的局部碎片; +4. 背景环境图åªé€‚é…æ–°æ•²å‡»ç‰©ä¸»é¢˜å’Œç”»é£Žï¼ŒèƒŒæ™¯ä¸­ä¸å¾—åŒ…å«æ–°æ•²å‡»ç‰©æœ¬ä½“,也ä¸å¾—增加木槌互动物å“ï¼›ä¸­å¤®ä¸»ä½“é¢„ç•™åŒºå¿…é¡»ä¿æŒå¹²å‡€ï¼Œç”»é¢ä¸­å¤® 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€ä¸»ä½“轮廓影å­ã€é‡å¤å…ƒç´ æˆ–主题主体的局部碎片;è¿è¡Œæ€çš„æ•²å‡»ç‰©åªåœ¨å‰ç«¯å æ”¾ï¼Œä¸å…许出现在背景图æç¤ºè¯é‡Œã€‚ 5. æç¤ºè¯ä¸¥æ ¼ä½¿ç”¨ï¼š ```text -ç”Ÿæˆæ•²æœ¨é±¼èƒŒæ™¯ï¼Œè¦æ±‚主题,画风与å‚è€ƒå›¾ä¿æŒé«˜åº¦ä¸€è‡´ï¼ŒèƒŒæ™¯å…ƒç´ å’Œé¢œè‰²æ­é…与主题对应,木鱼预设在å±å¹•中央ä½ç½®ï¼Œæœ¨é±¼ä¸»ä½“å‘¨å›´å…ƒç´ ä¿æŒå¹²å‡€ï¼ŒèƒŒæ™¯æ°›å›´å›´ç»•外围设计,背景环境图中ä¸åŒ…嫿–°æœ¨é±¼ç‰©å“,背景氛围中ä¸å¢žåŠ æœ¨æ§Œäº’åŠ¨ç‰©å“。尺寸竖å±9:16。å‚考图必须是第一步敲击物抠图完æˆåŽçš„逿˜Žå›¾ï¼Œä¸ç»§æ‰¿ä»»ä½•绿色底色ã€ç»¿å¹•åº•è‰²æˆ–çº¯ç»¿è‰²ç”»å¸ƒï¼Œå¹¶è¦æ±‚最终输出完整ä¸é€æ˜Žçš„èƒŒæ™¯çŽ¯å¢ƒå›¾ã€‚ä¸­å¤®ä¸»ä½“é¢„ç•™åŒºå¿…é¡»ä¿æŒå¹²å‡€ï¼Œç”»é¢ä¸­å¤® 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€ä¸»ä½“轮廓影å­ã€é‡å¤å…ƒç´ æˆ–主题主体的局部碎片;主题元素åªå…许出现在外围氛围,ä¸å¾—把主题物å“画在画é¢ä¸­å¤®ï¼Œä¹Ÿä¸è¦æŠŠä¸»é¢˜ç‰©å“作为背景中心装饰。 +ç”Ÿæˆæ•²æœ¨é±¼èƒŒæ™¯ï¼Œè¦æ±‚主题ã€ç”»é£Žä¸Žå‚è€ƒå›¾ä¿æŒé«˜åº¦ä¸€è‡´ï¼ŒèƒŒæ™¯å…ƒç´ å’Œé¢œè‰²æ­é…与主题对应,åªç”Ÿæˆç«–å±èƒŒæ™¯çŽ¯å¢ƒå›¾ï¼Œä¸ç”Ÿæˆã€ä¸æç»˜ã€ä¸æš—ç¤ºæ–°æœ¨é±¼ç‰©å“æœ¬ä½“,也ä¸è¦å‡ºçŽ°æœ¨æ§Œäº’åŠ¨ç‰©å“。尺寸竖å±9:16。å‚考图必须是第一步敲击物抠图完æˆåŽçš„逿˜Žå›¾ï¼Œä¸ç»§æ‰¿ä»»ä½•绿色底色ã€ç»¿å¹•åº•è‰²æˆ–çº¯ç»¿è‰²ç”»å¸ƒï¼Œå¹¶è¦æ±‚最终输出完整ä¸é€æ˜Žçš„èƒŒæ™¯çŽ¯å¢ƒå›¾ã€‚ä¸­å¤®ä¸»ä½“é¢„ç•™åŒºå¿…é¡»ä¿æŒå¹²å‡€ï¼Œä¸­å¤®åŒºåŸŸæ˜¯è¿è¡Œæ€å æ”¾æ•²å‡»ç‰©çš„留白区域,画é¢ä¸­å¤® 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€ä¸»ä½“轮廓影å­ã€é‡å¤å…ƒç´ æˆ–主题主体的局部碎片;主题元素åªå…许出现在外围氛围,ä¸å¾—把主题物å“画在画é¢ä¸­å¤®ï¼Œä¹Ÿä¸è¦æŠŠä¸»é¢˜ç‰©å“作为背景中心装饰。 主题为:(用户æä¾›å‚考图或用户输入关键è¯ï¼‰ ``` @@ -384,6 +384,8 @@ finish 公开列表优先消费 `wooden_fish_gallery_card_view` 订阅缓存。公开详情如果å¡ç‰‡æ‘˜è¦ä¸è¶³ä»¥è¿›å…¥è¿è¡Œæ€ï¼Œå¿…须补读完整 work profile。 +历å²å·²å‘布且 `generationStatus=ready` 的木鱼作å“如果仅缺 `backButtonAsset`,è¿è¡Œæ€å¯åЍå‰å¿…须补é½å†…置默认返回按钮 `/UI/11_left_arrow.png`,并æŒä¹…写回 work profile;这类历å²ä½œå“ä¸åº”é€šè¿‡æŽ¨èæµè¿‡æ»¤éšè—。 + ## 13. 验收 1. 创作入å£èƒ½çœ‹åˆ° `敲木鱼` 模æ¿ï¼› diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md index ef97e819..760a55d7 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘ç”Ÿæˆé¡µåœ†çŽ¯å¸ƒå±€å£å¾„-2026-05-23.md @@ -12,8 +12,8 @@ - 生æˆé¡µèƒŒæ™¯è§†é¢‘必须留在生æˆé¡µå®¹å™¨å†…部,直接作为 `fixed inset-0` 的底层背景,ä¸è¦å†é€šè¿‡ portal 挂到 `document.body`ï¼›é¡µé¢æ ¹å®¹å™¨ä½¿ç”¨ `z-[1]`ã€èƒŒæ™¯å®¹å™¨ä½¿ç”¨ `z-0`,确ä¿é¡¶éƒ¨å¯¼èˆªã€åœ†çŽ¯å’Œå½“å‰æ­¥éª¤å¡éƒ½ç¨³å®šè¦†ç›–在视频之上。 - 预计等待 / 已耗时信æ¯å¡è¦åŽ‹ç¼©ä¸ºæ›´è½»çš„åŠé€æ˜Žçª„å¡ï¼Œæ ‡ç­¾ä½¿ç”¨ `9px-10px`,数值使用 `12px-13px`,字å·å¯¹é½å…¶ä»–生æˆé¡µ UI çš„å°å­—å·ï¼Œä¸å†ä½¿ç”¨å大的æç¤ºæ–‡æœ¬ï¼›å¡ç‰‡æ ‡é¢˜å’Œæ—¶é—´å€¼éƒ½å±…中显示,两个数值åªå±•示时间本身,调用侧ä¸è¦å†æ‹¼æŽ¥â€œé¢„è®¡è¿˜éœ€â€æˆ–“已耗时â€å‰ç¼€ã€‚圆环中心ä¸å†ä¿ç•™ç‹¬ç«‹ç™½åº•å—,空心圆环åªä¿ç•™æ¡çŠ¶è¿›åº¦ï¼Œåœ†å¼§åŠå¾„ç»§ç»­åŠ å¤§ï¼Œè¿›åº¦æ•°å­—ä¸Žâ€œæ€»è¿›åº¦â€æ ‡é¢˜æ•´ä½“上移,é è¿‘圆环上åŠåŒºã€‚ - 顶部导航区采用“返回创作中心 / 状æ€èƒ¶å›Šâ€ç»“构,返回按钮使用左箭头图标,字å·ä½¿ç”¨ `text-xs-sm`,状æ€èƒ¶å›Šä½¿ç”¨ `11px-12px`,展示 `ç´ æç”Ÿæˆä¸­`ã€`è‰ç¨¿ç”Ÿæˆä¸­` 等调用侧传入文案。 -- 圆弧区域ä¸å†åŒ…独立大å¡ç‰‡ï¼Œå·¦å³æ‚¬æµ®ä¿¡æ¯å¡åªå±•示“预计等待â€å’Œâ€œå·²è€—æ—¶â€ï¼›æ€»è¿›åº¦æ•°å€¼æ”¾åœ¨åœ†å¼§å†…ä¾§å上的ä½ç½®å¹¶ä¿æŒæ›´å°å­—å·ã€‚当å‰åœ†çޝ外径以 `w-[min(35rem,94vw)] sm:w-[52rem]` 为基准,圆弧使用 `r=166`ã€`strokeWidth=18` çš„ SVG æè¾¹ï¼Œä¸å†ä½¿ç”¨ `conic-gradient + mask`,é¿å…进度æ¡è¾¹ç¼˜æ¨¡ç³Šã€‚ -- 圆弧æè¾¹ä»¥åœ†å¿ƒä¸ºä¸­å¿ƒæ•´ä½“按 `155deg` èµ·å§‹ï¼›åœ¨å½“å‰ SVG åæ ‡ç³»ä¸‹ï¼Œè¿™ç›¸å¯¹ `160deg` 会å‘左逆时针回调 `5deg`。track å’Œ fill 都必须共用åŒä¸€ä¸ª `rotate(155 200 200)` å˜æ¢ï¼Œé¿å…åªæ”¹è§†è§‰èµ·ç‚¹å´è®©å¡«å……和轨é“é”™ä½ã€‚ +- 圆弧区域ä¸å†åŒ…独立大å¡ç‰‡ï¼Œå·¦å³æ‚¬æµ®ä¿¡æ¯å¡åªå±•示“预计等待â€å’Œâ€œå·²è€—æ—¶â€ï¼›æ€»è¿›åº¦æ•°å€¼æ”¾åœ¨åœ†å¼§å†…ä¾§å上的ä½ç½®å¹¶ä¿æŒæ›´å°å­—å·ã€‚圆环本体固定在 `400x400` çš„ SVG 画布上,圆弧使用 `r=166`ã€`strokeWidth=18` çš„ SVG æè¾¹ï¼Œä¸å†è·Ÿéšé¡µé¢å®½åº¦ç¼©æ”¾ï¼Œä¹Ÿä¸å†ä½¿ç”¨ `conic-gradient + mask`,é¿å…进度æ¡è¾¹ç¼˜æ¨¡ç³Šã€‚ +- 圆弧æè¾¹ä»¥åœ†å¿ƒä¸ºä¸­å¿ƒæ•´ä½“按 `135deg` èµ·å§‹ï¼›`270deg` 扫æè§’é…åˆ `90deg` æ­£ä¸‹æ–¹ç¼ºå£æ—¶ï¼Œè½¨é“和填充都从åŒä¸€ä¸ªå¯¹ç§°èµ·ç‚¹å‡ºå‘,轨é“ä¿æŒ `rotate(135 200 200)`,填充端点也使用 `rotate(135 200 200)`。圆环本体尺寸固定,ä¸å…许å†éšå®¹å™¨è¾¹é•¿ä¼¸ç¼©ï¼Œåªèƒ½ç”±å¤–层布局决定放置ä½ç½®ã€‚ - 总进度标题和百分比数字必须显å¼é«˜äºŽ SVG 圆环层级渲染,é¿å…被圆环边缘压ä½ï¼›åœ†çŽ¯æœ¬èº«åªåšèƒŒæ™¯å±‚ï¼Œä¸æŠ¢æ–‡å­—å±‚ã€‚ - æ€»è¿›åº¦æ ‡é¢˜å’Œç™¾åˆ†æ¯”æ•°å­—è¦æ¯”圆环å†ä¸Šç§»ä¸€ç‚¹ï¼Œå½“å‰å†…容区上边è·ä»¥ `pt-[2%]` 为准,桌é¢ç«¯å¯è¿›ä¸€æ­¥å¾®è°ƒåˆ° `sm:pt-[1.5%]`ï¼Œç¡®ä¿æ•°å­—ä¸ä¸Žè¿›åº¦æ¡å¼§çº¿é‡åˆã€‚ - ä»Žä½œå“æž¶æˆ–刷新åŽçš„æŒä¹…åŒ–ç”Ÿæˆä¸­è‰ç¨¿è¿›å…¥ç”Ÿæˆé¡µæ—¶ï¼Œå‰ç«¯å¿…é¡»é‡ç½®â€œå±•ç¤ºæ€ startedAtMsâ€ä¸ºè¿›å…¥ç”Ÿæˆé¡µçš„当剿—¶é—´ï¼›åŽç«¯ `progressPercent` åªç”¨äºŽåŽç»­çœŸå®žæ­¥éª¤æŽ¨è¿›ï¼Œä¸å¾—å‚与首帧总进度展示,é¿å…æ¢å¤ç”Ÿæˆé¡µé¦–帧直接显示 `80%+`。 @@ -27,5 +27,5 @@ - `src/components/CustomWorldGenerationView.test.tsx` è¦†ç›–åœ†çŽ¯ä¸»è§†è§‰å’Œå•æ­¥å¡ç‰‡ã€‚ - `src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx` 覆盖汪汪声浪生æˆé¡µå¯¹é½åŽçš„圆环布局。 - 两个生æˆé¡µéƒ½åº”åœ¨æµ‹è¯•é‡Œæ–­è¨€é¡µé¢æ ¹å®¹å™¨å±‚级高于背景视频容器,且背景视频确实是页é¢å­èŠ‚ç‚¹ï¼Œé¿å… portal 背景把业务 UI 压ä½ã€‚ -- 还应断言圆弧正下方留空ã€åœ†çŽ¯ä¸­å¿ƒæ²¡æœ‰ç‹¬ç«‹åº•è‰²å—,时间å¡å’Œæ€»è¿›åº¦å­—å·ç¼©å°åŽä»èƒ½åœ¨æ¡Œé¢ä¸Žç§»åŠ¨ç«¯æ­£å¸¸æŽ’ç‰ˆï¼›åŒæ—¶æ–­è¨€æ—¶é—´å¡ `text-center`ã€æ ‡é¢˜è¡Œ `justify-center`ã€æ€»è¿›åº¦å†…容区上移到 `pt-[4%]`,圆弧 DOM 为 SVGï¼ŒåŒ…å«æ¸…æ™°çš„ track/fill circle æè¾¹ã€‚ +- 还应断言圆弧正下方留空ã€åœ†çŽ¯ä¸­å¿ƒæ²¡æœ‰ç‹¬ç«‹åº•è‰²å—,时间å¡å’Œæ€»è¿›åº¦å­—å·ç¼©å°åŽä»èƒ½åœ¨æ¡Œé¢ä¸Žç§»åŠ¨ç«¯æ­£å¸¸æŽ’ç‰ˆï¼›åŒæ—¶æ–­è¨€æ—¶é—´å¡ `text-center`ã€æ ‡é¢˜è¡Œ `justify-center`ã€æ€»è¿›åº¦å†…容区上移到 `pt-[2%]`,桌é¢ç«¯ä¿æŒ `sm:pt-[1.5%]`,圆弧 DOM 为 SVGï¼ŒåŒ…å«æ¸…æ™°çš„ track/fill circle æè¾¹ã€‚ - 页é¢åœ¨æ¡Œé¢å’Œç§»åŠ¨ç«¯éƒ½ä¸åº”å†å‡ºçŽ°ç”Ÿæˆæ­¥éª¤åˆ—表å—ï¼Œåœ†çŽ¯å’Œå½“å‰æ­¥éª¤å¡ä¸èƒ½è¢«å¤–层å¡ç‰‡åµŒå¥—出åŒå±‚颿¿æ„Ÿã€‚ diff --git a/server-rs/crates/api-server/src/wooden_fish.rs b/server-rs/crates/api-server/src/wooden_fish.rs index 7763ea0e..4a818996 100644 --- a/server-rs/crates/api-server/src/wooden_fish.rs +++ b/server-rs/crates/api-server/src/wooden_fish.rs @@ -758,7 +758,7 @@ fn build_wooden_fish_hit_object_prompt(prompt: &str) -> String { fn build_wooden_fish_background_prompt(prompt: &str) -> String { format!( - "ç”Ÿæˆæ•²æœ¨é±¼èƒŒæ™¯ï¼Œè¦æ±‚主题,画风与å‚è€ƒå›¾ä¿æŒé«˜åº¦ä¸€è‡´ï¼ŒèƒŒæ™¯å…ƒç´ å’Œé¢œè‰²æ­é…与主题对应,木鱼预设在å±å¹•中央ä½ç½®ï¼Œæœ¨é±¼ä¸»ä½“å‘¨å›´å…ƒç´ ä¿æŒå¹²å‡€ï¼ŒèƒŒæ™¯æ°›å›´å›´ç»•外围设计,背景环境图中ä¸åŒ…嫿–°æœ¨é±¼ç‰©å“,背景氛围中ä¸å¢žåŠ æœ¨æ§Œäº’åŠ¨ç‰©å“。尺寸竖å±9:16。å‚考图必须是第一步敲击物抠图完æˆåŽçš„逿˜Žå›¾ï¼Œä¸ç»§æ‰¿ä»»ä½•绿色底色ã€ç»¿å¹•åº•è‰²æˆ–çº¯ç»¿è‰²ç”»å¸ƒï¼Œå¹¶è¦æ±‚最终输出完整ä¸é€æ˜Žçš„èƒŒæ™¯çŽ¯å¢ƒå›¾ã€‚ä¸­å¤®ä¸»ä½“é¢„ç•™åŒºå¿…é¡»ä¿æŒå¹²å‡€ï¼Œç”»é¢ä¸­å¤® 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€ä¸»ä½“轮廓影å­ã€é‡å¤å…ƒç´ æˆ–主题主体的局部碎片;主题元素åªå…许出现在外围氛围,ä¸å¾—把主题物å“画在画é¢ä¸­å¤®ï¼Œä¹Ÿä¸è¦æŠŠä¸»é¢˜ç‰©å“作为背景中心装饰。\n主题为:{}", + "ç”Ÿæˆæ•²æœ¨é±¼èƒŒæ™¯ï¼Œè¦æ±‚主题ã€ç”»é£Žä¸Žå‚è€ƒå›¾ä¿æŒé«˜åº¦ä¸€è‡´ï¼ŒèƒŒæ™¯å…ƒç´ å’Œé¢œè‰²æ­é…与主题对应,åªç”Ÿæˆç«–å±èƒŒæ™¯çŽ¯å¢ƒå›¾ï¼Œä¸ç”Ÿæˆã€ä¸æç»˜ã€ä¸æš—ç¤ºæ–°æœ¨é±¼ç‰©å“æœ¬ä½“,也ä¸è¦å‡ºçŽ°æœ¨æ§Œäº’åŠ¨ç‰©å“。尺寸竖å±9:16。å‚考图必须是第一步敲击物抠图完æˆåŽçš„逿˜Žå›¾ï¼Œä¸ç»§æ‰¿ä»»ä½•绿色底色ã€ç»¿å¹•åº•è‰²æˆ–çº¯ç»¿è‰²ç”»å¸ƒï¼Œå¹¶è¦æ±‚最终输出完整ä¸é€æ˜Žçš„èƒŒæ™¯çŽ¯å¢ƒå›¾ã€‚ä¸­å¤®ä¸»ä½“é¢„ç•™åŒºå¿…é¡»ä¿æŒå¹²å‡€ï¼Œä¸­å¤®åŒºåŸŸæ˜¯è¿è¡Œæ€å æ”¾æ•²å‡»ç‰©çš„留白区域,画é¢ä¸­å¤® 40% åŒºåŸŸç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“ã€ä¸»ä½“局部特写ã€ä¸»ä½“轮廓影å­ã€é‡å¤å…ƒç´ æˆ–主题主体的局部碎片;主题元素åªå…许出现在外围氛围,ä¸å¾—把主题物å“画在画é¢ä¸­å¤®ï¼Œä¹Ÿä¸è¦æŠŠä¸»é¢˜ç‰©å“作为背景中心装饰。\n主题为:{}", clean_string(prompt, DEFAULT_HIT_OBJECT_PROMPT) ) } @@ -1228,14 +1228,17 @@ mod tests { fn wooden_fish_background_prompt_uses_hidden_image2_flow() { let prompt = build_wooden_fish_background_prompt("苹果"); - assert!(prompt.contains( - "ç”Ÿæˆæ•²æœ¨é±¼èƒŒæ™¯ï¼Œè¦æ±‚主题,画风与å‚è€ƒå›¾ä¿æŒé«˜åº¦ä¸€è‡´ï¼ŒèƒŒæ™¯å…ƒç´ å’Œé¢œè‰²æ­é…与主题对应,木鱼预设在å±å¹•中央ä½ç½®ï¼Œæœ¨é±¼ä¸»ä½“å‘¨å›´å…ƒç´ ä¿æŒå¹²å‡€ï¼ŒèƒŒæ™¯æ°›å›´å›´ç»•外围设计,背景环境图中ä¸åŒ…嫿–°æœ¨é±¼ç‰©å“,背景氛围中ä¸å¢žåŠ æœ¨æ§Œäº’åŠ¨ç‰©å“。" - )); + assert!(prompt.contains("åªç”Ÿæˆç«–å±èƒŒæ™¯çŽ¯å¢ƒå›¾")); + assert!(prompt.contains("ä¸ç”Ÿæˆã€ä¸æç»˜ã€ä¸æš—ç¤ºæ–°æœ¨é±¼ç‰©å“æœ¬ä½“")); + assert!(prompt.contains("ä¸è¦å‡ºçŽ°æœ¨æ§Œäº’åŠ¨ç‰©å“")); + assert!(!prompt.contains("木鱼预设在å±å¹•中央ä½ç½®")); + assert!(!prompt.contains("æœ¨é±¼ä¸»ä½“å‘¨å›´å…ƒç´ ä¿æŒå¹²å‡€")); assert!(prompt.contains("尺寸竖å±9:16")); assert!(prompt.contains("抠图完æˆåŽçš„逿˜Žå›¾")); assert!(prompt.contains("ä¸ç»§æ‰¿ä»»ä½•绿色底色")); assert!(prompt.contains("完整ä¸é€æ˜Žçš„背景环境图")); assert!(prompt.contains("中央主体预留区")); + assert!(prompt.contains("中央区域是è¿è¡Œæ€å æ”¾æ•²å‡»ç‰©çš„留白区域")); assert!(prompt.contains("ç¦æ­¢å‡ºçŽ°ä¸»é¢˜ä¸»ä½“")); assert!(prompt.contains("苹果")); assert!(prompt.contains("ä¸å¾—把主题物å“画在画é¢ä¸­å¤®")); diff --git a/server-rs/crates/spacetime-module/src/wooden_fish.rs b/server-rs/crates/spacetime-module/src/wooden_fish.rs index a8ef6954..18232d9e 100644 --- a/server-rs/crates/spacetime-module/src/wooden_fish.rs +++ b/server-rs/crates/spacetime-module/src/wooden_fish.rs @@ -13,6 +13,10 @@ use serde::Serialize; use serde::de::DeserializeOwned; use spacetimedb::AnonymousViewContext; +const DEFAULT_WOODEN_FISH_BACK_BUTTON_ASSET_ID: &str = "wooden-fish-default-back-button"; +const DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_SRC: &str = "/UI/11_left_arrow.png"; +const DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_OBJECT_KEY: &str = "public/UI/11_left_arrow.png"; + #[spacetimedb::view(accessor = wooden_fish_gallery_view, public)] pub fn wooden_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec { let mut items = ctx @@ -593,10 +597,14 @@ fn start_wooden_fish_run_tx( input: WoodenFishRunStartInput, ) -> Result { require_non_empty(&input.run_id, "wooden_fish run_id")?; - let work = find_work(ctx, &input.profile_id)?; + let stored_work = find_work(ctx, &input.profile_id)?; + let work = backfill_historical_runtime_content(&stored_work); if !is_publish_ready(&work) { return Err("敲木鱼è¿è¡Œæ€éœ€è¦å®Œæ•´ä½œå“é…ç½®".to_string()); } + if work.back_button_asset_json != stored_work.back_button_asset_json { + replace_work(ctx, &stored_work, clone_work(&work)); + } let snapshot = WoodenFishRunSnapshot { run_id: input.run_id.clone(), profile_id: input.profile_id.clone(), @@ -740,6 +748,7 @@ fn build_session_snapshot( } fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result { + let row = backfill_historical_runtime_content(row); Ok(WoodenFishWorkSnapshot { work_id: row.work_id.clone(), profile_id: row.profile_id.clone(), @@ -775,7 +784,7 @@ fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result bool { + is_publish_ready_except_back_button(row) + && row + .back_button_asset_json + .as_deref() + .and_then(clean_optional) + .is_some() +} + +fn is_publish_ready_except_back_button(row: &WoodenFishWorkProfileRow) -> bool { !row.work_title.trim().is_empty() && !row.hit_object_asset_json.trim().is_empty() && row @@ -1016,14 +1034,40 @@ fn is_publish_ready(row: &WoodenFishWorkProfileRow) -> bool { .as_deref() .and_then(clean_optional) .is_some() - && row + && !row.hit_sound_asset_json.trim().is_empty() + && !row.floating_words_json.trim().is_empty() + && row.generation_status == WOODEN_FISH_GENERATION_READY +} + +fn backfill_historical_runtime_content(row: &WoodenFishWorkProfileRow) -> WoodenFishWorkProfileRow { + if row.publication_status != WOODEN_FISH_PUBLICATION_PUBLISHED + || !is_publish_ready_except_back_button(row) + || row .back_button_asset_json .as_deref() .and_then(clean_optional) .is_some() - && !row.hit_sound_asset_json.trim().is_empty() - && !row.floating_words_json.trim().is_empty() - && row.generation_status == WOODEN_FISH_GENERATION_READY + { + return clone_work(row); + } + + WoodenFishWorkProfileRow { + back_button_asset_json: Some(to_json_string(&default_wooden_fish_back_button_asset())), + ..clone_work(row) + } +} + +fn default_wooden_fish_back_button_asset() -> WoodenFishImageAssetSnapshot { + WoodenFishImageAssetSnapshot { + asset_id: DEFAULT_WOODEN_FISH_BACK_BUTTON_ASSET_ID.to_string(), + image_src: DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_SRC.to_string(), + image_object_key: DEFAULT_WOODEN_FISH_BACK_BUTTON_IMAGE_OBJECT_KEY.to_string(), + asset_object_id: DEFAULT_WOODEN_FISH_BACK_BUTTON_ASSET_ID.to_string(), + generation_provider: "bundled-default".to_string(), + prompt: "åŽ†å²æ•²æœ¨é±¼é»˜è®¤è¿”回按钮".to_string(), + width: 28, + height: 28, + } } fn default_config_from_input( @@ -1288,3 +1332,82 @@ fn clone_run(row: &WoodenFishRuntimeRunRow) -> WoodenFishRuntimeRunRow { updated_at: row.updated_at, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn historical_published_work_without_back_button_gets_runtime_backfill() { + let row = published_ready_work_without_back_button(); + + assert!(!is_publish_ready(&row)); + let repaired = backfill_historical_runtime_content(&row); + let snapshot = build_work_snapshot(&repaired).expect("历å²ä½œå“è¡¥é½åŽåº”坿˜ å°„è¿è¡Œæ€å¿«ç…§"); + + assert!(is_publish_ready(&repaired)); + assert!(snapshot.publish_ready); + assert_eq!( + snapshot + .back_button_asset + .as_ref() + .map(|asset| asset.image_src.as_str()), + Some("/UI/11_left_arrow.png") + ); + } + + fn published_ready_work_without_back_button() -> WoodenFishWorkProfileRow { + let now = Timestamp::from_micros_since_unix_epoch(1_770_000_000_000_000); + WoodenFishWorkProfileRow { + profile_id: "wooden-fish-profile-history".to_string(), + work_id: "wooden-fish-profile-history".to_string(), + owner_user_id: "user-history".to_string(), + source_session_id: "wooden-fish-session-history".to_string(), + author_display_name: "敲木鱼玩家".to_string(), + work_title: "今日敲木鱼".to_string(), + work_description: String::new(), + theme_tags_json: to_json_string(&vec!["敲木鱼".to_string(), "解压".to_string()]), + hit_object_prompt: "é»˜è®¤æ•²å‡»ç‰©å›¾æ¡ˆï¼Œåœ†æ¶¦æœ¨è´¨è´¨æ„Ÿï¼Œé€æ˜ŽèƒŒæ™¯".to_string(), + hit_object_reference_image_src: String::new(), + hit_sound_prompt: String::new(), + hit_object_asset_json: to_json_string(&WoodenFishImageAssetSnapshot { + asset_id: "wooden-fish-hit-object-history".to_string(), + image_src: "/wooden-fish/default-hit-object.png".to_string(), + image_object_key: "public/wooden-fish/default-hit-object.png".to_string(), + asset_object_id: "wooden-fish-hit-object-history".to_string(), + generation_provider: "bundled-default".to_string(), + prompt: "默认敲击物图案".to_string(), + width: 1024, + height: 1024, + }), + hit_sound_asset_json: to_json_string(&WoodenFishAudioAssetSnapshot { + asset_id: "wooden-fish-hit-sound-history".to_string(), + audio_src: "/wooden-fish/default-hit-sound.mp3".to_string(), + audio_object_key: "public/wooden-fish/default-hit-sound.mp3".to_string(), + asset_object_id: "wooden-fish-hit-sound-history".to_string(), + source: "bundled-default".to_string(), + prompt: Some("默认木鱼音".to_string()), + duration_ms: Some(3_000), + }), + floating_words_json: to_json_string(&default_floating_words()), + cover_image_src: "/wooden-fish/default-hit-object.png".to_string(), + generation_status: WOODEN_FISH_GENERATION_READY.to_string(), + publication_status: WOODEN_FISH_PUBLICATION_PUBLISHED.to_string(), + play_count: 0, + updated_at: now, + published_at: Some(now), + background_asset_json: Some(to_json_string(&WoodenFishImageAssetSnapshot { + asset_id: "wooden-fish-background-history".to_string(), + image_src: "/generated-wooden-fish-assets/history/background/image.png".to_string(), + image_object_key: "generated-wooden-fish-assets/history/background/image.png" + .to_string(), + asset_object_id: "wooden-fish-background-history".to_string(), + generation_provider: "image2".to_string(), + prompt: "历å²èƒŒæ™¯".to_string(), + width: 1024, + height: 1536, + })), + back_button_asset_json: None, + } + } +} diff --git a/src/components/CustomWorldGenerationView.test.tsx b/src/components/CustomWorldGenerationView.test.tsx index d20da96d..14971ec7 100644 --- a/src/components/CustomWorldGenerationView.test.tsx +++ b/src/components/CustomWorldGenerationView.test.tsx @@ -142,17 +142,22 @@ describe('CustomWorldGenerationView', () => { screen .getByRole('progressbar', { name: progressTitle }) .className, - ).toContain('w-[min(35rem,94vw)]'); + ).toContain('w-[400px]'); expect( screen .getByRole('progressbar', { name: progressTitle }) .className, - ).toContain('sm:w-[52rem]'); + ).toContain('h-[400px]'); expect( screen .getByRole('progressbar', { name: progressTitle }) .getAttribute('data-ring-start-degrees'), - ).toBe('155'); + ).toBe('135'); + expect( + screen + .getByRole('progressbar', { name: progressTitle }) + .getAttribute('data-ring-fill-start-degrees'), + ).toBe('135'); expect( screen .getByRole('progressbar', { name: progressTitle }) @@ -193,12 +198,12 @@ describe('CustomWorldGenerationView', () => { screen .getByTestId('generation-hero-progress-ring-track') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill') diff --git a/src/components/GenerationProgressHero.tsx b/src/components/GenerationProgressHero.tsx index 9fa0af3a..369258a6 100644 --- a/src/components/GenerationProgressHero.tsx +++ b/src/components/GenerationProgressHero.tsx @@ -4,8 +4,16 @@ import { useEffect, useId, useRef } from 'react'; import generationHeroVideo from '../../media/create_bg_video.mp4'; -const GENERATION_PROGRESS_RING_START_DEGREES = 155; -const GENERATION_PROGRESS_RING_SWEEP_DEGREES = 270; +const GENERATION_PROGRESS_RING_GAP_DEGREES = 90; +const GENERATION_PROGRESS_RING_BOTTOM_DEGREES = 90; +// 中文注释:SVG 圆从 3 点钟方å‘起笔;起点放在 135deg,å¯è®© 90deg å¼€å£å±…中è½åœ¨æ­£ä¸‹æ–¹ã€‚ +const GENERATION_PROGRESS_RING_START_DEGREES = + GENERATION_PROGRESS_RING_BOTTOM_DEGREES + + GENERATION_PROGRESS_RING_GAP_DEGREES / 2; +const GENERATION_PROGRESS_RING_FILL_START_DEGREES = + GENERATION_PROGRESS_RING_START_DEGREES; +const GENERATION_PROGRESS_RING_SWEEP_DEGREES = + 360 - GENERATION_PROGRESS_RING_GAP_DEGREES; const GENERATION_PROGRESS_RING_VIEWBOX = 400; const GENERATION_PROGRESS_RING_CENTER = GENERATION_PROGRESS_RING_VIEWBOX / 2; const GENERATION_PROGRESS_RING_RADIUS = 166; @@ -118,7 +126,9 @@ export function GenerationProgressHero({ const safeProgress = clampGenerationProgress(progressValue); const ringGradientId = useId().replace(/:/g, ''); const ringMetrics = buildGenerationRingMetrics(safeProgress); - const ringDegrees = Math.round((safeProgress / 100) * 270); + const ringDegrees = Math.round( + (safeProgress / 100) * GENERATION_PROGRESS_RING_SWEEP_DEGREES, + ); const ringTrackDasharray = `${ringMetrics.sweepLength.toFixed(2)} ${ringMetrics.circumference.toFixed(2)}`; const ringFillDasharray = `${ringMetrics.progressLength.toFixed(2)} ${ringMetrics.circumference.toFixed(2)}`; @@ -160,16 +170,19 @@ export function GenerationProgressHero({
diff --git a/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx b/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx index e2d25871..e8a0ef84 100644 --- a/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx +++ b/src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx @@ -130,12 +130,12 @@ describe('BarkBattleGeneratingView', () => { screen .getByRole('progressbar', { name: '汪汪声浪素æç”Ÿæˆè¿›åº¦' }) .className, - ).toContain('w-[min(35rem,94vw)]'); + ).toContain('w-[400px]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素æç”Ÿæˆè¿›åº¦' }) .className, - ).toContain('sm:w-[52rem]'); + ).toContain('h-[400px]'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素æç”Ÿæˆè¿›åº¦' }) @@ -145,7 +145,12 @@ describe('BarkBattleGeneratingView', () => { screen .getByRole('progressbar', { name: '汪汪声浪素æç”Ÿæˆè¿›åº¦' }) .getAttribute('data-ring-start-degrees'), - ).toBe('155'); + ).toBe('135'); + expect( + screen + .getByRole('progressbar', { name: '汪汪声浪素æç”Ÿæˆè¿›åº¦' }) + .getAttribute('data-ring-fill-start-degrees'), + ).toBe('135'); expect( screen .getByRole('progressbar', { name: '汪汪声浪素æç”Ÿæˆè¿›åº¦' }) @@ -186,12 +191,12 @@ describe('BarkBattleGeneratingView', () => { screen .getByTestId('generation-hero-progress-ring-track') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill') .getAttribute('transform'), - ).toBe('rotate(155 200 200)'); + ).toBe('rotate(135 200 200)'); expect( screen .getByTestId('generation-hero-progress-ring-fill')