fix(v2.2): StoryPlayer 推进竞态加固(双审查 SHOULD-FIX)
- speak 兜底定时器先于 speak 注册 + try/catch:speak 抛异常不再导致 剧情永久卡死(估时/forceNext 未注册的失败模式) - speak onEnd 与估时闭包加行号守卫(lineIndex===i && !ended):迟到的 上一句 onEnd 不再提前放行当前句 - SceneTheater 估时封顶 20s(与 MAX_LINE_MS 对齐),注释同步 v2.2 语义 - plan Task 10 更新为实际实现(SetServerRoot + /static 挂载) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1392,9 +1392,10 @@ git commit -m "feat: 全自动演出——开场自动进入/回复自动继续/
|
|||||||
> 用户走查反馈:① 必须点跳过才出选项(speak onEnd 部分环境不回调,逐句推进靠 20s forceNext 兜底,体验像卡死);② 剧情不需要文字展示区,直接语音播放;③ 前端应与后端共用 :8080。规格:spec「v2.2 修订」章节。
|
> 用户走查反馈:① 必须点跳过才出选项(speak onEnd 部分环境不回调,逐句推进靠 20s forceNext 兜底,体验像卡死);② 剧情不需要文字展示区,直接语音播放;③ 前端应与后端共用 :8080。规格:spec「v2.2 修订」章节。
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `ui-src/src/components/StoryPlayer.vue`(纯语音 + 估时兜底)
|
- Modify: `ui-src/src/components/StoryPlayer.vue`(纯语音 + 估时兜底 + 审查加固)
|
||||||
|
- Modify: `ui-src/src/components/SceneTheater.vue`(估时兜底 + 20s 封顶)
|
||||||
- Modify: `ui-src/src/pages/play/play.vue`(移除 :show-pinyin 传参)
|
- Modify: `ui-src/src/pages/play/play.vue`(移除 :show-pinyin 传参)
|
||||||
- Modify: `main.go`(AddStaticPath 托管前端产物)
|
- Modify: `main.go`(SetServerRoot 托管前端产物 + /static 素材挂载)
|
||||||
|
|
||||||
- [ ] **Step 1: StoryPlayer 改纯语音演绎**
|
- [ ] **Step 1: StoryPlayer 改纯语音演绎**
|
||||||
|
|
||||||
@@ -1522,12 +1523,15 @@ export default {
|
|||||||
|
|
||||||
- [ ] **Step 3: main.go 托管前端产物(8080 单端口)**
|
- [ ] **Step 3: main.go 托管前端产物(8080 单端口)**
|
||||||
|
|
||||||
`main.go` 在 `controller.Register(s)` 后加:
|
`main.go` 在 `controller.Register(s)` 后加(**须用 SetServerRoot**——走查发现 GoFrame v2.10.2 `AddStaticPath("/", ...)` 因前缀防误匹配守卫 `uri[len(prefix)] != '/'` 只服务根路径,`/assets/*` 子路径全部 404;素材目录 `ui-src/static` 以 `/static` 前缀单独挂载,与后端 `scene.image` 返回的 `/static/generated/...` URL 对应):
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口)
|
// 生产形态:前端产物 ui-src/dist 由后端 :8080 统一托管(前后端同端口)
|
||||||
if _, err := os.Stat("ui-src/dist"); err == nil {
|
if _, err := os.Stat("ui-src/dist"); err == nil {
|
||||||
s.AddStaticPath("/", "ui-src/dist")
|
s.SetServerRoot("ui-src/dist")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat("ui-src/static"); err == nil {
|
||||||
|
s.AddStaticPath("/static", "ui-src/static")
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.speaking = true
|
this.speaking = true
|
||||||
// v2.1:朗读结束自动进入决策幕——最短展示 1.2s,之后 speak onEnd 即进入;
|
// v2.1:朗读结束自动进入决策幕——最短展示 1.2s,之后 speak onEnd 即进入
|
||||||
// 无 TTS/onEnd 缺失最迟 15s 兜底(「跳过」仍可手动跳过)
|
// v2.2:估时兜底——onEnd 缺失时按文本长度估时自动进入(最长 20s,不卡死;「跳过」仍可手动跳过)
|
||||||
this.speechDone = false
|
this.speechDone = false
|
||||||
this.ready = false
|
this.ready = false
|
||||||
let done = false
|
let done = false
|
||||||
@@ -91,8 +91,8 @@ export default {
|
|||||||
this.$emit('done')
|
this.$emit('done')
|
||||||
}
|
}
|
||||||
this.minTimer = setTimeout(() => { this.ready = true; if (this.speechDone) finishAuto() }, 1200)
|
this.minTimer = setTimeout(() => { this.ready = true; if (this.speechDone) finishAuto() }, 1200)
|
||||||
// 兜底按文本长度估时(onEnd 缺失时按朗读预期时长自动进入,不卡 15s)
|
// 兜底按文本长度估时(onEnd 缺失时按朗读预期时长自动进入),封顶 20s 与 StoryPlayer MAX_LINE_MS 对齐
|
||||||
this.autoTimer = setTimeout(finishAuto, Math.max(4000, this.content.length * 320 + 2000))
|
this.autoTimer = setTimeout(finishAuto, Math.max(4000, Math.min(this.content.length * 320 + 2000, 20000)))
|
||||||
const onSpeechEnd = () => {
|
const onSpeechEnd = () => {
|
||||||
this.speechDone = true
|
this.speechDone = true
|
||||||
if (this.ready) finishAuto()
|
if (this.ready) finishAuto()
|
||||||
|
|||||||
@@ -61,22 +61,30 @@ export default {
|
|||||||
}
|
}
|
||||||
this.lineIndex = i
|
this.lineIndex = i
|
||||||
this.ended = false
|
this.ended = false
|
||||||
this.lineDone = { sp: false, fb: false }
|
this.lineDone = { sp: false }
|
||||||
this.moodTick = true
|
this.moodTick = true
|
||||||
this.after(350, () => { this.moodTick = false })
|
this.after(350, () => { this.moodTick = false })
|
||||||
const total = Array.from(line.text).length
|
const total = Array.from(line.text).length
|
||||||
speak(line.text, '', () => {
|
// 估时/forceNext 兜底定时器先于 speak 注册:speak 抛异常(stub 环境/无 TTS)也不会卡死
|
||||||
this.lineDone.sp = true
|
|
||||||
this.tryNext(i)
|
|
||||||
})
|
|
||||||
// 估时兜底:onEnd 缺失(浏览器静音策略/无语音)时按文本长度估时放行,不卡死
|
|
||||||
this.after(Math.max(2500, total * 320 + 1200), () => {
|
this.after(Math.max(2500, total * 320 + 1200), () => {
|
||||||
if (!this.lineDone.sp) {
|
if (this.lineIndex === i && !this.ended && !this.lineDone.sp) {
|
||||||
this.lineDone.sp = true
|
this.lineDone.sp = true
|
||||||
this.tryNext(i)
|
this.tryNext(i)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.after(MAX_LINE_MS, () => this.forceNext(i))
|
this.after(MAX_LINE_MS, () => this.forceNext(i))
|
||||||
|
try {
|
||||||
|
speak(line.text, '', () => {
|
||||||
|
// 行号守卫:迟到的第 i 句 onEnd 不得提前放行当前句
|
||||||
|
if (this.lineIndex === i && !this.ended) {
|
||||||
|
this.lineDone.sp = true
|
||||||
|
this.tryNext(i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.lineDone.sp = true
|
||||||
|
this.tryNext(i)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tryNext(i) {
|
tryNext(i) {
|
||||||
if (this.ended || this.lineIndex !== i || !this.lineDone.sp) return
|
if (this.ended || this.lineIndex !== i || !this.lineDone.sp) return
|
||||||
|
|||||||
Reference in New Issue
Block a user