Compare commits

...

3 Commits

Author SHA1 Message Date
a7d7d1cf50 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>
2026-08-29 21:13:29 +02:00
03eec79b4c Fix duplicated text in Markdown from YouTube auto-captions
Rolling auto-caption cues repeat the previous line as plain text and
carry the new text in lines with inline word-timing tags. Keep only the
tagged lines in such cues (JS module and Python CLI); skip writing
Markdown files whose content is identical to one already written.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 21:11:42 +02:00
361c88be65 Add Handoff.md (session handoff document)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 20:57:35 +02:00
4 changed files with 82 additions and 10 deletions

41
Handoff.md Normal file
View File

@ -0,0 +1,41 @@
# yt-dlf — Handoff
> Übergabedokument für die Fortsetzung in einer Claude-Code-Session.
> Stand: 2026-08-29 — Web-Frontend für yt-dlp/ffmpeg mit Login, Ad-Spalten
> und User-Verwaltung; implementiert, Deploy-Status unbekannt.
> Dauerhafte Fakten stehen in `README.md`/`INSTALL.md`; hier steht nur, was
> **nicht** aus Code und Git-Historie ersichtlich ist.
**🔜 NÄCHSTE SESSION:** kein vorgeplanter Auftrag
## Offene Punkte
- [ ] Deploy-Status klären — `build/` und `users.txt` liegen lokal vor
(beide gitignored), aber ob und wo die App produktiv läuft, ist nirgends
festgehalten.
## Stolperfallen
- **Git-Konten-Mix** — Bestandsprojekt: Remote `origin` zeigt direkt auf
`git@gitea.101010.cloud:stwaidele/yt-dlf.git` und die Repo-Config steht
auf Stefans Konto. Claude-Commits daher per Author-Override
(`git -c user.name="Claude <Modell>" -c user.email="<slug>@waidele.info" commit …`),
Repo-Config nicht umbiegen. Push läuft über Stefans Default-Key.
## Session 2026-08-29 — Handoff angelegt (Claude Fable 5)
Projekt hatte noch keine `Handoff.md`; per `/handoff init` aus README,
INSTALL.md und Git-Historie erstellt. Kein Projekt-`CLAUDE.md` vorhanden.
Beide bisherigen Commits (16.05.) stammen von Stefan; seither keine
Änderungen im Working Tree.
## Typische Handgriffe
Dev-Server, Deployment und User-Verwaltung sind vollständig in `README.md`
und `INSTALL.md` dokumentiert — dort nachschlagen, hier nicht duplizieren.
## Verwandte Handoffs
_keine_
<!-- Ältere Sessions: git log -p Handoff.md -->

View File

@ -9,9 +9,18 @@ def strip_html(text):
return re.sub(r'<[^>]+>', '', text) return re.sub(r'<[^>]+>', '', text)
# 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): 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:
@ -23,11 +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 = ' '.join( text_lines = [strip_html(l).strip() for l in lines[ts_idx + 1:]]
strip_html(l).strip() if rolling:
for l in lines[ts_idx + 1:] while text_lines and prev_last and text_lines[0] == prev_last:
if strip_html(l).strip() 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: if text:
cues.append(text) cues.append(text)
return cues return cues

View File

@ -5,9 +5,18 @@ function stripHtml(text) {
return text.replace(/<[^>]+>/g, ''); return text.replace(/<[^>]+>/g, '');
} }
// 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) { 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;
@ -15,11 +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;
const text = lines const textLines = lines.slice(tsIdx + 1).map(l => stripHtml(l).trim());
.slice(tsIdx + 1) if (rolling) {
.map(l => stripHtml(l).trim()) while (textLines.length && prevLast && textLines[0] === prevLast) {
.filter(Boolean) textLines.shift();
.join(' '); }
}
const nonEmpty = textLines.filter(Boolean);
if (rolling && nonEmpty.length) prevLast = nonEmpty[nonEmpty.length - 1];
const text = nonEmpty.join(' ');
if (text) cues.push(text); if (text) cues.push(text);
} }
return cues; return cues;

View File

@ -105,12 +105,18 @@ async function runDownload(videoUrl, audioOnly, send, signal) {
} }
} }
const writtenMd = new Set();
for (const subPath of subtitlePaths) { for (const subPath of subtitlePaths) {
if (!existsSync(subPath)) continue; if (!existsSync(subPath)) continue;
send('info', `Converting: ${basename(subPath)}`); send('info', `Converting: ${basename(subPath)}`);
try { try {
const md = convertSubtitleToMarkdown(subPath); const md = convertSubtitleToMarkdown(subPath);
const mdPath = subPath.replace(/\.(vtt|srt)$/, '.md'); const mdPath = subPath.replace(/\.(vtt|srt)$/, '.md');
if (writtenMd.has(md)) {
send('info', `Skipped (identical): ${basename(mdPath)}`);
continue;
}
writtenMd.add(md);
writeFileSync(mdPath, md, 'utf-8'); writeFileSync(mdPath, md, 'utf-8');
send('info', `Saved: ${basename(mdPath)}`); send('info', `Saved: ${basename(mdPath)}`);
} catch (err) { } catch (err) {