Skip to content

fix: 翻译失效后能自愈 — 不再「一次失效就整场翻不出来」 - #3

Merged
zcl0621 merged 1 commit into
mainfrom
fix/translation-session-recovery
Aug 15, 2026
Merged

zcl0621 merged 1 commit into
mainfrom
fix/translation-session-recovery

Conversation

@zcl0621

@zcl0621 zcl0621 commented Aug 15, 2026

Copy link
Copy Markdown
Owner

症状(用户反馈)

经常突然不能翻译,必须去 系统设置 → 语言与地区 → 翻译语言 重新下载语言包才恢复。

代码里能解释这个症状的缺陷

TranslationSessionwarmUp() 里建一次就一直用,而 translate 是:

return try? await s.translate(en).targetText   // 错误被整个吞掉,失效的 session 原样留着

于是一旦 session 失效,这一场剩下的每一句都翻不出来 —— 没有任何恢复路径,
只能靠用户去系统设置折腾一圈再重开字幕。

改法

  • 翻译失败 → 丢掉旧 session、重建一次再试
  • 重建也失败(多半语言包真没了)→ 退避 30s,并置 needsLanguagePackReinstall 供 UI 给可操作提示。
  • 退避逻辑抽成 TranslationRetryGate 值类型,不接 Apple 框架就能单测(+5 条):
    边界到点即放行、一次成功立刻解封、反复失败不叠加封锁。守的是「别每句话都去重建 session」。
  • CaptionEngine 暖机循环里的 break → 「只报一次错但每条轨都试」。老写法下第一条轨
    暖机失败会连累第二条轨压根不暖机,即使它本可以正常翻译。
  • translate 对空串早退,免得因为空输入翻失败而白白重建一次 session。

⚠️ 未验证根因

复现不了「session 失效」这件事本身,它多半在 Apple 的翻译资源管理里。
本次改的是「失效后能自愈」,不是「不再失效」 —— 若重下语言包的频率没降,
说明根因不在 session 上,得另查。

193 tests,0 failures。

用户反馈:经常突然不能翻译,必须去 系统设置 → 语言与地区 → 翻译语言 重新下载
语言包才恢复。

代码里有个明确的缺陷能解释这个症状:TranslationSession 在 warmUp 里建一次就一直用,
而 translate 是 `try? await s.translate(...)` —— 错误被整个吞掉、失效的 session
原样留着。于是一旦它失效,**这一场剩下的每一句都翻不出来**,只能靠用户去系统设置
折腾一圈再重开字幕。

改法:失败后丢掉旧 session、重建一次再试。真的是语言包没了(重建也失败),才退避 30s
并置 needsLanguagePackReinstall 供 UI 给可操作提示。退避逻辑抽成 TranslationRetryGate
值类型,不接 Apple 框架就能单测(+5 条,守「别每句都重建」这条不变量:边界到点即放行、
成功立刻解封、反复失败不叠加封锁)。

顺带两处:
- CaptionEngine 暖机循环里的 `break` 改成「只报一次错但每条轨都试」。老写法下第一条轨
  暖机失败会连累第二条轨压根不暖机,即使它本可以正常翻译。
- translate 对空串早退,免得因为空输入翻失败而白白重建一次 session。

⚠️ **未验证根因**:我无法复现「session 失效」这件事本身,它多半在 Apple 的翻译资源
管理里。本次改的是「失效后能自愈」而不是「不再失效」—— 如果重下语言包的频率没降,
说明根因不是 session 失效,得另查。

swift build && swift test:193 tests,0 failures。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 15, 2026 09:24
@zcl0621
zcl0621 merged commit 47c941e into main Aug 15, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the resiliency of the app’s English→Chinese translation pipeline by making TranslationService self-heal when the underlying TranslationSession becomes invalid, and by preventing repeated expensive session rebuilds via a cooldown gate.

Changes:

  • Add TranslationRetryGate + tests to enforce retry cooldown invariants.
  • Update TranslationService.translate to rebuild the translation session once on failure and back off for 30s on repeated failures, exposing a UI hint flag for missing language packs.
  • Adjust CaptionEngine warm-up loop to avoid break, so each track attempts warm-up while only reporting the “install language pack” error once.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
Tests/LiveSubtitleTests/TranslationRetryGateTests.swift Adds unit tests covering retry gate cooldown behavior and boundaries.
Sources/LiveSubtitle/Translation/TranslationService.swift Implements session rebuild-on-failure, cooldown gating, and a UI hint flag for missing language packs.
Sources/LiveSubtitle/Pipeline/CaptionEngine.swift Changes warm-up loop to try all tracks while rate-limiting the user-facing error message.
Suppressed comments (2)

Sources/LiveSubtitle/Translation/TranslationService.swift:72

  • translateSerially 在旧 session 翻译成功时直接 return out,但不会解除之前的 cooldown,也不会清掉 needsLanguagePackReinstall。这会导致“已经恢复可翻译”后 UI 仍可能提示重装语言包,且后续真需要重建时还可能被旧 cooldown 拦住。
        if let s = session, let out = try? await s.translate(en).targetText {
            return out
        }

Sources/LiveSubtitle/Translation/TranslationService.swift:87

  • makeSession() 成功但 fresh.translate(...) 失败时,当前不会清掉 needsLanguagePackReinstall。如果之前因为缺包置过该标记,之后即使语言包已装好(能成功 prepareTranslation)但翻译仍失败,UI 也可能持续提示“去重下语言包”。建议在新 session 构建成功后就先清掉该标记。
        session = fresh
        gate.clear()
        guard let out = try? await fresh.translate(en).targetText else {
            // 新 session 也翻不动:不是"失效"能解释的,同样退避,别每句都重建。
            gate.block(now: now)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 40 to 44
func warmUp() async throws {
let s = TranslationSession(installedSource: Locale.Language(identifier: "en"),
target: Locale.Language(identifier: "zh-Hans"))
do {
try await s.prepareTranslation()
session = try await Self.makeSession()
} catch {
throw TranslateError.notInstalled
Comment on lines +94 to +98
/// UI 取走提示后清零,免得同一条提示反复弹。
func consumeReinstallHint() -> Bool {
defer { needsLanguagePackReinstall = false }
return needsLanguagePackReinstall
}
Comment on lines +67 to +68
// 空串没有翻译的必要,更不该因为它翻失败就去重建 session
guard !en.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants