From 8d973f2bc57a7b932448849921e7c04543498d75 Mon Sep 17 00:00:00 2001 From: jzocb Date: Sun, 22 Mar 2026 15:52:30 -0400 Subject: [PATCH 1/3] feat(youtube-transcript): add end times to chapter data Add 'end' field to Chapter interface and parseChapters output. Each chapter's end is derived from the next chapter's start time, with the last chapter ending at the video's total duration. This makes chapter data complete and ready for downstream consumers (e.g. video clipping with ffmpeg) without requiring them to compute end times from adjacent chapters. Before: { title: 'Overview', start: 0 } After: { title: 'Overview', start: 0, end: 21 } --- skills/baoyu-youtube-transcript/scripts/main.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/skills/baoyu-youtube-transcript/scripts/main.ts b/skills/baoyu-youtube-transcript/scripts/main.ts index d96a804..53ada2d 100644 --- a/skills/baoyu-youtube-transcript/scripts/main.ts +++ b/skills/baoyu-youtube-transcript/scripts/main.ts @@ -44,6 +44,7 @@ interface TranscriptInfo { interface Chapter { title: string; start: number; + end: number; } interface VideoMeta { @@ -249,16 +250,21 @@ async function fetchTranscriptSnippets(info: TranscriptInfo, translateTo?: strin // --- Metadata & chapters --- -function parseChapters(description: string): Chapter[] { - const chapters: Chapter[] = []; +function parseChapters(description: string, duration: number = 0): Chapter[] { + const raw: { title: string; start: number }[] = []; for (const line of description.split("\n")) { const m = line.trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})\s+(.+)$/); if (m) { const h = m[1] ? parseInt(m[1]) : 0; - chapters.push({ title: m[4].trim(), start: h * 3600 + parseInt(m[2]) * 60 + parseInt(m[3]) }); + raw.push({ title: m[4].trim(), start: h * 3600 + parseInt(m[2]) * 60 + parseInt(m[3]) }); } } - return chapters.length >= 2 ? chapters : []; + if (raw.length < 2) return []; + return raw.map((ch, i) => ({ + title: ch.title, + start: ch.start, + end: i < raw.length - 1 ? raw[i + 1].start : duration, + })); } function getThumbnailUrls(videoId: string, data: any): string[] { @@ -644,7 +650,8 @@ async function fetchAndCache(videoId: string, baseDir: string, opts: Options): P const info = findTranscript(transcripts, opts.languages, opts.excludeGenerated, opts.excludeManual); const result = await fetchTranscriptSnippets(info, opts.translate || undefined); const description = data?.videoDetails?.shortDescription || ""; - const chapters = parseChapters(description); + const duration = parseInt(data?.videoDetails?.lengthSeconds || "0"); + const chapters = parseChapters(description, duration); const langInfo = { code: result.languageCode, name: result.language, isGenerated: info.isGenerated }; const meta = buildVideoMeta(data, videoId, langInfo, chapters); From c7e32b45909cdcc69e2711b1c63bbca59abff712 Mon Sep 17 00:00:00 2001 From: jzocb Date: Sun, 22 Mar 2026 15:58:13 -0400 Subject: [PATCH 2/3] fix: backfill chapter end times for cached videos Videos cached before the chapter end-time change would silently lack the 'end' field when loaded from cache. This adds a migration that detects missing 'end' fields on cache hit, computes them from adjacent chapters, and persists the updated meta.json. This ensures consistent output regardless of whether the data was freshly fetched or loaded from cache. --- skills/baoyu-youtube-transcript/scripts/main.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/skills/baoyu-youtube-transcript/scripts/main.ts b/skills/baoyu-youtube-transcript/scripts/main.ts index 53ada2d..d1ba3d9 100644 --- a/skills/baoyu-youtube-transcript/scripts/main.ts +++ b/skills/baoyu-youtube-transcript/scripts/main.ts @@ -699,6 +699,15 @@ async function processVideo(videoId: string, opts: Options): Promise 0 && meta.chapters[0].end === undefined) { + for (let i = 0; i < meta.chapters.length; i++) { + (meta.chapters[i] as any).end = i < meta.chapters.length - 1 + ? meta.chapters[i + 1].start + : meta.duration; + } + writeFileSync(join(videoDir, "meta.json"), JSON.stringify(meta, null, 2)); + } } if (needsFetch) { From f53af25e65282880d24fdee83825df596e6828c1 Mon Sep 17 00:00:00 2001 From: jzocb Date: Sun, 22 Mar 2026 16:07:05 -0400 Subject: [PATCH 3/3] fix: address review feedback on chapter end times - Guard last chapter end against duration=0: use Math.max(duration, ch.start) - Remove unnecessary 'as any' cast in backfill - Check all chapters for missing end (not just first) via .some() - Skip backfill when needsFetch is true (about to refetch anyway) - Wrap backfill writeFileSync in try/catch (best-effort persistence) --- skills/baoyu-youtube-transcript/scripts/main.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skills/baoyu-youtube-transcript/scripts/main.ts b/skills/baoyu-youtube-transcript/scripts/main.ts index d1ba3d9..3e618e7 100644 --- a/skills/baoyu-youtube-transcript/scripts/main.ts +++ b/skills/baoyu-youtube-transcript/scripts/main.ts @@ -263,7 +263,7 @@ function parseChapters(description: string, duration: number = 0): Chapter[] { return raw.map((ch, i) => ({ title: ch.title, start: ch.start, - end: i < raw.length - 1 ? raw[i + 1].start : duration, + end: i < raw.length - 1 ? raw[i + 1].start : Math.max(duration, ch.start), })); } @@ -700,13 +700,13 @@ async function processVideo(videoId: string, opts: Options): Promise 0 && meta.chapters[0].end === undefined) { + if (!needsFetch && meta.chapters.length > 0 && meta.chapters.some((ch: any) => ch.end === undefined)) { for (let i = 0; i < meta.chapters.length; i++) { - (meta.chapters[i] as any).end = i < meta.chapters.length - 1 + meta.chapters[i].end = i < meta.chapters.length - 1 ? meta.chapters[i + 1].start - : meta.duration; + : Math.max(meta.duration, meta.chapters[i].start); } - writeFileSync(join(videoDir, "meta.json"), JSON.stringify(meta, null, 2)); + try { writeFileSync(join(videoDir, "meta.json"), JSON.stringify(meta, null, 2)); } catch {} } }