Handle untagged continuation lines in rolling auto-captions

Short continuations (e.g. a single trailing word) carry no inline timing
tags, so filtering by tags missed them. Instead, in files with inline
timing tags, drop leading cue lines that repeat the previous cue's last
line — covers both tagged and untagged new text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Fable 5 2026-08-29 21:13:29 +02:00
parent 03eec79b4c
commit a7d7d1cf50
2 changed files with 30 additions and 25 deletions

View File

@ -9,16 +9,18 @@ def strip_html(text):
return re.sub(r'<[^>]+>', '', text)
# YouTube auto-captions use rolling cues: the previous line is repeated as
# plain text while the new line carries inline word timings (<00:00:00.200>,
# <c>…</c>). Only the tagged lines contain new text.
def has_inline_timing_tags(line):
return re.search(r'<\d{2}:\d{2}:|<c[.\w]*>', line) is not None
# YouTube auto-captions use rolling cues: each cue repeats the previous
# cue's last line as plain text before the new text (which usually, but not
# always, carries inline word timings like <00:00:00.200> or <c>…</c>).
def has_inline_timing_tags(text):
return re.search(r'<\d{2}:\d{2}:|<c[.\w]*>', text) is not None
def parse_vtt(content):
rolling = has_inline_timing_tags(content)
blocks = re.split(r'\n{2,}', content.strip())
cues = []
prev_last = None
for block in blocks:
lines = block.strip().splitlines()
if not lines:
@ -30,14 +32,14 @@ def parse_vtt(content):
ts_idx = next((i for i, l in enumerate(lines) if '-->' in l), None)
if ts_idx is None:
continue
text_lines = lines[ts_idx + 1:]
if any(has_inline_timing_tags(l) for l in text_lines):
text_lines = [l for l in text_lines if has_inline_timing_tags(l)]
text = ' '.join(
strip_html(l).strip()
for l in text_lines
if strip_html(l).strip()
)
text_lines = [strip_html(l).strip() for l in lines[ts_idx + 1:]]
if rolling:
while text_lines and prev_last and text_lines[0] == prev_last:
text_lines.pop(0)
non_empty = [l for l in text_lines if l]
if rolling and non_empty:
prev_last = non_empty[-1]
text = ' '.join(non_empty)
if text:
cues.append(text)
return cues

View File

@ -5,16 +5,18 @@ function stripHtml(text) {
return text.replace(/<[^>]+>/g, '');
}
// YouTube auto-captions use rolling cues: the previous line is repeated as
// plain text while the new line carries inline word timings (<00:00:00.200>,
// <c>…</c>). Only the tagged lines contain new text.
function hasInlineTimingTags(line) {
return /<\d{2}:\d{2}:|<c[.\w]*>/.test(line);
// YouTube auto-captions use rolling cues: each cue repeats the previous
// cue's last line as plain text before the new text (which usually, but not
// always, carries inline word timings like <00:00:00.200> or <c>…</c>).
function hasInlineTimingTags(text) {
return /<\d{2}:\d{2}:|<c[.\w]*>/.test(text);
}
function parseVtt(content) {
const rolling = hasInlineTimingTags(content);
const blocks = content.split(/\n{2,}/);
const cues = [];
let prevLast = null;
for (const block of blocks) {
const lines = block.trim().split('\n');
if (!lines.length) continue;
@ -22,14 +24,15 @@ function parseVtt(content) {
if (lines[0].startsWith('NOTE') || lines[0].startsWith('STYLE')) continue;
const tsIdx = lines.findIndex(l => l.includes('-->'));
if (tsIdx === -1) continue;
let textLines = lines.slice(tsIdx + 1);
if (textLines.some(hasInlineTimingTags)) {
textLines = textLines.filter(hasInlineTimingTags);
const textLines = lines.slice(tsIdx + 1).map(l => stripHtml(l).trim());
if (rolling) {
while (textLines.length && prevLast && textLines[0] === prevLast) {
textLines.shift();
}
}
const text = textLines
.map(l => stripHtml(l).trim())
.filter(Boolean)
.join(' ');
const nonEmpty = textLines.filter(Boolean);
if (rolling && nonEmpty.length) prevLast = nonEmpty[nonEmpty.length - 1];
const text = nonEmpty.join(' ');
if (text) cues.push(text);
}
return cues;