mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-12 13:59:47 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c44a524fa6 | |||
| 826535abe4 | |||
| fc50f31694 | |||
| 204765a137 | |||
| 4874cd2dae |
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||
"version": "1.89.1"
|
||||
"version": "1.90.0"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.89.1**.
|
||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.90.0**.
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
@@ -116,6 +116,10 @@ Xiaohongshu (RedNote) infographic series generator. Breaks down content into 1-1
|
||||
|
||||
# Direct content input
|
||||
/baoyu-xhs-images 今日星座运势
|
||||
|
||||
# Non-interactive (skip all confirmations, for scheduled tasks)
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
|
||||
```
|
||||
|
||||
**Styles** (visual aesthetics): `cute` (default), `fresh`, `warm`, `bold`, `minimal`, `retro`, `pop`, `notion`, `chalkboard`
|
||||
|
||||
@@ -116,6 +116,10 @@ clawhub install baoyu-markdown-to-html
|
||||
|
||||
# 直接输入内容
|
||||
/baoyu-xhs-images 今日星座运势
|
||||
|
||||
# 非交互模式(跳过所有确认,适用于定时任务)
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
|
||||
```
|
||||
|
||||
**风格**(视觉美学):`cute`(默认)、`fresh`、`warm`、`bold`、`minimal`、`retro`、`pop`、`notion`、`chalkboard`
|
||||
|
||||
@@ -352,7 +352,7 @@ WECHAT_APP_SECRET=<user_input>
|
||||
| Field | If Missing |
|
||||
|-------|------------|
|
||||
| Title | Prompt: "Enter title, or press Enter to auto-generate from content" |
|
||||
| Summary | Prompt: "Enter summary, or press Enter to auto-generate (recommended for SEO)" |
|
||||
| Summary | Use fallback chain: frontmatter `description` → frontmatter `summary` → prompt user or auto-generate |
|
||||
| Author | Use fallback chain: CLI `--author` → frontmatter `author` → EXTEND.md `default_author` |
|
||||
|
||||
**Auto-Generation Logic**:
|
||||
|
||||
@@ -39,6 +39,10 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
|
||||
# Direct input with options
|
||||
/baoyu-xhs-images --style bold --layout comparison
|
||||
[paste content]
|
||||
|
||||
# Non-interactive (for scheduled tasks / automation)
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes
|
||||
/baoyu-xhs-images posts/ai-future/article.md --yes --preset knowledge-card
|
||||
```
|
||||
|
||||
## Options
|
||||
@@ -48,6 +52,7 @@ Break down complex content into eye-catching infographic series for Xiaohongshu
|
||||
| `--style <name>` | Visual style (see Style Gallery) |
|
||||
| `--layout <name>` | Information layout (see Layout Gallery) |
|
||||
| `--preset <name>` | Style + layout shorthand (see [Style Presets](references/style-presets.md)) |
|
||||
| `--yes` | Non-interactive mode: skip all confirmations. Uses EXTEND.md preferences if found, otherwise uses defaults (no watermark, auto style/layout). Auto-confirms recommended plan (Path A). Suitable for scheduled tasks and automation. |
|
||||
|
||||
## Two Dimensions
|
||||
|
||||
@@ -237,11 +242,11 @@ Copy and track progress:
|
||||
|
||||
```
|
||||
XHS Infographic Progress:
|
||||
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING
|
||||
- [ ] Step 0: Check preferences (EXTEND.md) ⛔ BLOCKING (--yes: use defaults if not found)
|
||||
- [ ] Found → load preferences → continue
|
||||
- [ ] Not found → run first-time setup → MUST complete before Step 1
|
||||
- [ ] Not found → run first-time setup → MUST complete before Step 1 (--yes: skip setup, use defaults)
|
||||
- [ ] Step 1: Analyze content → analysis.md
|
||||
- [ ] Step 2: Smart Confirm ⚠️ REQUIRED
|
||||
- [ ] Step 2: Smart Confirm ⚠️ REQUIRED (--yes: auto-confirm Path A)
|
||||
- [ ] Path A: Quick confirm → generate recommended outline
|
||||
- [ ] Path B: Customize → adjust then generate outline
|
||||
- [ ] Path C: Detailed → 3 outlines → second confirm → generate outline
|
||||
@@ -252,26 +257,30 @@ XHS Infographic Progress:
|
||||
### Flow
|
||||
|
||||
```
|
||||
Input → [Step 0: Preferences] ─┬─ Found → Continue
|
||||
│
|
||||
└─ Not found → First-Time Setup ⛔ BLOCKING
|
||||
│
|
||||
└─ Complete setup → Save EXTEND.md → Continue
|
||||
│
|
||||
┌───────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
Analyze → [Smart Confirm] ─┬─ Quick: confirm recommended → outline.md → Generate → Complete
|
||||
│
|
||||
├─ Customize: adjust options → outline.md → Generate → Complete
|
||||
│
|
||||
└─ Detailed: 3 outlines → [Confirm 2] → outline.md → Generate → Complete
|
||||
Input → [--yes?] ─┬─ Yes → [Step 0: Load or defaults] → Analyze → Auto-confirm → Generate → Complete
|
||||
│
|
||||
└─ No → [Step 0: Preferences] ─┬─ Found → Continue
|
||||
│
|
||||
└─ Not found → First-Time Setup ⛔ BLOCKING
|
||||
│
|
||||
└─ Complete setup → Save EXTEND.md → Continue
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
Analyze → [Smart Confirm] ─┬─ Quick: confirm recommended → outline.md → Generate → Complete
|
||||
│
|
||||
├─ Customize: adjust options → outline.md → Generate → Complete
|
||||
│
|
||||
└─ Detailed: 3 outlines → [Confirm 2] → outline.md → Generate → Complete
|
||||
```
|
||||
|
||||
### Step 0: Load Preferences (EXTEND.md) ⛔ BLOCKING
|
||||
|
||||
**Purpose**: Load user preferences or run first-time setup.
|
||||
|
||||
**CRITICAL**: If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to content analysis, do NOT ask about style, do NOT ask about layout — ONLY complete the preferences setup first.
|
||||
**`--yes` mode**: If EXTEND.md found → load it. If not found → use built-in defaults (no watermark, style/layout auto-select, language from content). Do NOT run first-time setup, do NOT create EXTEND.md, do NOT ask any questions. Proceed directly to Step 1.
|
||||
|
||||
**CRITICAL** (interactive mode only): If EXTEND.md not found, MUST complete first-time setup before ANY other questions or steps. Do NOT proceed to content analysis, do NOT ask about style, do NOT ask about layout — ONLY complete the preferences setup first.
|
||||
|
||||
Check EXTEND.md existence (priority order):
|
||||
|
||||
@@ -340,7 +349,11 @@ Read source content, save it if needed, and perform deep analysis.
|
||||
|
||||
### Step 2: Smart Confirm ⚠️
|
||||
|
||||
**Purpose**: Present auto-recommended plan, let user confirm or adjust. **Do NOT skip.**
|
||||
**Purpose**: Present auto-recommended plan, let user confirm or adjust.
|
||||
|
||||
**`--yes` mode**: Skip this entire step. Use auto-recommended strategy + style + layout from Step 1 analysis (or `--style`/`--layout`/`--preset` if provided). Generate outline directly using Path A logic → save to `outline.md` → proceed to Step 3. No AskUserQuestion calls.
|
||||
|
||||
**Interactive mode**: Do NOT skip.
|
||||
|
||||
**Auto-Recommendation Logic**:
|
||||
1. Use Auto Selection table to match content signals → best strategy + style + layout
|
||||
@@ -491,7 +504,7 @@ Reference: `references/config/watermark-guide.md`
|
||||
|
||||
**Image Generation Skill Selection**:
|
||||
- Check available image generation skills
|
||||
- If multiple skills available, ask user preference
|
||||
- If multiple skills available: ask user preference (interactive) or use first available skill (`--yes` mode)
|
||||
|
||||
**Session Management**:
|
||||
If image generation skill supports `--sessionId`:
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { findTranscript, parseTranscriptJson3, parseWebVtt } from "./transcript.ts";
|
||||
import { buildTranscriptListFromYtDlp, resolveVideoSource, selectYtDlpTrack } from "./youtube.ts";
|
||||
import { buildTranscriptListFromYtDlp, fetchTranscriptWithFallback, resolveVideoSource, selectYtDlpTrack } from "./youtube.ts";
|
||||
|
||||
test("selectYtDlpTrack prefers json3 over xml and vtt", () => {
|
||||
const track = selectYtDlpTrack([
|
||||
@@ -123,3 +123,61 @@ test("resolveVideoSource falls back to yt-dlp only after fallback-eligible error
|
||||
assert.equal(fallbackCalled, true);
|
||||
assert.equal(source.transcripts[0].languageCode, "en");
|
||||
});
|
||||
|
||||
test("fetchTranscriptWithFallback retries with yt-dlp when InnerTube transcript payload is empty", async () => {
|
||||
const warnings: string[] = [];
|
||||
let fallbackCalled = false;
|
||||
const result = await fetchTranscriptWithFallback(
|
||||
"video12345ab",
|
||||
{
|
||||
kind: "innertube",
|
||||
data: { videoDetails: { title: "Primary" } },
|
||||
transcripts: [{
|
||||
language: "English",
|
||||
languageCode: "en",
|
||||
isGenerated: false,
|
||||
isTranslatable: false,
|
||||
baseUrl: "https://www.youtube.com/api/timedtext?v=video12345ab&lang=en&fmt=json3",
|
||||
translationLanguages: [],
|
||||
}],
|
||||
},
|
||||
{
|
||||
languages: ["en"],
|
||||
translate: "",
|
||||
excludeGenerated: false,
|
||||
excludeManual: false,
|
||||
},
|
||||
async (info) => {
|
||||
if (info.baseUrl.includes("youtube.com/api/timedtext")) {
|
||||
return { snippets: [], language: info.language, languageCode: info.languageCode };
|
||||
}
|
||||
return {
|
||||
snippets: [{ text: "Recovered subtitle", start: 0, duration: 2 }],
|
||||
language: info.language,
|
||||
languageCode: info.languageCode,
|
||||
};
|
||||
},
|
||||
async () => {
|
||||
fallbackCalled = true;
|
||||
return {
|
||||
kind: "yt-dlp",
|
||||
info: { title: "Fallback" },
|
||||
transcripts: [{
|
||||
language: "English",
|
||||
languageCode: "en",
|
||||
isGenerated: false,
|
||||
isTranslatable: false,
|
||||
baseUrl: "https://example.com/subtitles.en.json3",
|
||||
translationLanguages: [],
|
||||
}],
|
||||
};
|
||||
},
|
||||
(message) => warnings.push(message)
|
||||
);
|
||||
|
||||
assert.equal(fallbackCalled, true);
|
||||
assert.equal(result.source.kind, "yt-dlp");
|
||||
assert.equal(result.snippets.length, 1);
|
||||
assert.equal(result.snippets[0].text, "Recovered subtitle");
|
||||
assert.match(warnings[0] || "", /Retrying with yt-dlp fallback/);
|
||||
});
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
registerVideoDir,
|
||||
resolveBaseDir,
|
||||
} from "./storage.ts";
|
||||
import { findTranscript, formatListOutput, formatMarkdown, formatSrt, segmentIntoSentences } from "./transcript.ts";
|
||||
import { formatListOutput, formatMarkdown, formatSrt, segmentIntoSentences } from "./transcript.ts";
|
||||
import type { Options, Sentence, Snippet, VideoMeta, VideoResult } from "./types.ts";
|
||||
import {
|
||||
buildVideoMeta,
|
||||
buildVideoMetaFromYtDlp,
|
||||
downloadCoverImage,
|
||||
fetchTranscriptSnippets,
|
||||
fetchTranscriptWithFallback,
|
||||
fetchVideoSource,
|
||||
getThumbnailUrls,
|
||||
getYtDlpThumbnailUrls,
|
||||
@@ -31,10 +31,12 @@ async function fetchAndCache(
|
||||
baseDir: string,
|
||||
opts: Options
|
||||
): Promise<{ meta: VideoMeta; snippets: Snippet[]; sentences: Sentence[]; videoDir: string }> {
|
||||
const source = await fetchVideoSource(videoId);
|
||||
const requestedLanguages = source.kind === "yt-dlp" && opts.translate ? [opts.translate] : opts.languages;
|
||||
const transcript = findTranscript(source.transcripts, requestedLanguages, opts.excludeGenerated, opts.excludeManual);
|
||||
const result = await fetchTranscriptSnippets(transcript, source.kind === "yt-dlp" ? undefined : opts.translate || undefined);
|
||||
const initialSource = await fetchVideoSource(videoId);
|
||||
const { source, transcript, snippets, language, languageCode } = await fetchTranscriptWithFallback(
|
||||
videoId,
|
||||
initialSource,
|
||||
opts
|
||||
);
|
||||
const description = source.kind === "yt-dlp"
|
||||
? source.info.description || ""
|
||||
: source.data?.videoDetails?.shortDescription || "";
|
||||
@@ -42,21 +44,21 @@ async function fetchAndCache(
|
||||
? Number(source.info.duration || 0)
|
||||
: parseInt(source.data?.videoDetails?.lengthSeconds || "0");
|
||||
const chapters = parseChapters(description, duration);
|
||||
const language = {
|
||||
code: result.languageCode,
|
||||
name: result.language,
|
||||
const languageMeta = {
|
||||
code: languageCode,
|
||||
name: language,
|
||||
isGenerated: transcript.isGenerated,
|
||||
};
|
||||
const meta = source.kind === "yt-dlp"
|
||||
? buildVideoMetaFromYtDlp(source.info, videoId, language, chapters)
|
||||
: buildVideoMeta(source.data, videoId, language, chapters);
|
||||
? buildVideoMetaFromYtDlp(source.info, videoId, languageMeta, chapters)
|
||||
: buildVideoMeta(source.data, videoId, languageMeta, chapters);
|
||||
|
||||
const videoDir = registerVideoDir(videoId, slugify(meta.channel), slugify(meta.title), baseDir);
|
||||
ensureDir(join(videoDir, "meta.json"));
|
||||
|
||||
writeFileSync(join(videoDir, "transcript-raw.json"), JSON.stringify(result.snippets, null, 2));
|
||||
writeFileSync(join(videoDir, "transcript-raw.json"), JSON.stringify(snippets, null, 2));
|
||||
|
||||
const sentences = segmentIntoSentences(result.snippets);
|
||||
const sentences = segmentIntoSentences(snippets);
|
||||
writeFileSync(join(videoDir, "transcript-sentences.json"), JSON.stringify(sentences, null, 2));
|
||||
|
||||
const imagePath = join(videoDir, "imgs", "cover.jpg");
|
||||
@@ -69,7 +71,7 @@ async function fetchAndCache(
|
||||
|
||||
writeFileSync(join(videoDir, "meta.json"), JSON.stringify(meta, null, 2));
|
||||
|
||||
return { meta, snippets: result.snippets, sentences, videoDir };
|
||||
return { meta, snippets, sentences, videoDir };
|
||||
}
|
||||
|
||||
async function processVideo(videoId: string, opts: Options): Promise<VideoResult> {
|
||||
|
||||
@@ -2,12 +2,13 @@ import { spawnSync } from "child_process";
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
import { makeError, normalizeError, normalizePublishDate, shouldTryAlternateClient, shouldTryYtDlpFallback } from "./shared.ts";
|
||||
import { parseTranscriptPayload } from "./transcript.ts";
|
||||
import { findTranscript, parseTranscriptPayload } from "./transcript.ts";
|
||||
import type {
|
||||
Chapter,
|
||||
InnerTubeClient,
|
||||
InnerTubeSession,
|
||||
LanguageMeta,
|
||||
Options,
|
||||
Snippet,
|
||||
TranscriptInfo,
|
||||
VideoMeta,
|
||||
@@ -219,6 +220,68 @@ export async function fetchTranscriptSnippets(
|
||||
};
|
||||
}
|
||||
|
||||
function buildYtDlpVideoSource(videoId: string, info: YtDlpInfo): VideoSource {
|
||||
const transcripts = buildTranscriptListFromYtDlp(info);
|
||||
if (!transcripts.length) throw makeError(`Transcripts disabled for ${videoId}`, "TRANSCRIPTS_DISABLED");
|
||||
return { kind: "yt-dlp", info, transcripts };
|
||||
}
|
||||
|
||||
function getRequestedLanguages(
|
||||
source: VideoSource,
|
||||
opts: Pick<Options, "languages" | "translate">
|
||||
): string[] {
|
||||
return source.kind === "yt-dlp" && opts.translate ? [opts.translate] : opts.languages;
|
||||
}
|
||||
|
||||
export async function fetchTranscriptWithFallback(
|
||||
videoId: string,
|
||||
source: VideoSource,
|
||||
opts: Pick<Options, "languages" | "translate" | "excludeGenerated" | "excludeManual">,
|
||||
fetchSnippets: (
|
||||
info: TranscriptInfo,
|
||||
translateTo?: string
|
||||
) => Promise<{ snippets: Snippet[]; language: string; languageCode: string }> = fetchTranscriptSnippets,
|
||||
fetchFallbackSource: (videoId: string) => Promise<VideoSource> | VideoSource = (requestedVideoId) =>
|
||||
buildYtDlpVideoSource(requestedVideoId, fetchYtDlpInfo(requestedVideoId)),
|
||||
logWarning: (message: string) => void = (message) => console.error(message)
|
||||
): Promise<{
|
||||
source: VideoSource;
|
||||
transcript: TranscriptInfo;
|
||||
snippets: Snippet[];
|
||||
language: string;
|
||||
languageCode: string;
|
||||
}> {
|
||||
const transcript = findTranscript(
|
||||
source.transcripts,
|
||||
getRequestedLanguages(source, opts),
|
||||
opts.excludeGenerated,
|
||||
opts.excludeManual
|
||||
);
|
||||
const result = await fetchSnippets(transcript, source.kind === "yt-dlp" ? undefined : opts.translate || undefined);
|
||||
if (result.snippets.length > 0) return { source, transcript, ...result };
|
||||
|
||||
if (source.kind === "yt-dlp") {
|
||||
throw makeError(`Transcript fetch returned empty snippets for ${videoId}`, "EMPTY_TRANSCRIPT");
|
||||
}
|
||||
|
||||
logWarning(`Warning (${videoId}): Transcript fetch returned empty snippets. Retrying with yt-dlp fallback.`);
|
||||
const fallbackSource = await fetchFallbackSource(videoId);
|
||||
const fallbackTranscript = findTranscript(
|
||||
fallbackSource.transcripts,
|
||||
getRequestedLanguages(fallbackSource, opts),
|
||||
opts.excludeGenerated,
|
||||
opts.excludeManual
|
||||
);
|
||||
const fallbackResult = await fetchSnippets(
|
||||
fallbackTranscript,
|
||||
fallbackSource.kind === "yt-dlp" ? undefined : opts.translate || undefined
|
||||
);
|
||||
if (!fallbackResult.snippets.length) {
|
||||
throw makeError(`Transcript fetch returned empty snippets for ${videoId} after yt-dlp fallback`, "EMPTY_TRANSCRIPT");
|
||||
}
|
||||
return { source: fallbackSource, transcript: fallbackTranscript, ...fallbackResult };
|
||||
}
|
||||
|
||||
export function detectYtDlpCommand(): { command: string; args: string[]; label: string } | null {
|
||||
if (cachedYtDlpCommand !== undefined) return cachedYtDlpCommand;
|
||||
const candidates = [
|
||||
@@ -366,10 +429,7 @@ export async function resolveVideoSource(
|
||||
const normalized = normalizeError(error);
|
||||
if (!shouldTryYtDlpFallback(normalized)) throw normalized;
|
||||
logWarning(`Warning (${videoId}): ${normalized.message}. Retrying with yt-dlp fallback.`);
|
||||
const info = fetchFallback(videoId);
|
||||
const transcripts = buildTranscriptListFromYtDlp(info);
|
||||
if (!transcripts.length) throw makeError(`Transcripts disabled for ${videoId}`, "TRANSCRIPTS_DISABLED");
|
||||
return { kind: "yt-dlp", info, transcripts };
|
||||
return buildYtDlpVideoSource(videoId, fetchFallback(videoId));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user