mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-13 22:29:48 +08:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a4b312146 | |||
| 2d6fe533eb | |||
| f53af25e65 | |||
| c7e32b4590 | |||
| 8d973f2bc5 | |||
| ba20cf89f2 | |||
| 1827be9234 | |||
| 93c98dfc3c | |||
| fbd9f9b622 | |||
| b6e293d059 | |||
| bb78aab095 |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.75.0"
|
||||
"version": "1.77.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
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
|
||||
|
||||
### Documentation
|
||||
- `baoyu-youtube-transcript`: fix zsh glob issue — always single-quote YouTube URLs when running the script
|
||||
|
||||
## 1.76.0 - 2026-03-21
|
||||
|
||||
### Features
|
||||
- `baoyu-youtube-transcript`: add title heading, description summary, and cover image to markdown output
|
||||
|
||||
### Fixes
|
||||
- `baoyu-markdown-to-html`: use process.execPath and tsx import in test runner
|
||||
|
||||
## 1.75.0 - 2026-03-21
|
||||
|
||||
### Features
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
[English](./CHANGELOG.md) | 中文
|
||||
|
||||
## 1.77.0 - 2026-03-22
|
||||
|
||||
### 新功能
|
||||
- `baoyu-youtube-transcript`:为章节数据添加结束时间 (by @jzOcb)
|
||||
|
||||
### 修复
|
||||
- `sync-clawhub`:跳过失败的技能而不是中止同步
|
||||
|
||||
## 1.76.1 - 2026-03-21
|
||||
|
||||
### 文档
|
||||
- `baoyu-youtube-transcript`:修复 zsh glob 问题 — 运行脚本时始终对 YouTube URL 使用单引号
|
||||
|
||||
## 1.76.0 - 2026-03-21
|
||||
|
||||
### 新功能
|
||||
- `baoyu-youtube-transcript`:Markdown 输出中新增标题、描述摘要和封面图片
|
||||
|
||||
### 修复
|
||||
- `baoyu-markdown-to-html`:测试运行器改用 process.execPath 和 tsx import
|
||||
|
||||
## 1.75.0 - 2026-03-21
|
||||
|
||||
### 新功能
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.75.0**.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.77.0**.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
+24
-11
@@ -151,6 +151,9 @@ async function main() {
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
let succeeded = 0;
|
||||
const failed = [];
|
||||
|
||||
for (const candidate of actionable) {
|
||||
const version =
|
||||
candidate.status === "new"
|
||||
@@ -158,20 +161,30 @@ async function main() {
|
||||
: bumpSemver(candidate.latestVersion, options.bump);
|
||||
|
||||
console.log(`Publishing ${candidate.slug}@${version}`);
|
||||
const files = await listTextFiles(candidate.folder);
|
||||
await publishSkill({
|
||||
registry,
|
||||
token: config.token,
|
||||
skill: candidate,
|
||||
files,
|
||||
version,
|
||||
changelog: options.changelog,
|
||||
tags,
|
||||
});
|
||||
try {
|
||||
const files = await listTextFiles(candidate.folder);
|
||||
await publishSkill({
|
||||
registry,
|
||||
token: config.token,
|
||||
skill: candidate,
|
||||
files,
|
||||
version,
|
||||
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(`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) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
@@ -21,8 +22,10 @@ test("CLI forwards wrapper title and vendor render options", async () => {
|
||||
await fs.writeFile(markdownPath, "## Section\n\nParagraph with **bold** text.\n", "utf-8");
|
||||
|
||||
const { stdout } = await execFileAsync(
|
||||
"bun",
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
SCRIPT_PATH,
|
||||
markdownPath,
|
||||
"--theme", "grace",
|
||||
|
||||
@@ -88,7 +88,7 @@ Accepts any of these as video input:
|
||||
|
||||
| Format | Extension | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `text` | `.md` | Markdown with frontmatter, natural paragraphs, optional timestamps/chapters/speakers |
|
||||
| `text` | `.md` | Markdown with frontmatter (incl. `description`), title heading, summary, optional TOC/cover/timestamps/chapters/speakers |
|
||||
| `srt` | `.srt` | SubRip subtitle format for video players |
|
||||
|
||||
## Output Directory
|
||||
@@ -128,7 +128,8 @@ SRT output (`--format srt`) is generated from `transcript-raw.json`. Text/markdo
|
||||
When user provides a YouTube URL and wants the transcript:
|
||||
|
||||
1. Run with `--list` first if the user hasn't specified a language, to show available options
|
||||
2. Default: run with `--chapters --speakers` for the richest output (chapters + speaker identification)
|
||||
2. **Always single-quote the URL** when running the script — zsh treats `?` as a glob wildcard, so an unquoted YouTube URL causes "no matches found": use `'https://www.youtube.com/watch?v=ID'`
|
||||
3. Default: run with `--chapters --speakers` for the richest output (chapters + speaker identification)
|
||||
3. The script auto-saves cached data + output file and prints the file path
|
||||
4. For `--speakers` mode: after the script saves the raw file, follow the speaker identification workflow below to post-process with speaker labels
|
||||
|
||||
@@ -147,7 +148,7 @@ If no chapter timestamps exist in the description, the transcript is output as g
|
||||
### Speaker Identification (`--speakers`)
|
||||
|
||||
Speaker identification requires AI processing. The script outputs a raw `.md` file containing:
|
||||
- YAML frontmatter with video metadata (title, channel, date, cover, language)
|
||||
- YAML frontmatter with video metadata (title, channel, date, cover, description, language)
|
||||
- Video description (for speaker name extraction)
|
||||
- Chapter list from description (if available)
|
||||
- Raw transcript in SRT format (pre-computed start/end timestamps, token-efficient)
|
||||
|
||||
@@ -5,9 +5,12 @@ You are an expert transcript specialist. Process the raw transcript file (with Y
|
||||
## Output Structure
|
||||
|
||||
Produce a single cohesive markdown file containing:
|
||||
1. YAML frontmatter (keep the original frontmatter from the raw file)
|
||||
2. Table of Contents
|
||||
3. Full chapter-segmented transcript with speaker labels
|
||||
1. YAML frontmatter (keep the original frontmatter from the raw file, which includes `description`)
|
||||
2. `# Title` heading (from frontmatter title)
|
||||
3. Description/summary paragraph (from frontmatter `description`)
|
||||
4. Table of Contents
|
||||
5. Cover image (if `cover` exists in frontmatter): `` — right after the ToC
|
||||
6. Full chapter-segmented transcript with speaker labels
|
||||
|
||||
Use the same language as the transcription for the title and ToC.
|
||||
|
||||
@@ -79,13 +82,20 @@ channel: "The Show"
|
||||
date: 2024-04-15
|
||||
url: "https://www.youtube.com/watch?v=xxx"
|
||||
cover: imgs/cover.jpg
|
||||
description: "Jane Doe discusses her groundbreaking five-year study on the long-term effects of dietary changes."
|
||||
language: en
|
||||
---
|
||||
|
||||
# Example Interview
|
||||
|
||||
Jane Doe discusses her groundbreaking five-year study on the long-term effects of dietary changes.
|
||||
|
||||
## Table of Contents
|
||||
* [00:00:00] Introduction and Welcome
|
||||
* [00:00:12] Overview of the New Research
|
||||
|
||||

|
||||
|
||||
|
||||
## Introduction and Welcome [00:00:00]
|
||||
|
||||
|
||||
@@ -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 : Math.max(duration, ch.start),
|
||||
}));
|
||||
}
|
||||
|
||||
function getThumbnailUrls(videoId: string, data: any): string[] {
|
||||
@@ -505,17 +511,28 @@ function yamlEscape(s: string): string {
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractSummary(description: string): string {
|
||||
if (!description) return "";
|
||||
const firstPara = description.split(/\n\s*\n/)[0].trim();
|
||||
const lines = firstPara.split("\n").filter(l => !/^\s*(https?:\/\/|#|@|\d+:\d+)/.test(l) && l.trim());
|
||||
return lines.join(" ").slice(0, 300).trim();
|
||||
}
|
||||
|
||||
function formatMarkdown(sentences: Sentence[], meta: VideoMeta, opts: { timestamps: boolean; chapters: boolean; speakers: boolean }, snippets?: Snippet[]): string {
|
||||
const summary = extractSummary(meta.description);
|
||||
let md = "---\n";
|
||||
md += `title: ${yamlEscape(meta.title)}\n`;
|
||||
md += `channel: ${yamlEscape(meta.channel)}\n`;
|
||||
if (meta.publishDate) md += `date: ${meta.publishDate}\n`;
|
||||
md += `url: ${yamlEscape(meta.url)}\n`;
|
||||
if (meta.coverImage) md += `cover: ${meta.coverImage}\n`;
|
||||
if (summary) md += `description: ${yamlEscape(summary)}\n`;
|
||||
if (meta.language) md += `language: ${meta.language.code}\n`;
|
||||
md += "---\n\n";
|
||||
|
||||
if (opts.speakers) {
|
||||
md += `# ${meta.title}\n\n`;
|
||||
if (summary) md += `${summary}\n\n`;
|
||||
if (meta.description) md += "# Description\n\n" + meta.description.trim() + "\n\n";
|
||||
if (meta.chapters.length) {
|
||||
md += "# Chapters\n\n";
|
||||
@@ -527,12 +544,17 @@ function formatMarkdown(sentences: Sentence[], meta: VideoMeta, opts: { timestam
|
||||
return md;
|
||||
}
|
||||
|
||||
md += `# ${meta.title}\n\n`;
|
||||
if (summary) md += `${summary}\n\n`;
|
||||
|
||||
const chapters = opts.chapters ? meta.chapters : [];
|
||||
|
||||
if (chapters.length) {
|
||||
md += "## Table of Contents\n\n";
|
||||
for (const ch of chapters) md += opts.timestamps ? `* [${ts(ch.start)}] ${ch.title}\n` : `* ${ch.title}\n`;
|
||||
md += "\n\n";
|
||||
md += "\n";
|
||||
if (meta.coverImage) md += `\n\n`;
|
||||
md += "\n";
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const nextStart = i < chapters.length - 1 ? chapters[i + 1].start : Infinity;
|
||||
const chSentences = sentences.filter(s => parseTs(s.start) >= chapters[i].start && parseTs(s.start) < nextStart);
|
||||
@@ -628,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);
|
||||
|
||||
@@ -676,6 +699,15 @@ async function processVideo(videoId: string, opts: Options): Promise<VideoResult
|
||||
sentences = loadSentences(videoDir);
|
||||
const wantLangs = opts.translate ? [opts.translate] : opts.languages;
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user