diff --git a/.gitignore b/.gitignore index 9b8116e..a919e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ ui-src/node_modules/ ui-src/dist/ admin-src/node_modules/ admin-src/dist/ +.gstack/ +genasset diff --git a/docs/superpowers/plans/2026-08-13-story-theater-v2.md b/docs/superpowers/plans/2026-08-13-story-theater-v2.md index 454c3cb..a2b8b1b 100644 --- a/docs/superpowers/plans/2026-08-13-story-theater-v2.md +++ b/docs/superpowers/plans/2026-08-13-story-theater-v2.md @@ -1387,7 +1387,171 @@ git commit -m "feat: 全自动演出——开场自动进入/回复自动继续/ --- -## 验收对照(spec v2 + v2.1) +### Task 10: 纯语音演绎 + 估时兜底 + 8080 托管(v2.2) + +> 用户走查反馈:① 必须点跳过才出选项(speak onEnd 部分环境不回调,逐句推进靠 20s forceNext 兜底,体验像卡死);② 剧情不需要文字展示区,直接语音播放;③ 前端应与后端共用 :8080。规格:spec「v2.2 修订」章节。 + +**Files:** +- Modify: `ui-src/src/components/StoryPlayer.vue`(纯语音 + 估时兜底) +- Modify: `ui-src/src/pages/play/play.vue`(移除 :show-pinyin 传参) +- Modify: `main.go`(AddStaticPath 托管前端产物) + +- [ ] **Step 1: StoryPlayer 改纯语音演绎** + +`StoryPlayer.vue` 整体重写为:无文字展示区(删 RubyText 气泡、逐字高亮、pinyin),保留角色 + mood 表情 + 跳过;句推进改「speak onEnd + 文本长度估时兜底」: + +```vue + + + + + +``` + +- [ ] **Step 2: play.vue 移除 StoryPlayer 的 :show-pinyin 传参** + +两处 ``(剧情流 + 终局演绎)删除 `:show-pinyin="showPinyin"` 属性。 + +- [ ] **Step 3: main.go 托管前端产物(8080 单端口)** + +`main.go` 在 `controller.Register(s)` 后加: + +```go + // 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口) + if _, err := os.Stat("ui-src/dist"); err == nil { + s.AddStaticPath("/", "ui-src/dist") + } +``` + +- [ ] **Step 4: 构建 + 验证** + +Run: `cd ui-src && npx vite build --mode h5 2>&1 | tail -3 && cd .. && go build ./...` +Expected: 构建成功;产物在 `ui-src/dist` + +重启后端后走查 `http://localhost:8080`(H5 构建,前后端同端口): +1. 打开即应用首页(非 404) +2. 第 1 计关卡:剧情纯语音无文字区;台词播完选项自动出现(不点跳过) +3. 模拟 onEnd 不回调(playwright 注入 stub speechSynthesis:speak 不发声、不发事件):逐句按估时兜底推进,选项最终自动出现,不卡死 +4. 终局台词演绎后自动跳结算(8080 端口同样生效) + +- [ ] **Step 5: Commit** + +```bash +git add ui-src/src/components/StoryPlayer.vue ui-src/src/pages/play/play.vue main.go +git commit -m "feat: 剧情纯语音演绎(无文字区)+ 句推进估时兜底 + 前端产物由 :8080 托管(v2.2)" +``` + +--- + +## 验收对照(spec v2 + v2.1 + v2.2) | 验收标准 | 任务 | |---|---| diff --git a/docs/superpowers/specs/2026-08-13-story-theater-design.md b/docs/superpowers/specs/2026-08-13-story-theater-design.md index 3af2648..c2cf562 100644 --- a/docs/superpowers/specs/2026-08-13-story-theater-design.md +++ b/docs/superpowers/specs/2026-08-13-story-theater-design.md @@ -124,6 +124,27 @@ --- +## v2.2 修订(2026-08-13):纯语音演绎 + 单端口托管 + +> 用户走查反馈(v2.1 落地后):① **「必须点跳过按钮才显示出选项」**——StoryPlayer 自然播完不触发 done(speak onEnd 在部分浏览器环境不回调,逐句推进只能靠 20s forceNext 兜底,体验像卡死);② **「剧情也不应该显示文字,直接语音播放就可以了不需要文字展示区」**;③ **「前端应该跟后端共用端口」**(后端 :8080,前端产物由后端托管)。 + +### 核心转变 + +| 维度 | v2.1 | v2.2 | +|---|---|---| +| 剧情呈现 | 气泡文字 + 逐字高亮 + 拼音 | 纯语音演绎:无文字展示区,角色立绘 + mood 表情 + 逐句朗读 | +| 句推进 | speak onEnd + 逐字高亮双门控 | speak onEnd 驱动 + **文本长度估时兜底**(320ms/字 + 1.2s,最短 2.5s);20s forceNext 保留为最后防线 | +| 部署 | H5 dev server 5173(开发代理) | 前端产物 `ui-src/dist` 由后端 :8080 `AddStaticPath` 托管(生产单端口) | + +### 验收标准(v2.2) + +1. 剧情无任何文字展示:只有角色 + 表情徽章 + 语音 +2. 台词播完后选项自动出现(无需点跳过);跳过按钮仅作可选的手动加速 +3. speak onEnd 不回调的环境(模拟浏览器静音策略/无语音)下按估时兜底自动推进,不卡死 +4. `http://localhost:8080` 直接打开应用(前后端同端口) + +--- + ## 设计总览 每关 = 一部 2-3 分钟儿童小剧场,三幕结构: diff --git a/main.go b/main.go index 0d52a7e..b21d59f 100644 --- a/main.go +++ b/main.go @@ -26,5 +26,14 @@ func main() { s := g.Server() controller.Register(s) + // 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口) + // 注:根路径须用 SetServerRoot——AddStaticPath("/", ...) 因前缀防误匹配守卫只服务根路径, + // /assets 等子路径全部 404;素材目录 ui-src/static 以 /static 前缀单独挂载 + if _, err := os.Stat("ui-src/dist"); err == nil { + s.SetServerRoot("ui-src/dist") + } + if _, err := os.Stat("ui-src/static"); err == nil { + s.AddStaticPath("/static", "ui-src/static") + } s.Run() } diff --git a/ui-src/src/components/SceneTheater.vue b/ui-src/src/components/SceneTheater.vue index fd60171..231b93d 100644 --- a/ui-src/src/components/SceneTheater.vue +++ b/ui-src/src/components/SceneTheater.vue @@ -91,7 +91,8 @@ export default { this.$emit('done') } this.minTimer = setTimeout(() => { this.ready = true; if (this.speechDone) finishAuto() }, 1200) - this.autoTimer = setTimeout(finishAuto, 15000) + // 兜底按文本长度估时(onEnd 缺失时按朗读预期时长自动进入,不卡 15s) + this.autoTimer = setTimeout(finishAuto, Math.max(4000, this.content.length * 320 + 2000)) const onSpeechEnd = () => { this.speechDone = true if (this.ready) finishAuto() diff --git a/ui-src/src/components/StoryPlayer.vue b/ui-src/src/components/StoryPlayer.vue index 379cc4d..c152dbe 100644 --- a/ui-src/src/components/StoryPlayer.vue +++ b/ui-src/src/components/StoryPlayer.vue @@ -7,43 +7,27 @@ {{ character.name.slice(0, 1) }} {{ moodEmoji }} - - - - - 📖 旁白 - - - - {{ line.speaker }} - - - - +