feat(subtitles): AI subtitle auto-sync — semantic timing correction with drift calibration and OCR fallback - #625
feat(subtitles): AI subtitle auto-sync — semantic timing correction with drift calibration and OCR fallback#625theNuvioGuy wants to merge 2 commits into
Conversation
…nd renderer-side apply Port and extend the NuvioTV AI subtitle auto-sync feature: - Anchor 1: LLM semantic line matching between built-in reference cues and the addon subtitle file, pooled offsets reduced with a robust mean and applied as a flat delay. - Background drift calibration (anchors 2-4) via an invisible text-only ExoPlayer at 4x speed; robust OLS line fit with anchor reliability gates, a protected anchor 1, and a noise floor that snaps unmeasurable slopes to a flat fit. - Renderer-side apply (Nuvio's delay-base + rate x (position - anchor) mechanism with a self-capturing anchor on the render thread): no MediaItem rebuild, no playback hiccup, for anchor 1, drift refits, and cached re-applies; file-baking kept only as fallback. - ML Kit OCR fallback for image-based (PGS/DVB) built-in reference tracks, in both the foreground gather and the drift calibrator. - Non-linear rescue: when anchors measure fine but no line fits (stepwise cut differences), re-measure a flat offset whenever playback moves 10+ minutes from the last measurement (covers natural progress and jumps). - Startup auto-trigger for auto-selected addon subs with one-attempt-per- stream guards keyed on provider|id; cached syncs re-apply with no LLM. - Settings: auto-sync toggle mutually exclusive with find-best-match; AI model/key rows no longer visually gated on the translation toggle. - Result toasts report offset and drift; per-stream cache persists offset + rate across playbacks (cloud backup aware). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@silentbil Please see :) |
|
Thanks for the work @theNuvioGuy |
|
Great work and thanks for the pr. Will leave this to @silentbil since he did all the current AI infrastructure. |
… drift resync Combines "Find Best Match" and "AI Auto-Sync" into a single Auto Sync feature with one master toggle (default ON) plus a "Use AI" sub-option, and makes the AI pipeline recover from a wrong-cut subtitle instead of just tracking its drift forever. Settings (PlayerViewModel/SettingsViewModel/SettingsScreen/CloudSyncRepository): - New "Auto Sync Subtitles" master toggle (subtitle_auto_sync_enabled, default ON) replaces the old "Find Best Match" toggle. - "Use AI for Auto Sync" (subtitle_ai_auto_sync) becomes a sub-option of the master: off -> auto-run the timing-based match scan; on -> run the full AI pipeline, falling back to the scan when no built-in reference track or API key is available. - Subtitle picker exposes mode-aware actions: both manual "Auto Sync - Without AI" / "Auto Sync - With AI" entries when the master is off, or a single "Auto Sync" entry matching the configured mode when on. Every manual action now fully resets prior sync state (cached match, applied transform, retained cues/anchors, session guards) before restarting from anchor 1. Drift calibration (SubtitleDriftCalibrator): - Adaptive periodic resync loop replaces the old fixed 10-minute interval: seeds its rule/cadence from already-measured anchors (no blind wait), then adapts the next interval from the measured local drift velocity (tolerance / |v|, clamped 2-10 min), backing off on confirmations and tightening on steps/unattributable changes. - Fixed a false-negative in NON_LINEAR detection: the robust line fit can silently drop one disagreeing anchor as an "outlier" and return a fit from the rest, masking real non-linear timing. An end-of-pass validation now checks every trusted measurement against the final fit and forces NON_LINEAR if any anchor disagrees beyond noise. - New background rematch on NON_LINEAR drift: score addon candidates against reference intervals collected locally near the live position (mirrors what the on-screen scan does), and swap to a better-cut subtitle when one is found, re-entering the pipeline on the winner. Falls back to the existing resync loop when no better candidate is confirmed. - Reference cues/intervals gathered by anchor 1 (primary player) and the anchor pass (secondary player) are now retained per-stream and reused for background rematch scoring and the winner's anchor-1, avoiding redundant stream connections/downloads. - Pacing fixes so background gathers don't starve the primary buffer: buffer-health gate re-checked before every gather (not just once), short settle before consecutive secondary-player connections, and a 15-per-hour sampling budget with graceful degradation. - Persistent background-status message (top-center pill) during refinement/rematch so long-running background work is visible instead of appearing stuck. Player behavior (PlayerViewModel/PlayerScreen): - Subtitle view is hidden during anchor 1's initial reference-track gather so the viewer never sees the raw built-in track mid-sync. - Renderer-side auto-sync transform is capped at a small delay (RENDERER_TRANSFORM_MAX_DELAY_MS): larger corrections shift the renderer clock far enough back that a seek asks for already-flushed cues, blanking subtitles until playback catches up. Large delays now bake into the served file instead, keeping seeks instant.
Update: Unified Auto Sync + adaptive drift resyncAdded commit What changedCombined "Find Best Match" and "AI Auto-Sync" into one feature:
Adaptive drift resync (replaces the old fixed 10-minute interval):
New: background rematch on non-linear drift. When AI auto-sync detects non-linear timing (evidence of a wrong-cut subtitle for this rip), it now searches in the background for a better-cut addon subtitle and swaps to it automatically if one scores well — instead of just tracking the mismatch forever. Falls back to the existing resync loop if nothing better is found. Reference material gathered along the way is reused across steps to avoid redundant downloads/connections. Playback fixes:
Note for reviewersThis build currently shows extra toast/status messages describing background progress (e.g. "Auto-sync refinement 1/3", "Non-linear drift found — searching for a better subtitle…", "Better-match search — scoring N subtitles…"). These are intentionally left in for now so we can verify the background pipeline is actually progressing through each stage during testing. They will be removed/quieted before this ships for a smooth, mostly-silent UX — only the final outcome toast should remain. |
|
Thanks for the substantial work here. This is a genuinely useful feature and the overall approach looks promising. The PR merges cleanly, the Sideload build compiles, and the new unit tests pass. I found two things that should be fixed before release:
The bundled ML Kit ARM64 library is 16 KB compatible. It does increase the app by roughly 19 MB of ARM native/model files, which is worth being aware of. Also, as noted in the PR, please quiet the detailed background progress messages before shipping. One separate issue: the Play flavor currently fails in Once the two PR-specific issues are addressed and source-switch/seek/PGS/debrid/HLS cases have been tested on a real TV, this should be in good shape to merge. |
|
@theNuvioGuy An update/release is planned tomorrow. Would be nice to have this included in it. |
Im working with him offline to create a better UX |
|
Alright |
Motivation
Addon subtitles are frequently out of sync: cut for a different rip, a different frame rate, or a different edit. Today the practical fallback is AI translation of an embedded track — which works, but has two real costs this feature avoids:
Cost / token usage. AI translation calls the LLM continuously for the entire runtime — every subtitle window of a 2-hour movie goes through the model. Auto-sync sends a handful of tiny requests total (≈5 short dialogue lines each, roughly 3–8 calls per movie regardless of runtime), because the model is only asked to match a few lines, never to translate content. The result is then cached per stream, so future playbacks of the same file re-apply the sync with zero LLM calls. In practice this is orders of magnitude cheaper.
Quality in gendered languages. For languages with grammatical gender and gendered address (Hebrew, Arabic, and many others), machine translation from English is structurally handicapped: English "you said" doesn't say whether the speaker addresses a man or a woman, so the model must guess verb forms and pronouns — and it guesses wrong constantly. A human-authored subtitle in the target language already has all of this right. The only thing wrong with it is timing — and timing is exactly what this feature fixes. Auto-sync makes the good subtitles usable instead of generating mediocre ones.
The algorithm
Anchor 1 — flat offset (foreground, seconds):
builtInTime − addonTime.♪,[Music]…) are filtered — they rarely exist in addon files.Anchors 2–4 — drift calibration (background, best-effort):
delay(position) = intercept + rate·positionis fit across all anchors — correcting frame-rate-style drift (e.g. 25 ↔ 23.976 fps ≈ +2.6 s/min). Safeguards, each of which exists because it prevented a real failure during testing:Applying — renderer-side, zero interruption:
delay(position) = base + rate·(position − anchor), with the anchor self-captured on the render thread (render positions live in ExoPlayer's private offset timebase — delta-only math is the only safe form). No MediaItem rebuild, no buffering hiccup, refits are instant and seek-proof.Code changes — deliberately minimal, built on what's already here
The diff is one commit touching a small set of files, because ARVIO already had almost every building block:
SubtitleTranslationService— auto-sync adds one method (matchSubtitleLines)AiSubtitleRenderersFactoryreflection walkers +onPlayerCuesSubtitleOffsetRenderer— extended with the base+rate transformSubtitleSyncMatcher,localizeSubtitleCachedSubMatchgains aratefield)New files are only the isolated logic:
SubtitleAutoSync.kt(pure math — unit-tested, no Android deps),SubtitleDriftCalibrator.kt(the invisible second player),SubtitleCueOcr.kt(~30 lines of ML Kit). The single new dependency iscom.google.mlkit:text-recognition(bundled on-device model, no Play Services).Nothing about existing behavior changes when the feature is off; the auto-sync toggle is mutually exclusive with the auto match-scan since they'd fight over the reference track.
Credits & testing
The idea and design direction are mine; the implementation was done with Claude Fable 5. I tested it end-to-end on genuinely problematic, old sources — The Office episodes (PAL-style drift) and A Bronx Tale among them — and it works nicely, including files where the subtitle offset changes partway through. Unit tests cover the math (robust mean, window selection, drift fit guards, noise floor, timestamp transforms).