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:
parent
03eec79b4c
commit
a7d7d1cf50
@ -9,16 +9,18 @@ def strip_html(text):
|
|||||||
return re.sub(r'<[^>]+>', '', text)
|
return re.sub(r'<[^>]+>', '', text)
|
||||||
|
|
||||||
|
|
||||||
# YouTube auto-captions use rolling cues: the previous line is repeated as
|
# YouTube auto-captions use rolling cues: each cue repeats the previous
|
||||||
# plain text while the new line carries inline word timings (<00:00:00.200>,
|
# cue's last line as plain text before the new text (which usually, but not
|
||||||
# <c>…</c>). Only the tagged lines contain new text.
|
# always, carries inline word timings like <00:00:00.200> or <c>…</c>).
|
||||||
def has_inline_timing_tags(line):
|
def has_inline_timing_tags(text):
|
||||||
return re.search(r'<\d{2}:\d{2}:|<c[.\w]*>', line) is not None
|
return re.search(r'<\d{2}:\d{2}:|<c[.\w]*>', text) is not None
|
||||||
|
|
||||||
|
|
||||||
def parse_vtt(content):
|
def parse_vtt(content):
|
||||||
|
rolling = has_inline_timing_tags(content)
|
||||||
blocks = re.split(r'\n{2,}', content.strip())
|
blocks = re.split(r'\n{2,}', content.strip())
|
||||||
cues = []
|
cues = []
|
||||||
|
prev_last = None
|
||||||
for block in blocks:
|
for block in blocks:
|
||||||
lines = block.strip().splitlines()
|
lines = block.strip().splitlines()
|
||||||
if not lines:
|
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)
|
ts_idx = next((i for i, l in enumerate(lines) if '-->' in l), None)
|
||||||
if ts_idx is None:
|
if ts_idx is None:
|
||||||
continue
|
continue
|
||||||
text_lines = lines[ts_idx + 1:]
|
text_lines = [strip_html(l).strip() for l in lines[ts_idx + 1:]]
|
||||||
if any(has_inline_timing_tags(l) for l in text_lines):
|
if rolling:
|
||||||
text_lines = [l for l in text_lines if has_inline_timing_tags(l)]
|
while text_lines and prev_last and text_lines[0] == prev_last:
|
||||||
text = ' '.join(
|
text_lines.pop(0)
|
||||||
strip_html(l).strip()
|
non_empty = [l for l in text_lines if l]
|
||||||
for l in text_lines
|
if rolling and non_empty:
|
||||||
if strip_html(l).strip()
|
prev_last = non_empty[-1]
|
||||||
)
|
text = ' '.join(non_empty)
|
||||||
if text:
|
if text:
|
||||||
cues.append(text)
|
cues.append(text)
|
||||||
return cues
|
return cues
|
||||||
|
|||||||
@ -5,16 +5,18 @@ function stripHtml(text) {
|
|||||||
return text.replace(/<[^>]+>/g, '');
|
return text.replace(/<[^>]+>/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
// YouTube auto-captions use rolling cues: the previous line is repeated as
|
// YouTube auto-captions use rolling cues: each cue repeats the previous
|
||||||
// plain text while the new line carries inline word timings (<00:00:00.200>,
|
// cue's last line as plain text before the new text (which usually, but not
|
||||||
// <c>…</c>). Only the tagged lines contain new text.
|
// always, carries inline word timings like <00:00:00.200> or <c>…</c>).
|
||||||
function hasInlineTimingTags(line) {
|
function hasInlineTimingTags(text) {
|
||||||
return /<\d{2}:\d{2}:|<c[.\w]*>/.test(line);
|
return /<\d{2}:\d{2}:|<c[.\w]*>/.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseVtt(content) {
|
function parseVtt(content) {
|
||||||
|
const rolling = hasInlineTimingTags(content);
|
||||||
const blocks = content.split(/\n{2,}/);
|
const blocks = content.split(/\n{2,}/);
|
||||||
const cues = [];
|
const cues = [];
|
||||||
|
let prevLast = null;
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
const lines = block.trim().split('\n');
|
const lines = block.trim().split('\n');
|
||||||
if (!lines.length) continue;
|
if (!lines.length) continue;
|
||||||
@ -22,14 +24,15 @@ function parseVtt(content) {
|
|||||||
if (lines[0].startsWith('NOTE') || lines[0].startsWith('STYLE')) continue;
|
if (lines[0].startsWith('NOTE') || lines[0].startsWith('STYLE')) continue;
|
||||||
const tsIdx = lines.findIndex(l => l.includes('-->'));
|
const tsIdx = lines.findIndex(l => l.includes('-->'));
|
||||||
if (tsIdx === -1) continue;
|
if (tsIdx === -1) continue;
|
||||||
let textLines = lines.slice(tsIdx + 1);
|
const textLines = lines.slice(tsIdx + 1).map(l => stripHtml(l).trim());
|
||||||
if (textLines.some(hasInlineTimingTags)) {
|
if (rolling) {
|
||||||
textLines = textLines.filter(hasInlineTimingTags);
|
while (textLines.length && prevLast && textLines[0] === prevLast) {
|
||||||
|
textLines.shift();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const text = textLines
|
const nonEmpty = textLines.filter(Boolean);
|
||||||
.map(l => stripHtml(l).trim())
|
if (rolling && nonEmpty.length) prevLast = nonEmpty[nonEmpty.length - 1];
|
||||||
.filter(Boolean)
|
const text = nonEmpty.join(' ');
|
||||||
.join(' ');
|
|
||||||
if (text) cues.push(text);
|
if (text) cues.push(text);
|
||||||
}
|
}
|
||||||
return cues;
|
return cues;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user