From 51d2ef70d8b2eed3d4aaae740f02a4eacc45fbb8 Mon Sep 17 00:00:00 2001 From: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:05:46 -0700 Subject: [PATCH 1/3] docs: add Coming-from-Python section + fix stale counts Adds a "Coming from Python" on-ramp table (verified against the repo's own examples/ and tests) and corrects suite/test-count accuracy issues. Co-Authored-By: Claude --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e45ed09..fdeb986 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,22 @@ with Chain of Thought's own episode transcripts: pulling quotes for show notes, picking the timestamp window for a clip, and turning an SRT or VTT export into a clean transcript. +### Coming from Python + +There is no Python stdlib equivalent for subtitles — the closest third-party +library is `webvtt-py`. The rough mapping: + +| Python (`webvtt-py`, closest) | mojo-captions | +| ----------------------------------- | -------------------------------------- | +| `vtt = webvtt.read("f.vtt")` | `var caps = parse_captions(source)` | +| `for c in vtt: c.text` | `for cue in caps.cues: cue.text` | +| `c.start` / `c.end` | `cue.start_ms` / `cue.end_ms` | +| `vtt.save_as_srt(...)` | `to_srt(caps)` (also `to_vtt(caps)`) | + +mojo-captions parses both SRT and WebVTT (detected automatically), and adds +transcript helpers with no `webvtt-py` parallel: `plain_text(caps)`, +`cues_between(caps, start_ms, end_ms)`, and `duration_ms(caps)`. + ## What it handles - **Auto-detection**: a leading `WEBVTT` header means WebVTT, anything else @@ -116,9 +132,11 @@ liberally or is skipped, never fatal. ## Part of a pure-Mojo library suite -Ten pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, +Eleven pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, filling gaps in the native Mojo ecosystem: +- [mojo-xml](https://github.com/conorbronsdon/mojo-xml) — general-purpose XML + parsing, an ElementTree-shaped DOM (Python's `xml.etree.ElementTree`) - [mojo-feed](https://github.com/conorbronsdon/mojo-feed) — RSS, Atom, and JSON Feed parsing (Python's `feedparser`) - [mojo-html](https://github.com/conorbronsdon/mojo-html) — HTML parsing and From e6de6f15a892ab082afae6c8a5a93b2c09dd10e0 Mon Sep 17 00:00:00 2001 From: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:09:51 -0700 Subject: [PATCH 2/3] fix: treat `-->` inside cue text as prose, not a cue boundary A cue whose text contains `-->` (e.g. "Use map --> filter here.") parsed as zero cues: `_parse_block` treated any line containing the substring `-->` as a timing line, so the text line was taken as a new cue boundary, then failed to parse as `timestamp --> timestamp` and threw away the whole block. Add `_is_timing_line`, which only treats a line as a cue boundary when both sides parse as timestamps, and use it for both the first-timing- line scan and the glued-cue boundary scan. Also isolate each segment's `_parse_timing` in its own try so a malformed timing line skips only that segment instead of discarding cues already gathered from the block. Also fix the pixi mojo pin (`>=1.0.0b3` sorts below dev nightlies, so `pixi install` failed to solve) to `>=1.0.0b3.dev0,<2` as a build prerequisite for verifying the fix. Adds regression tests for `-->` in single-line and multi-line cue text and for a genuinely glued second cue whose predecessor's text holds a `-->`. Full suite: 32 tests, all passing. Co-Authored-By: Claude --- pixi.toml | 2 +- src/captions/captions.mojo | 34 +++++++++++++++++++++++++--- test/test_captions.mojo | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/pixi.toml b/pixi.toml index e90e1ca..ba9d3d6 100644 --- a/pixi.toml +++ b/pixi.toml @@ -11,4 +11,4 @@ test = "mojo run -I src test/test_captions.mojo" demo = "mojo run -I src examples/clip_transcript.mojo test/data/sample.vtt 0 20000" [dependencies] -mojo = ">=1.0.0b3,<2" +mojo = ">=1.0.0b3.dev0,<2" diff --git a/src/captions/captions.mojo b/src/captions/captions.mojo index c05fdcc..55abb01 100644 --- a/src/captions/captions.mojo +++ b/src/captions/captions.mojo @@ -176,6 +176,24 @@ def _parse_timing(line: String, mut start_ms: Int, mut end_ms: Int) raises: end_ms = _parse_ts(right) +def _is_timing_line(line: String) -> Bool: + """Whether `line` is a genuine `timestamp --> timestamp` timing line. + + Cue text may legitimately contain `-->` (e.g. "map --> filter", or a + Unicode-arrow gloss), so a bare `-->` substring is not enough to mark + a line as a cue boundary; both sides must parse as timestamps. + """ + if line.find("-->") == -1: + return False + var start_ms = 0 + var end_ms = 0 + try: + _parse_timing(line, start_ms, end_ms) + except: + return False + return True + + def _strip_voice_tags(text: String, mut speaker: String) -> String: """Remove `` / `` markup; record the first voice's name.""" if text.find(" List[Cue]: while seg_start < end: var t = -1 for j in range(seg_start, end): - if lines[j].find("-->") != -1: + if _is_timing_line(lines[j]): t = j break if t == -1: @@ -299,14 +317,24 @@ def _parse_block(lines: List[String], start: Int, end: Int) raises -> List[Cue]: index = index * 10 + Int(b) - ord("0") var start_ms = 0 var end_ms = 0 - _parse_timing(lines[t], start_ms, end_ms) + # Isolate each segment's parse: a malformed timing line skips just + # this segment, never discarding cues already gathered from the + # block. `_is_timing_line` already validated `lines[t]`, so this + # is defense-in-depth against divergence between the two. + try: + _parse_timing(lines[t], start_ms, end_ms) + except: + seg_start = t + 1 + continue # A block can hold a second (or third...) cue glued on with no # blank-line separator. If a later "text" line is itself a # timing line, that's where this cue's text ends and the next # cue begins — including its optional index line just before it. + # A bare `-->` inside cue text is not a boundary; only a line that + # parses as `timestamp --> timestamp` is. var text_end = end for j in range(t + 1, end): - if lines[j].find("-->") != -1: + if _is_timing_line(lines[j]): if j > t + 1 and _is_all_digits(lines[j - 1]): text_end = j - 1 else: diff --git a/test/test_captions.mojo b/test/test_captions.mojo index 520ba8b..b3d9fe9 100644 --- a/test/test_captions.mojo +++ b/test/test_captions.mojo @@ -334,6 +334,51 @@ def test_srt_glued_cues_without_blank_line() raises: assert_equal(caps.cues[1].text, "Second cue text.") +def test_arrow_in_cue_text_not_a_boundary() raises: + """A `-->` inside cue text is prose, not a timing line: the cue must + survive whole rather than being split (or discarded) as a boundary.""" + var caps = parse_captions( + String("1\n00:00:01,000 --> 00:00:02,000\nUse map --> filter here.\n") + ) + assert_equal(len(caps.cues), 1) + assert_equal(caps.cues[0].text, "Use map --> filter here.") + # And it must round-trip through both serializers unchanged. + var via_srt = parse_captions(to_srt(caps)) + assert_equal(len(via_srt.cues), 1) + assert_equal(via_srt.cues[0].text, "Use map --> filter here.") + var via_vtt = parse_captions(to_vtt(caps)) + assert_equal(len(via_vtt.cues), 1) + assert_equal(via_vtt.cues[0].text, "Use map --> filter here.") + + +def test_arrow_in_multiline_text_preserved() raises: + """A `-->` on a later text line must not truncate the cue or spawn a + bogus second cue; the full multi-line text is kept.""" + var caps = parse_captions( + String( + "1\n00:00:01,000 --> 00:00:02,000\n" + "first line\nx --> y transform\nthird line\n" + ) + ) + assert_equal(len(caps.cues), 1) + assert_equal(caps.cues[0].text, "first line\nx --> y transform\nthird line") + + +def test_arrow_text_still_splits_glued_real_cue() raises: + """Even when a cue's text holds a `-->`, a genuinely glued second cue + (a real timing line, no blank separator) is still split out.""" + var caps = parse_captions( + String( + "1\n00:00:01,000 --> 00:00:02,000\nUse map --> filter here.\n" + "2\n00:00:03,000 --> 00:00:04,000\nSecond cue text.\n" + ) + ) + assert_equal(len(caps.cues), 2) + assert_equal(caps.cues[0].text, "Use map --> filter here.") + assert_equal(caps.cues[1].index, 2) + assert_equal(caps.cues[1].text, "Second cue text.") + + def test_srt_explicit_zero_index_roundtrip() raises: """An explicit cue number of 0 is a real index, not the "absent" sentinel, and must survive a to_srt round trip unchanged.""" From 7975ea2b39a6158911d502a9b5dfb8afdce6f143 Mon Sep 17 00:00:00 2001 From: Conor Bronsdon <120674402+conorbronsdon@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:04:53 -0700 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20keep=20hardening=20PR=20code-only=20?= =?UTF-8?q?=E2=80=94=20drop=20README=20docs=20swept=20in=20from=20the=20pa?= =?UTF-8?q?rallel=20docs=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Coming-from-Python README section belongs to the docs PR, not this security/robustness fix branch. Co-Authored-By: Claude --- README.md | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/README.md b/README.md index fdeb986..e45ed09 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,6 @@ with Chain of Thought's own episode transcripts: pulling quotes for show notes, picking the timestamp window for a clip, and turning an SRT or VTT export into a clean transcript. -### Coming from Python - -There is no Python stdlib equivalent for subtitles — the closest third-party -library is `webvtt-py`. The rough mapping: - -| Python (`webvtt-py`, closest) | mojo-captions | -| ----------------------------------- | -------------------------------------- | -| `vtt = webvtt.read("f.vtt")` | `var caps = parse_captions(source)` | -| `for c in vtt: c.text` | `for cue in caps.cues: cue.text` | -| `c.start` / `c.end` | `cue.start_ms` / `cue.end_ms` | -| `vtt.save_as_srt(...)` | `to_srt(caps)` (also `to_vtt(caps)`) | - -mojo-captions parses both SRT and WebVTT (detected automatically), and adds -transcript helpers with no `webvtt-py` parallel: `plain_text(caps)`, -`cues_between(caps, start_ms, end_ms)`, and `duration_ms(caps)`. - ## What it handles - **Auto-detection**: a leading `WEBVTT` header means WebVTT, anything else @@ -132,11 +116,9 @@ liberally or is skipped, never fatal. ## Part of a pure-Mojo library suite -Eleven pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, +Ten pure-Mojo libraries that mirror familiar Python stdlib and PyPI APIs, filling gaps in the native Mojo ecosystem: -- [mojo-xml](https://github.com/conorbronsdon/mojo-xml) — general-purpose XML - parsing, an ElementTree-shaped DOM (Python's `xml.etree.ElementTree`) - [mojo-feed](https://github.com/conorbronsdon/mojo-feed) — RSS, Atom, and JSON Feed parsing (Python's `feedparser`) - [mojo-html](https://github.com/conorbronsdon/mojo-html) — HTML parsing and