Compare commits

..

5 Commits

Author SHA1 Message Date
Jim Liu 宝玉 1827be9234 chore: release v1.76.1 2026-03-21 23:17:04 -05:00
Jim Liu 宝玉 93c98dfc3c docs(baoyu-youtube-transcript): fix zsh glob issue for YouTube URLs 2026-03-21 23:16:51 -05:00
Jim Liu 宝玉 fbd9f9b622 chore: release v1.76.0 2026-03-21 23:08:21 -05:00
Jim Liu 宝玉 b6e293d059 fix(baoyu-markdown-to-html): use process.execPath and tsx import in test runner 2026-03-21 23:07:46 -05:00
Jim Liu 宝玉 bb78aab095 feat(baoyu-youtube-transcript): add title heading, description summary, and cover image to markdown output 2026-03-21 23:07:44 -05:00
8 changed files with 66 additions and 10 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.75.0" "version": "1.76.1"
}, },
"plugins": [ "plugins": [
{ {
+13
View File
@@ -2,6 +2,19 @@
English | [中文](./CHANGELOG.zh.md) English | [中文](./CHANGELOG.zh.md)
## 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 ## 1.75.0 - 2026-03-21
### Features ### Features
+13
View File
@@ -2,6 +2,19 @@
[English](./CHANGELOG.md) | 中文 [English](./CHANGELOG.md) | 中文
## 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.75.0 - 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.75.0**. Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.76.0**.
## Architecture ## Architecture
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import process from "node:process";
import test from "node:test"; import test from "node:test";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { promisify } from "node:util"; 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"); await fs.writeFile(markdownPath, "## Section\n\nParagraph with **bold** text.\n", "utf-8");
const { stdout } = await execFileAsync( const { stdout } = await execFileAsync(
"bun", process.execPath,
[ [
"--import",
"tsx",
SCRIPT_PATH, SCRIPT_PATH,
markdownPath, markdownPath,
"--theme", "grace", "--theme", "grace",
+4 -3
View File
@@ -88,7 +88,7 @@ Accepts any of these as video input:
| Format | Extension | Description | | 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 | | `srt` | `.srt` | SubRip subtitle format for video players |
## Output Directory ## 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: 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 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 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 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 (`--speakers`)
Speaker identification requires AI processing. The script outputs a raw `.md` file containing: 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) - Video description (for speaker name extraction)
- Chapter list from description (if available) - Chapter list from description (if available)
- Raw transcript in SRT format (pre-computed start/end timestamps, token-efficient) - 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 ## Output Structure
Produce a single cohesive markdown file containing: Produce a single cohesive markdown file containing:
1. YAML frontmatter (keep the original frontmatter from the raw file) 1. YAML frontmatter (keep the original frontmatter from the raw file, which includes `description`)
2. Table of Contents 2. `# Title` heading (from frontmatter title)
3. Full chapter-segmented transcript with speaker labels 3. Description/summary paragraph (from frontmatter `description`)
4. Table of Contents
5. Cover image (if `cover` exists in frontmatter): `![cover](imgs/cover.jpg)` — right after the ToC
6. Full chapter-segmented transcript with speaker labels
Use the same language as the transcription for the title and ToC. Use the same language as the transcription for the title and ToC.
@@ -79,13 +82,20 @@ channel: "The Show"
date: 2024-04-15 date: 2024-04-15
url: "https://www.youtube.com/watch?v=xxx" url: "https://www.youtube.com/watch?v=xxx"
cover: imgs/cover.jpg cover: imgs/cover.jpg
description: "Jane Doe discusses her groundbreaking five-year study on the long-term effects of dietary changes."
language: en language: en
--- ---
# Example Interview
Jane Doe discusses her groundbreaking five-year study on the long-term effects of dietary changes.
## Table of Contents ## Table of Contents
* [00:00:00] Introduction and Welcome * [00:00:00] Introduction and Welcome
* [00:00:12] Overview of the New Research * [00:00:12] Overview of the New Research
![cover](imgs/cover.jpg)
## Introduction and Welcome [00:00:00] ## Introduction and Welcome [00:00:00]
@@ -505,17 +505,28 @@ function yamlEscape(s: string): string {
return s; 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 { function formatMarkdown(sentences: Sentence[], meta: VideoMeta, opts: { timestamps: boolean; chapters: boolean; speakers: boolean }, snippets?: Snippet[]): string {
const summary = extractSummary(meta.description);
let md = "---\n"; let md = "---\n";
md += `title: ${yamlEscape(meta.title)}\n`; md += `title: ${yamlEscape(meta.title)}\n`;
md += `channel: ${yamlEscape(meta.channel)}\n`; md += `channel: ${yamlEscape(meta.channel)}\n`;
if (meta.publishDate) md += `date: ${meta.publishDate}\n`; if (meta.publishDate) md += `date: ${meta.publishDate}\n`;
md += `url: ${yamlEscape(meta.url)}\n`; md += `url: ${yamlEscape(meta.url)}\n`;
if (meta.coverImage) md += `cover: ${meta.coverImage}\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`; if (meta.language) md += `language: ${meta.language.code}\n`;
md += "---\n\n"; md += "---\n\n";
if (opts.speakers) { 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.description) md += "# Description\n\n" + meta.description.trim() + "\n\n";
if (meta.chapters.length) { if (meta.chapters.length) {
md += "# Chapters\n\n"; md += "# Chapters\n\n";
@@ -527,12 +538,17 @@ function formatMarkdown(sentences: Sentence[], meta: VideoMeta, opts: { timestam
return md; return md;
} }
md += `# ${meta.title}\n\n`;
if (summary) md += `${summary}\n\n`;
const chapters = opts.chapters ? meta.chapters : []; const chapters = opts.chapters ? meta.chapters : [];
if (chapters.length) { if (chapters.length) {
md += "## Table of Contents\n\n"; md += "## Table of Contents\n\n";
for (const ch of chapters) md += opts.timestamps ? `* [${ts(ch.start)}] ${ch.title}\n` : `* ${ch.title}\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![cover](${meta.coverImage})\n`;
md += "\n";
for (let i = 0; i < chapters.length; i++) { for (let i = 0; i < chapters.length; i++) {
const nextStart = i < chapters.length - 1 ? chapters[i + 1].start : Infinity; 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); const chSentences = sentences.filter(s => parseTs(s.start) >= chapters[i].start && parseTs(s.start) < nextStart);