Commit Graph
2 Commits
Author SHA1 Message Date
debpalashandClaude Opus 4.8 f3286c5e6e feat(dub): paste a translation from an external source onto existing segments
After transcription the user can paste a translation produced elsewhere
(ChatGPT, DeepL, a human translator) and have it map onto the segments
that already exist — no re-transcription, no timing loss.

Three input shapes are auto-detected: a timestamped .srt/.vtt (cues matched
to segments by time overlap, greedy one-to-one so one long cue can't be
copied onto several rows), numbered lines (`1.` / `2)` / `[3]`, mapped by
number and falling back to order when a model renumbers mid-answer), and
plain lines (positional, blank lines treated as separators rather than
empty translations). Nothing is applied until the preview dialog has shown
every row as before→after with unmatched rows flagged.

Applying goes through `pasteTranslations` in useSegmentEditing, which
mirrors `segmentEditField`'s duties across rows in ONE undo step: write
`text` and `translations[dubLangCode]` in lock-step and clear the stale
machine-translation badges. It never writes `text_original` (the translate
source `handleTranslateAll` reads — overwriting it would poison every later
re-translate) and never touches a language other than the active one.
Changing `text` alone marks those rows stale via the existing per-language
fingerprints, so no new flag is needed.

The new `POST /dub/parse-subtitle-text` is a stateless wrapper over the
existing `services.srt_parser.parse_srt`, so the lenient cue parsing stays
single-sourced instead of being reimplemented in JavaScript.

Also fixes a ReDoS in that parser, reachable today via /dub/import-srt:
`_TIMING_RE` used `^\s*` under re.MULTILINE, so at every line start the
engine consumed all remaining blank lines before failing on the first
digit — quadratic. 20k blank lines already took 1.7s and a 2 MB blank-line
file never returned, pinning the request thread. Horizontal-whitespace-only
classes make the scan linear.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 13:18:51 +05:30
Palash DebnathandClaude Opus 4.7 e46b4e3d47 feat: import .srt subtitles to bypass Whisper (closes #52)
Closes #52. Users who already have correct, pre-synced subtitles can now
skip ASR entirely — they upload a video as normal and then hit "Import
.srt" instead of "Upload & Transcribe". The .srt cues populate the dub
segment list directly, so the rest of the pipeline (translate, dub,
export) just works.

Backend
- services/srt_parser.py: lenient SubRip parser. Tolerates BOM, CRLF,
  missing index numbers, dot-vs-comma ms separator, and overlap (shifts
  the later cue's start to the earlier's end rather than dropping). Skips
  cues with non-positive duration or empty bodies; reports counts so the
  UI can warn.
- dub_core.py: new POST /dub/import-srt/{job_id} accepts the .srt file,
  parses it, clamps cues that run past the source media's duration, and
  replaces job["segments"]. Tries UTF-8 with BOM first, falls back to
  latin-1 for legacy Windows subs.

Frontend
- api/dub.ts: dubImportSrt helper with a typed response.
- hooks/useDubWorkflow.js: handleDubImportSrt — sets segments, flips
  dubStep to 'editing', shows a toast with per-bucket counts (imported /
  skipped / overlap-shifted / clamped) so the user sees what happened.
- pages/DubTab.jsx: "Import .srt" button next to "Upload & Transcribe"
  once a job exists, plus a smaller "Import .srt instead" affordance in
  the transcription-failure banner — the exact recovery path the
  reporter asked for.

Tests
- tests/test_srt_parser.py: 12 cases covering well-formed input,
  multi-line cues, dot-as-separator, BOM, CRLF, malformed cues, empty
  bodies, overlap shift, overlap-becomes-zero-drop, missing indices,
  empty input, and segment shape (sequential ids, speaker filler).
  pytest is now 226 passed (was 214); vitest unchanged at 11.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 12:39:01 +05:30