Compare commits

..

6 Commits

Author SHA1 Message Date
Jim Liu 宝玉 6a4b312146 chore: release v1.77.0 2026-03-22 15:17:12 -05:00
Jim Liu 宝玉 2d6fe533eb Merge pull request #105 from jzOcb/feat/chapter-end-times
feat(youtube-transcript): add end times to chapter data
2026-03-22 15:13:44 -05:00
jzocb f53af25e65 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)
2026-03-22 16:07:05 -04:00
jzocb c7e32b4590 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.
2026-03-22 15:58:13 -04:00
jzocb 8d973f2bc5 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 }
2026-03-22 15:52:30 -04:00
Jim Liu 宝玉 ba20cf89f2 fix(sync-clawhub): skip failed skills instead of aborting 2026-03-21 23:25:45 -05:00
6 changed files with 63 additions and 18 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
}, },
"metadata": { "metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency", "description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.76.1" "version": "1.77.0"
}, },
"plugins": [ "plugins": [
{ {
+8
View File
@@ -2,6 +2,14 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 1.77.0 - 2026-03-22
### Features
- `baoyu-youtube-transcript`: add end times to chapter data (by @jzOcb)
### Fixes
- `sync-clawhub`: skip failed skills instead of aborting
## 1.76.1 - 2026-03-21 ## 1.76.1 - 2026-03-21
### Documentation ### Documentation
+8
View File
@@ -2,6 +2,14 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 1.77.0 - 2026-03-22
### 新功能
- `baoyu-youtube-transcript`:为章节数据添加结束时间 (by @jzOcb)
### 修复
- `sync-clawhub`:跳过失败的技能而不是中止同步
## 1.76.1 - 2026-03-21 ## 1.76.1 - 2026-03-21
### 文档 ### 文档
+1 -1
View File
@@ -1,6 +1,6 @@
# CLAUDE.md # CLAUDE.md
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.76.0**. Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.77.0**.
## Architecture ## Architecture
+24 -11
View File
@@ -151,6 +151,9 @@ async function main() {
.map((tag) => tag.trim()) .map((tag) => tag.trim())
.filter(Boolean); .filter(Boolean);
let succeeded = 0;
const failed = [];
for (const candidate of actionable) { for (const candidate of actionable) {
const version = const version =
candidate.status === "new" candidate.status === "new"
@@ -158,20 +161,30 @@ async function main() {
: bumpSemver(candidate.latestVersion, options.bump); : bumpSemver(candidate.latestVersion, options.bump);
console.log(`Publishing ${candidate.slug}@${version}`); console.log(`Publishing ${candidate.slug}@${version}`);
const files = await listTextFiles(candidate.folder); try {
await publishSkill({ const files = await listTextFiles(candidate.folder);
registry, await publishSkill({
token: config.token, registry,
skill: candidate, token: config.token,
files, skill: candidate,
version, files,
changelog: options.changelog, version,
tags, changelog: options.changelog,
}); tags,
});
succeeded++;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.error(`SKIPPED ${candidate.slug}: ${msg}`);
failed.push(candidate.slug);
}
} }
console.log(""); console.log("");
console.log(`Uploaded ${actionable.length} skill(s).`); console.log(`Uploaded ${succeeded}/${actionable.length} skill(s).`);
if (failed.length > 0) {
console.log(`Failed (${failed.length}): ${failed.join(", ")}`);
}
} }
function parseArgs(argv) { function parseArgs(argv) {
@@ -44,6 +44,7 @@ interface TranscriptInfo {
interface Chapter { interface Chapter {
title: string; title: string;
start: number; start: number;
end: number;
} }
interface VideoMeta { interface VideoMeta {
@@ -249,16 +250,21 @@ async function fetchTranscriptSnippets(info: TranscriptInfo, translateTo?: strin
// --- Metadata & chapters --- // --- Metadata & chapters ---
function parseChapters(description: string): Chapter[] { function parseChapters(description: string, duration: number = 0): Chapter[] {
const chapters: Chapter[] = []; const raw: { title: string; start: number }[] = [];
for (const line of description.split("\n")) { for (const line of description.split("\n")) {
const m = line.trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})\s+(.+)$/); const m = line.trim().match(/^(?:(\d{1,2}):)?(\d{1,2}):(\d{2})\s+(.+)$/);
if (m) { if (m) {
const h = m[1] ? parseInt(m[1]) : 0; 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 : Math.max(duration, ch.start),
}));
} }
function getThumbnailUrls(videoId: string, data: any): string[] { 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 info = findTranscript(transcripts, opts.languages, opts.excludeGenerated, opts.excludeManual);
const result = await fetchTranscriptSnippets(info, opts.translate || undefined); const result = await fetchTranscriptSnippets(info, opts.translate || undefined);
const description = data?.videoDetails?.shortDescription || ""; 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 langInfo = { code: result.languageCode, name: result.language, isGenerated: info.isGenerated };
const meta = buildVideoMeta(data, videoId, langInfo, chapters); const meta = buildVideoMeta(data, videoId, langInfo, chapters);
@@ -692,6 +699,15 @@ async function processVideo(videoId: string, opts: Options): Promise<VideoResult
sentences = loadSentences(videoDir); sentences = loadSentences(videoDir);
const wantLangs = opts.translate ? [opts.translate] : opts.languages; const wantLangs = opts.translate ? [opts.translate] : opts.languages;
if (!wantLangs.includes(meta.language.code)) needsFetch = true; if (!wantLangs.includes(meta.language.code)) needsFetch = true;
// Backfill chapter end times for caches created before this field existed
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].end = i < meta.chapters.length - 1
? meta.chapters[i + 1].start
: Math.max(meta.duration, meta.chapters[i].start);
}
try { writeFileSync(join(videoDir, "meta.json"), JSON.stringify(meta, null, 2)); } catch {}
}
} }
if (needsFetch) { if (needsFetch) {