mirror of
https://github.com/daymade/claude-code-skills.git
synced 2026-09-14 16:15:21 +08:00
deprecate(slides-creator): retire to stub(方法论已并入 deck-creator Route A)
与 ppt-creator 同一合并(2026-08-07 PPT 工具链四合一): First Law / ABCDEFG / baoyu 委托协议 / 四层目录治理已整体并入 deck-creator Route A · narrative。公共仓退役为 stub: SKILL.md 改退役通告,references/scripts 移除(git 历史保留; prompt-templates 4 件已先迁入 deck-creator route-a/)。 marketplace 2.0.0 + CHANGELOG。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
Security scan passed
|
||||
Scanned at: 2026-04-19T23:10:12.585729
|
||||
Tool: gitleaks + pattern-based validation
|
||||
Content hash: 12cbafeb3e9d8320bbd4469335891d8a58bacf49c11d22a6e346dc038a90fb09
|
||||
@@ -1,116 +0,0 @@
|
||||
# Content Creation First Law
|
||||
|
||||
## The Principle
|
||||
|
||||
**AI cannot write high-quality content for the user. It can only help the user express their own content better.**
|
||||
|
||||
> The most important step in creating any piece of content — whether a slide deck, an article, an advertisement, or a course — is **collecting the user's original words first**.
|
||||
>
|
||||
> Without the user's source material, AI produces polished, plausible, unusable garbage. It may sound impressive, but it won't sound like the user. It won't reflect their expertise. It won't be something they can confidently deliver or stand behind.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
| Approach | Result |
|
||||
|----------|--------|
|
||||
| AI writes alone | Generic, AI-tinged content that the user cannot authentically deliver |
|
||||
| AI + user's raw material | Content that sounds like the user at their best — authentic, specific, powerful |
|
||||
|
||||
The goal is not to have AI do what the user cannot do. The goal is to **reduce the user's workload** while **preserving and elevating their voice**.
|
||||
|
||||
## The Weight Hierarchy
|
||||
|
||||
All content sources are NOT equal. Use this priority order:
|
||||
|
||||
```
|
||||
1. User's own words (highest authority)
|
||||
- Transcripts of their talks
|
||||
- Articles they wrote
|
||||
- Notes they prepared
|
||||
- Voice memos or recordings
|
||||
- Their explicit statements during discussion
|
||||
|
||||
2. User-approved external material
|
||||
- Articles they selected
|
||||
- References they endorsed
|
||||
- Research they reviewed
|
||||
|
||||
3. AI synthesis (requires user validation)
|
||||
- AI-organized structure
|
||||
- AI-suggested transitions
|
||||
- AI-generated visual descriptions
|
||||
|
||||
4. AI invention (lowest authority, use sparingly)
|
||||
- AI-fabricated examples
|
||||
- AI-generated quotes
|
||||
- AI-invented statistics
|
||||
→ MUST be labeled as draft, MUST be validated by user
|
||||
```
|
||||
|
||||
## The Process
|
||||
|
||||
### Step 1: Collect
|
||||
|
||||
Before creating ANY content, collect from the user:
|
||||
|
||||
- Past transcripts (meetings, talks, interviews)
|
||||
- Existing writings (articles, drafts, notes)
|
||||
- Previous decks or materials
|
||||
- Voice memos or recorded thoughts
|
||||
- Key messages they want to convey
|
||||
|
||||
If no written material exists, the entire narrative must be extracted through structured conversation. **Do not fabricate.**
|
||||
|
||||
### Step 2: Validate
|
||||
|
||||
For external materials:
|
||||
- Present findings to the user
|
||||
- Let them select what is relevant
|
||||
- Only include user-approved references
|
||||
|
||||
### Step 3: Assist
|
||||
|
||||
With user source material as the foundation:
|
||||
- Organize and structure their ideas
|
||||
- Suggest narrative arcs and transitions
|
||||
- Design visuals that amplify their message
|
||||
- Polish expression without changing substance
|
||||
|
||||
### Step 4: Verify
|
||||
|
||||
Final content must pass:
|
||||
- [ ] Could the user actually say this in their own words?
|
||||
- [ ] Does it reflect their expertise and perspective?
|
||||
- [ ] Are all claims traceable to user's words or approved sources?
|
||||
- [ ] Does it sound like them, not like generic AI output?
|
||||
|
||||
## Failure Modes
|
||||
|
||||
| Failure | Symptom | Prevention |
|
||||
|---------|---------|------------|
|
||||
| AI hallucinated content | User says "This doesn't sound like me" or "I never said that" | Always ground content in Phase 0 source materials |
|
||||
| AI invented examples | Examples feel generic or disconnected from user's experience | Ask user for real examples; mark gaps as [TODO] |
|
||||
| AI-generated quotes | Quotes don't match user's speaking style | Use verbatim from transcripts; don't fabricate |
|
||||
| AI chose references | User disagrees with included sources | Present options; user selects |
|
||||
| Content is "AI-flavored" | Long sentences, hedging language, "delve into", "leverage" | Rewrite in user's voice; use their sentence patterns |
|
||||
|
||||
## Application to Different Content Types
|
||||
|
||||
| Content Type | User Source Material | AI's Role |
|
||||
|--------------|---------------------|-----------|
|
||||
| **Slide deck** | Transcripts, talk notes, previous decks | Structure narrative, design visuals, synthesize |
|
||||
| **Article** | Draft notes, voice memos, talking points | Organize structure, improve flow, polish language |
|
||||
| **Advertisement** | Product description, user testimonials, brand voice guide | Write copy that matches brand voice, suggest hooks |
|
||||
| **Course** | Teaching notes, workshop transcripts, exercise ideas | Design curriculum, create pacing, build assessments |
|
||||
| **Email/WeChat** | Their usual writing style, specific message intent | Draft in their voice, suggest phrasing |
|
||||
|
||||
## Corollaries
|
||||
|
||||
1. **If the user has no existing content**, your job is to help them articulate their thoughts through structured conversation — NOT to fabricate content for them.
|
||||
|
||||
2. **External materials must be user-selected**. AI does not decide what is relevant. The user does.
|
||||
|
||||
3. **AI synthesis must be user-validated**. AI can organize and connect, but the user must approve the connections.
|
||||
|
||||
4. **AI invention must be labeled**. Anything AI creates that is not grounded in user source material must be clearly marked as draft and requires user approval.
|
||||
|
||||
5. **The output must be speakable/deliverable**. If the user cannot confidently say it in their own voice, it is not done.
|
||||
@@ -1,108 +0,0 @@
|
||||
# Narrative Design Guide (ABCDEFG Model)
|
||||
|
||||
## A - Attention (Hook)
|
||||
|
||||
**Goal**: Capture attention in the first 30 seconds.
|
||||
|
||||
**Techniques**:
|
||||
- **Contrarian statement**: "Everything you know about X is wrong"
|
||||
- **Surprising statistic**: "90% of teams fail at this"
|
||||
- **Personal story**: "Three years ago, I made a $2M mistake..."
|
||||
- **Demo/Visual**: Show, don't tell
|
||||
- **Question**: "What if I told you..."
|
||||
|
||||
**Slide design**: Big, bold, minimal text. One visual element that demands attention.
|
||||
|
||||
## B - Benefit (Promise)
|
||||
|
||||
**Goal**: Let the audience know what's in it for them.
|
||||
|
||||
**Techniques**:
|
||||
- Clear statement of takeaway
|
||||
- Time-bound promise ("In 30 minutes, you'll learn...")
|
||||
- Audience-specific benefit ("As engineers, this means...")
|
||||
|
||||
**Slide design**: Headline + 3 bullet points of promised outcomes.
|
||||
|
||||
## C - Credibility (Why Trust You?)
|
||||
|
||||
**Goal**: Establish authority without boasting.
|
||||
|
||||
**Techniques**:
|
||||
- Relevant experience (not laundry list)
|
||||
- Social proof (logos, numbers)
|
||||
- Vulnerability ("I failed at this 3 times before getting it right")
|
||||
|
||||
**Slide design**: Logos, numbers, or a brief bio. Keep it humble.
|
||||
|
||||
## D - Difference (Novel Angle)
|
||||
|
||||
**Goal**: Show what's different about YOUR perspective.
|
||||
|
||||
**Techniques**:
|
||||
- Reframe common wisdom
|
||||
- Expose hidden assumptions
|
||||
- Bridge two unlikely domains
|
||||
|
||||
**Slide design**: Comparison (old way vs new way), or metaphor illustration.
|
||||
|
||||
## E - Evidence (Proof)
|
||||
|
||||
**Goal**: Back up claims with data, demos, or stories.
|
||||
|
||||
**Techniques**:
|
||||
- Case studies
|
||||
- Live demos
|
||||
- Data visualizations
|
||||
- Customer quotes
|
||||
|
||||
**Slide design**: Charts, screenshots, or story panels.
|
||||
|
||||
## F - Framework (Mental Model)
|
||||
|
||||
**Goal**: Give the audience a reusable mental model.
|
||||
|
||||
**Techniques**:
|
||||
- 3-part framework
|
||||
- Matrix/quadrant
|
||||
- Process flow
|
||||
- Mnemonic
|
||||
|
||||
**Slide design**: Diagram, flowchart, or structured list.
|
||||
|
||||
## G - Go (Call to Action)
|
||||
|
||||
**Goal**: Tell them exactly what to do next.
|
||||
|
||||
**Techniques**:
|
||||
- Specific next step (not "think about it")
|
||||
- Low friction ("Scan this QR code")
|
||||
- Time-bound ("Join before Friday")
|
||||
|
||||
**Slide design**: Clear CTA, contact info, or QR code.
|
||||
|
||||
---
|
||||
|
||||
## Narrative Arc Patterns
|
||||
|
||||
### Pattern 1: Problem → Solution → Proof
|
||||
Best for: Product pitches, tool introductions
|
||||
|
||||
### Pattern 2: What → So What → Now What
|
||||
Best for: Educational talks, research presentations
|
||||
|
||||
### Pattern 3: Story → Lesson → Application
|
||||
Best for: Keynotes, inspirational talks
|
||||
|
||||
### Pattern 4: Myth → Truth → Practice
|
||||
Best for: Debunking talks, contrarian content
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **The Wikipedia talk**: Listing facts without a through-line
|
||||
2. **The feature dump**: Showing everything instead of the best thing
|
||||
3. **The no-arc**: Flat energy from start to finish
|
||||
4. **The surprise ending**: No setup for the conclusion
|
||||
5. **The too-many-things**: Trying to teach 5 topics in 20 minutes
|
||||
@@ -1,53 +0,0 @@
|
||||
Create a presentation slide image following these guidelines:
|
||||
|
||||
## Image Specifications
|
||||
|
||||
- **Type**: Presentation slide
|
||||
- **Aspect Ratio**: 16:9 (landscape)
|
||||
- **Style**: {{style_name}}
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Flat cartoon style throughout - NO realistic or photographic elements
|
||||
- NO slide numbers, page numbers, footers, headers, or logos
|
||||
- Clean, uncluttered layouts with clear visual hierarchy
|
||||
- Each slide conveys ONE clear message
|
||||
|
||||
## Text Style (CRITICAL)
|
||||
|
||||
- **ALL text MUST match the designated style exactly**
|
||||
- Title text: Large, bold, immediately readable
|
||||
- Body text: Clear, legible, appropriate sizing
|
||||
- Max 3-4 text elements per slide
|
||||
|
||||
## Language
|
||||
|
||||
- All text in {{language}}
|
||||
- Write in direct, confident language
|
||||
|
||||
---
|
||||
|
||||
<STYLE_INSTRUCTIONS>
|
||||
{{style_instructions}}
|
||||
</STYLE_INSTRUCTIONS>
|
||||
|
||||
---
|
||||
|
||||
## SLIDE CONTENT
|
||||
|
||||
**Slide {{slide_number}} of {{total_slides}} — Quote Focus**
|
||||
**Filename**: {{slide_number}}-slide-{{name}}.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
{{narrative_goal}}
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: {{headline}}
|
||||
Quote: {{quote_text}}
|
||||
Attribution: {{attribution}} (optional)
|
||||
|
||||
// VISUAL
|
||||
{{visual_description}}
|
||||
|
||||
// LAYOUT
|
||||
Layout: quote-focus. Headline at top. Large centered quote text (largest text on slide). Attribution below quote in smaller text. Optional decorative quotation mark graphic. Clean, minimal, high impact.
|
||||
@@ -1,53 +0,0 @@
|
||||
Create a presentation slide image following these guidelines:
|
||||
|
||||
## Image Specifications
|
||||
|
||||
- **Type**: Presentation slide
|
||||
- **Aspect Ratio**: 16:9 (landscape)
|
||||
- **Style**: {{style_name}}
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Flat cartoon style throughout - NO realistic or photographic elements
|
||||
- NO slide numbers, page numbers, footers, headers, or logos
|
||||
- Clean, uncluttered layouts with clear visual hierarchy
|
||||
- Each slide conveys ONE clear message
|
||||
|
||||
## Text Style (CRITICAL)
|
||||
|
||||
- **ALL text MUST match the designated style exactly**
|
||||
- Title text: Large, bold, immediately readable
|
||||
- Body text: Clear, legible, appropriate sizing
|
||||
- Max 3-4 text elements per slide
|
||||
|
||||
## Language
|
||||
|
||||
- All text in {{language}}
|
||||
- Write in direct, confident language
|
||||
|
||||
---
|
||||
|
||||
<STYLE_INSTRUCTIONS>
|
||||
{{style_instructions}}
|
||||
</STYLE_INSTRUCTIONS>
|
||||
|
||||
---
|
||||
|
||||
## SLIDE CONTENT
|
||||
|
||||
**Slide {{slide_number}} of {{total_slides}} — Comparison**
|
||||
**Filename**: {{slide_number}}-slide-{{name}}.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
{{narrative_goal}}
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: {{headline}}
|
||||
Left Label: {{left_label}}
|
||||
Right Label: {{right_label}}
|
||||
|
||||
// VISUAL
|
||||
{{visual_description}}
|
||||
|
||||
// LAYOUT
|
||||
Layout: split-comparison. Headline at top. Two columns side by side at center (50/50 split). Clear visual divider between sides. Labels below each column. Optional summary banner at bottom 20%.
|
||||
@@ -1,54 +0,0 @@
|
||||
Create a presentation slide image following these guidelines:
|
||||
|
||||
## Image Specifications
|
||||
|
||||
- **Type**: Presentation slide
|
||||
- **Aspect Ratio**: 16:9 (landscape)
|
||||
- **Style**: {{style_name}}
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Flat cartoon style throughout - NO realistic or photographic elements
|
||||
- NO slide numbers, page numbers, footers, headers, or logos
|
||||
- Clean, uncluttered layouts with clear visual hierarchy
|
||||
- Each slide conveys ONE clear message
|
||||
|
||||
## Text Style (CRITICAL)
|
||||
|
||||
- **ALL text MUST match the designated style exactly**
|
||||
- Title text: Large, bold, immediately readable
|
||||
- Body text: Clear, legible, appropriate sizing
|
||||
- Max 3-4 text elements per slide
|
||||
|
||||
## Language
|
||||
|
||||
- All text in {{language}}
|
||||
- Write in direct, confident language
|
||||
|
||||
---
|
||||
|
||||
<STYLE_INSTRUCTIONS>
|
||||
{{style_instructions}}
|
||||
</STYLE_INSTRUCTIONS>
|
||||
|
||||
---
|
||||
|
||||
## SLIDE CONTENT
|
||||
|
||||
**Slide {{slide_number}} of {{total_slides}} — Three Columns**
|
||||
**Filename**: {{slide_number}}-slide-{{name}}.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
{{narrative_goal}}
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: {{headline}}
|
||||
Column 1: {{column_1_title}} - {{column_1_content}}
|
||||
Column 2: {{column_2_title}} - {{column_2_content}}
|
||||
Column 3: {{column_3_title}} - {{column_3_content}}
|
||||
|
||||
// VISUAL
|
||||
{{visual_description}}
|
||||
|
||||
// LAYOUT
|
||||
Layout: three-column. Headline at top. Three equal columns (33/33/33) side by side. Each column has a distinct accent color. Generous spacing between columns. Optional connecting element (arrow, flow) between columns.
|
||||
@@ -1,52 +0,0 @@
|
||||
Create a presentation slide image following these guidelines:
|
||||
|
||||
## Image Specifications
|
||||
|
||||
- **Type**: Presentation slide
|
||||
- **Aspect Ratio**: 16:9 (landscape)
|
||||
- **Style**: {{style_name}}
|
||||
|
||||
## Core Principles
|
||||
|
||||
- Flat cartoon style throughout - NO realistic or photographic elements
|
||||
- NO slide numbers, page numbers, footers, headers, or logos
|
||||
- Clean, uncluttered layouts with clear visual hierarchy
|
||||
- Each slide conveys ONE clear message
|
||||
|
||||
## Text Style (CRITICAL)
|
||||
|
||||
- **ALL text MUST match the designated style exactly**
|
||||
- Title text: Large, bold, immediately readable
|
||||
- Body text: Clear, legible, appropriate sizing
|
||||
- Max 3-4 text elements per slide
|
||||
|
||||
## Language
|
||||
|
||||
- All text in {{language}}
|
||||
- Write in direct, confident language
|
||||
|
||||
---
|
||||
|
||||
<STYLE_INSTRUCTIONS>
|
||||
{{style_instructions}}
|
||||
</STYLE_INSTRUCTIONS>
|
||||
|
||||
---
|
||||
|
||||
## SLIDE CONTENT
|
||||
|
||||
**Slide {{slide_number}} of {{total_slides}} — Cover**
|
||||
**Filename**: {{slide_number}}-slide-cover.png
|
||||
|
||||
// NARRATIVE GOAL
|
||||
{{narrative_goal}}
|
||||
|
||||
// KEY CONTENT
|
||||
Headline: {{headline}}
|
||||
Sub-headline: {{sub_headline}}
|
||||
|
||||
// VISUAL
|
||||
{{visual_description}}
|
||||
|
||||
// LAYOUT
|
||||
Layout: title-hero. Headline centered at top. Sub-headline below. Hero visual composition centered in the lower 60% of the slide.
|
||||
@@ -1,65 +0,0 @@
|
||||
# Style Gallery
|
||||
|
||||
## flat-cartoon-infographic
|
||||
|
||||
**Best for**: Tech explainers, approachable content, WeChat-style infographics
|
||||
|
||||
**Tokens**:
|
||||
- White background (#FFFFFF)
|
||||
- Flat cartoon illustrations (simple rounded shapes)
|
||||
- Colorful pill/tag shapes for lists
|
||||
- Bold color contrasts
|
||||
- 2-3 key points per slide
|
||||
|
||||
**Example**: "龙虾 vs Claude Code" deck
|
||||
|
||||
## corporate-clean
|
||||
|
||||
**Best for**: Investor decks, B2B sales, executive briefings
|
||||
|
||||
**Tokens**:
|
||||
- Minimal design
|
||||
- Geometric shapes
|
||||
- Muted palette (blues, grays)
|
||||
- Lots of whitespace
|
||||
- Data-forward
|
||||
|
||||
## chalkboard-edu
|
||||
|
||||
**Best for**: Tutorials, workshops, educational content
|
||||
|
||||
**Tokens**:
|
||||
- Dark background (blackboard)
|
||||
- Handwritten-style typography
|
||||
- Warm tones (yellows, oranges)
|
||||
- Sketch/doodle aesthetic
|
||||
|
||||
## bold-editorial
|
||||
|
||||
**Best for**: Keynotes, product launches, creative presentations
|
||||
|
||||
**Tokens**:
|
||||
- High contrast
|
||||
- Editorial typography (serif headlines)
|
||||
- Full-bleed images
|
||||
- Magazine-like layouts
|
||||
|
||||
## scientific
|
||||
|
||||
**Best for**: Research presentations, academic talks, technical deep-dives
|
||||
|
||||
**Tokens**:
|
||||
- Clean, structured layouts
|
||||
- Cool color palette (blues, teals)
|
||||
- Data visualizations
|
||||
- Citation-ready
|
||||
|
||||
## notion
|
||||
|
||||
**Best for**: Product demos, SaaS presentations, tool tutorials
|
||||
|
||||
**Tokens**:
|
||||
- Clean, neutral colors
|
||||
- Geometric icons
|
||||
- Dense but organized information
|
||||
- Modern SaaS aesthetic
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""Archive current slide version to _archive/v{N}/."""
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_next_version(archive_dir: Path) -> int:
|
||||
"""Find the next version number (v1, v2, ...)."""
|
||||
if not archive_dir.exists():
|
||||
return 1
|
||||
versions = []
|
||||
for d in archive_dir.iterdir():
|
||||
if d.is_dir() and d.name.startswith("v"):
|
||||
try:
|
||||
versions.append(int(d.name[1:]))
|
||||
except ValueError:
|
||||
pass
|
||||
return max(versions, default=0) + 1
|
||||
|
||||
|
||||
def archive_version(project_dir: str):
|
||||
"""Archive 02-slides/ and 03-prompts/ to _archive/v{N}/."""
|
||||
project_path = Path(project_dir)
|
||||
slides_dir = project_path / "02-slides"
|
||||
prompts_dir = project_path / "03-prompts"
|
||||
|
||||
if not slides_dir.exists():
|
||||
print(f"No slides to archive: {slides_dir}")
|
||||
return False
|
||||
|
||||
archive_dir = project_path / "_archive"
|
||||
next_ver = get_next_version(archive_dir)
|
||||
target_dir = archive_dir / f"v{next_ver}"
|
||||
|
||||
print(f"Archiving to: {target_dir}")
|
||||
target_dir.mkdir(parents=True, exist_ok=False)
|
||||
|
||||
# Copy slides
|
||||
target_slides = target_dir / "02-slides"
|
||||
shutil.copytree(slides_dir, target_slides)
|
||||
print(f" Copied slides: {len(list(target_slides.glob('*.png')))} files")
|
||||
|
||||
# Copy prompts if exist
|
||||
if prompts_dir.exists():
|
||||
target_prompts = target_dir / "03-prompts"
|
||||
shutil.copytree(prompts_dir, target_prompts)
|
||||
prompt_count = sum(1 for _ in target_prompts.rglob("*.md"))
|
||||
print(f" Copied prompts: {prompt_count} files")
|
||||
|
||||
print(f"Archive v{next_ver} complete.")
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Archive slide version")
|
||||
parser.add_argument("--project", required=True, help="Project directory")
|
||||
args = parser.parse_args()
|
||||
|
||||
archive_version(args.project)
|
||||
@@ -1,120 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = []
|
||||
# ///
|
||||
"""Extract structured speaker notes from slide prompt files."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_prompt_file(slide_name: str, prompts_dir: Path) -> Path | None:
|
||||
"""Find matching prompt .md for a slide name."""
|
||||
# Try flat structure first: 03-prompts/01-slide-cover.md
|
||||
flat = prompts_dir / f"{slide_name}.md"
|
||||
if flat.exists():
|
||||
return flat
|
||||
|
||||
# Try versioned subdirectories: 03-prompts/v6/01-slide-cover.md
|
||||
for subdir in prompts_dir.iterdir():
|
||||
if subdir.is_dir():
|
||||
candidate = subdir / f"{slide_name}.md"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_notes(prompt_text: str) -> dict:
|
||||
"""Extract NARRATIVE GOAL and SPEAKER NOTES sections from prompt."""
|
||||
result = {}
|
||||
|
||||
ng_match = re.search(
|
||||
r"// NARRATIVE GOAL\s*\n(.+?)(?=\n// |\n## |\Z)",
|
||||
prompt_text,
|
||||
re.DOTALL,
|
||||
)
|
||||
if ng_match:
|
||||
result["narrative_goal"] = ng_match.group(1).strip()
|
||||
|
||||
sn_match = re.search(
|
||||
r"// SPEAKER NOTES\s*\n(.+?)(?=\n// |\n## |\Z)",
|
||||
prompt_text,
|
||||
re.DOTALL,
|
||||
)
|
||||
if sn_match:
|
||||
result["speaker_notes"] = sn_match.group(1).strip()
|
||||
|
||||
# Fallback: KEY CONTENT
|
||||
if not result:
|
||||
kc_match = re.search(
|
||||
r"// KEY CONTENT\s*\n(.+?)(?=\n// |\n## |\Z)",
|
||||
prompt_text,
|
||||
re.DOTALL,
|
||||
)
|
||||
if kc_match:
|
||||
result["key_content"] = kc_match.group(1).strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def extract_all_notes(prompts_dir: str, output: str | None = None) -> str:
|
||||
"""Extract notes from all prompt files and return as markdown."""
|
||||
prompts_path = Path(prompts_dir)
|
||||
if not prompts_path.exists():
|
||||
raise FileNotFoundError(f"Prompts directory not found: {prompts_dir}")
|
||||
|
||||
# Find all slide prompt files
|
||||
prompt_files = []
|
||||
for path in prompts_path.rglob("*.md"):
|
||||
# Only match files like 01-slide-xxx.md (not README etc.)
|
||||
if re.match(r"\d+-slide-", path.name):
|
||||
prompt_files.append(path)
|
||||
|
||||
# Sort by slide number extracted from filename
|
||||
def sort_key(p: Path) -> int:
|
||||
match = re.match(r"(\d+)-slide-", p.name)
|
||||
return int(match.group(1)) if match else 999
|
||||
|
||||
prompt_files.sort(key=sort_key)
|
||||
|
||||
lines = ["# Speaker Notes\n"]
|
||||
matched = 0
|
||||
|
||||
for prompt_file in prompt_files:
|
||||
slide_name = prompt_file.stem # e.g. "01-slide-cover"
|
||||
prompt_text = prompt_file.read_text(encoding="utf-8")
|
||||
notes = extract_notes(prompt_text)
|
||||
|
||||
if notes:
|
||||
matched += 1
|
||||
lines.append(f"## {slide_name}")
|
||||
if "narrative_goal" in notes:
|
||||
lines.append(f"**Narrative Goal**: {notes['narrative_goal']}")
|
||||
if "speaker_notes" in notes:
|
||||
lines.append(f"**Speaker Notes**: {notes['speaker_notes']}")
|
||||
if "key_content" in notes:
|
||||
lines.append(f"**Key Content**: {notes['key_content']}")
|
||||
lines.append("")
|
||||
|
||||
result = "\n".join(lines)
|
||||
|
||||
if output:
|
||||
output_path = Path(output)
|
||||
output_path.write_text(result, encoding="utf-8")
|
||||
print(f"Wrote speaker notes to: {output}")
|
||||
print(f"Prompts scanned: {len(prompt_files)}")
|
||||
print(f"Slides with notes: {matched}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Extract speaker notes from prompts")
|
||||
parser.add_argument("--prompts", required=True, help="Directory containing per-slide prompt .md files")
|
||||
parser.add_argument("--output", help="Output markdown file path (default: print to stdout)")
|
||||
args = parser.parse_args()
|
||||
|
||||
extract_all_notes(args.prompts, args.output)
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Post-processing: assemble deliverables from generated slides
|
||||
*
|
||||
* After baoyu-slide-deck generates PNGs, this script:
|
||||
* 1. Validates the slide sequence (aspect ratio, naming, gaps)
|
||||
* 2. Extracts structured speaker notes from 03-prompts/
|
||||
* 3. Generates PDF + PPTX in 01-成品/
|
||||
*
|
||||
* This does NOT generate images — image generation is delegated to baoyu-slide-deck.
|
||||
* Post-processing handles: validation, notes extraction, and assembly into deliverables.
|
||||
*
|
||||
* Usage:
|
||||
* bun main.ts --project /path/to/slide-deck/project-name
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
import { existsSync } from "fs";
|
||||
import { resolve, basename } from "path";
|
||||
|
||||
interface Args {
|
||||
projectDir: string;
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
const idx = process.argv.indexOf("--project");
|
||||
if (idx === -1 || !process.argv[idx + 1]) {
|
||||
console.error("Usage: bun main.ts --project /path/to/slide-deck/project-name");
|
||||
process.exit(1);
|
||||
}
|
||||
return { projectDir: resolve(process.argv[idx + 1]) };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { projectDir } = parseArgs();
|
||||
const projectName = basename(projectDir);
|
||||
|
||||
const slidesDir = `${projectDir}/02-slides`;
|
||||
const outputDir = `${projectDir}/01-成品`;
|
||||
|
||||
if (!existsSync(slidesDir)) {
|
||||
console.error(`Slides directory not found: ${slidesDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(outputDir)) {
|
||||
console.log(`Creating output directory: ${outputDir}`);
|
||||
await $`mkdir -p ${outputDir}`;
|
||||
}
|
||||
|
||||
// Validate slides
|
||||
console.log("Validating slides...");
|
||||
const { stdout } = await $`uv run ${import.meta.dir}/validate_slides.py --slides ${slidesDir}`;
|
||||
const result = JSON.parse(stdout.toString());
|
||||
|
||||
if (result.status === "FAILED") {
|
||||
console.error("Validation failed:", result.issues);
|
||||
process.exit(1);
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
console.warn("Warnings:", result.warnings);
|
||||
}
|
||||
console.log(`Validation passed: ${result.slide_count} slides`);
|
||||
|
||||
// Extract speaker notes
|
||||
const promptsDir = `${projectDir}/03-prompts`;
|
||||
const notesPath = `${projectDir}/speaker-notes.md`;
|
||||
if (existsSync(promptsDir)) {
|
||||
console.log(`Extracting speaker notes: ${notesPath}`);
|
||||
await $`uv run ${import.meta.dir}/extract_notes.py --prompts ${promptsDir} --output ${notesPath}`;
|
||||
}
|
||||
|
||||
// Generate PDF
|
||||
const pdfPath = `${outputDir}/${projectName}.pdf`;
|
||||
console.log(`Generating PDF: ${pdfPath}`);
|
||||
await $`uv run ${import.meta.dir}/merge_to_pdf.py --slides ${slidesDir} --output ${pdfPath}`;
|
||||
|
||||
// Generate PPTX (with speaker notes from 03-prompts/)
|
||||
const pptxPath = `${outputDir}/${projectName}.pptx`;
|
||||
console.log(`Generating PPTX: ${pptxPath}`);
|
||||
if (existsSync(promptsDir)) {
|
||||
await $`uv run ${import.meta.dir}/merge_to_pptx.py --slides ${slidesDir} --output ${pptxPath} --prompts ${promptsDir}`;
|
||||
} else {
|
||||
await $`uv run ${import.meta.dir}/merge_to_pptx.py --slides ${slidesDir} --output ${pptxPath}`;
|
||||
}
|
||||
|
||||
console.log("\nPost-processing complete!");
|
||||
console.log(`Deliverables in ${outputDir}:`);
|
||||
console.log(` - ${projectName}.pdf`);
|
||||
console.log(` - ${projectName}.pptx`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["Pillow"]
|
||||
# ///
|
||||
"""Merge slide PNGs into a single PDF."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def parse_slide_number(filename: str) -> int | None:
|
||||
"""Extract slide number from filename like '01-slide-cover.png'."""
|
||||
match = re.match(r"(\d+)-slide-", filename)
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def merge_to_pdf(slides_dir: str, output: str):
|
||||
"""Merge PNG slides into PDF."""
|
||||
slides_path = Path(slides_dir)
|
||||
png_files = sorted(
|
||||
[f for f in slides_path.glob("*.png") if parse_slide_number(f.name) is not None],
|
||||
key=lambda f: parse_slide_number(f.name) or 0,
|
||||
)
|
||||
|
||||
if not png_files:
|
||||
print(f"No PNG files found in {slides_dir}")
|
||||
return False
|
||||
|
||||
images = []
|
||||
for png in png_files:
|
||||
img = Image.open(png)
|
||||
if img.mode == 'RGBA':
|
||||
# Convert RGBA to RGB for PDF compatibility
|
||||
background = Image.new('RGB', img.size, (255, 255, 255))
|
||||
background.paste(img, mask=img.split()[3])
|
||||
img = background
|
||||
images.append(img)
|
||||
|
||||
# Save first image, append rest
|
||||
first_image = images[0]
|
||||
rest_images = images[1:] if len(images) > 1 else []
|
||||
|
||||
first_image.save(
|
||||
output,
|
||||
save_all=True,
|
||||
append_images=rest_images,
|
||||
resolution=150.0,
|
||||
)
|
||||
|
||||
print(f"Created PDF: {output}")
|
||||
print(f"Slides included: {len(images)}")
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Merge slide PNGs to PDF")
|
||||
parser.add_argument("--slides", required=True, help="Directory containing PNG slides")
|
||||
parser.add_argument("--output", required=True, help="Output PDF path")
|
||||
args = parser.parse_args()
|
||||
|
||||
merge_to_pdf(args.slides, args.output)
|
||||
@@ -1,146 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["python-pptx", "Pillow"]
|
||||
# ///
|
||||
"""Merge slide PNGs into a PowerPoint file with optional speaker notes."""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches
|
||||
|
||||
|
||||
EMUS_PER_INCH = 914400
|
||||
|
||||
|
||||
def parse_slide_number(filename: str) -> int | None:
|
||||
"""Extract slide number from filename like '01-slide-cover.png'."""
|
||||
match = re.match(r"(\d+)-slide-", filename)
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def find_prompt_file(slide_name: str, prompts_dir: Path) -> Path | None:
|
||||
"""Find matching prompt .md for a slide name."""
|
||||
# Try flat structure first: 03-prompts/01-slide-cover.md
|
||||
flat = prompts_dir / f"{slide_name}.md"
|
||||
if flat.exists():
|
||||
return flat
|
||||
|
||||
# Try versioned subdirectories: 03-prompts/v6/01-slide-cover.md
|
||||
for subdir in prompts_dir.iterdir():
|
||||
if subdir.is_dir():
|
||||
candidate = subdir / f"{slide_name}.md"
|
||||
if candidate.exists():
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_notes(prompt_text: str) -> str:
|
||||
"""Extract NARRATIVE GOAL and SPEAKER NOTES sections from prompt."""
|
||||
notes_parts = []
|
||||
|
||||
# Extract // NARRATIVE GOAL
|
||||
ng_match = re.search(r"// NARRATIVE GOAL\s*\n(.+?)(?=\n// |\n## |\Z)", prompt_text, re.DOTALL)
|
||||
if ng_match:
|
||||
notes_parts.append(f"NARRATIVE GOAL:\n{ng_match.group(1).strip()}")
|
||||
|
||||
# Extract // SPEAKER NOTES
|
||||
sn_match = re.search(r"// SPEAKER NOTES\s*\n(.+?)(?=\n// |\n## |\Z)", prompt_text, re.DOTALL)
|
||||
if sn_match:
|
||||
notes_parts.append(f"SPEAKER NOTES:\n{sn_match.group(1).strip()}")
|
||||
|
||||
# If neither section found but there's KEY CONTENT, use that as fallback
|
||||
if not notes_parts:
|
||||
kc_match = re.search(r"// KEY CONTENT\s*\n(.+?)(?=\n// |\n## |\Z)", prompt_text, re.DOTALL)
|
||||
if kc_match:
|
||||
notes_parts.append(f"KEY CONTENT:\n{kc_match.group(1).strip()}")
|
||||
|
||||
return "\n\n".join(notes_parts) if notes_parts else ""
|
||||
|
||||
|
||||
def get_image_aspect_ratio(image_path: Path) -> float:
|
||||
"""Return width / height ratio of an image."""
|
||||
with Image.open(image_path) as img:
|
||||
return img.width / img.height
|
||||
|
||||
|
||||
def merge_to_pptx(
|
||||
slides_dir: str,
|
||||
output: str,
|
||||
prompts_dir: str | None = None,
|
||||
title: str | None = None,
|
||||
):
|
||||
"""Merge PNG slides into PowerPoint with speaker notes."""
|
||||
slides_path = Path(slides_dir)
|
||||
png_files = sorted(
|
||||
[f for f in slides_path.glob("*.png") if parse_slide_number(f.name) is not None],
|
||||
key=lambda f: parse_slide_number(f.name) or 0,
|
||||
)
|
||||
|
||||
if not png_files:
|
||||
print(f"No PNG files found in {slides_dir}")
|
||||
return False
|
||||
|
||||
# Auto-detect prompts directory
|
||||
if prompts_dir is None:
|
||||
project_root = slides_path.parent
|
||||
auto_prompts = project_root / "03-prompts"
|
||||
if auto_prompts.exists():
|
||||
prompts_dir = str(auto_prompts)
|
||||
prompts_path = Path(prompts_dir) if prompts_dir else None
|
||||
|
||||
# Dynamic slide size from first image
|
||||
first_ratio = get_image_aspect_ratio(png_files[0])
|
||||
# Standard 16:9 is 13.333 x 7.5 inches; scale height to match ratio
|
||||
target_height = Inches(7.5)
|
||||
target_width = Inches(7.5 * first_ratio)
|
||||
|
||||
prs = Presentation()
|
||||
prs.slide_width = int(target_width)
|
||||
prs.slide_height = int(target_height)
|
||||
|
||||
notes_count = 0
|
||||
for png in png_files:
|
||||
slide_layout = prs.slide_layouts[6] # blank layout
|
||||
slide = prs.slides.add_slide(slide_layout)
|
||||
slide.shapes.add_picture(
|
||||
str(png),
|
||||
Inches(0),
|
||||
Inches(0),
|
||||
width=prs.slide_width,
|
||||
height=prs.slide_height,
|
||||
)
|
||||
|
||||
# Add speaker notes if prompt file found
|
||||
slide_name = png.stem # e.g. "01-slide-cover"
|
||||
if prompts_path:
|
||||
prompt_file = find_prompt_file(slide_name, prompts_path)
|
||||
if prompt_file:
|
||||
prompt_text = prompt_file.read_text(encoding="utf-8")
|
||||
notes = extract_notes(prompt_text)
|
||||
if notes:
|
||||
notes_slide = slide.notes_slide
|
||||
notes_slide.notes_text_frame.text = notes
|
||||
notes_count += 1
|
||||
|
||||
prs.save(output)
|
||||
print(f"Created PPTX: {output}")
|
||||
print(f"Slides included: {len(png_files)}")
|
||||
if prompts_path:
|
||||
print(f"Slides with notes: {notes_count}")
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Merge slide PNGs to PPTX")
|
||||
parser.add_argument("--slides", required=True, help="Directory containing PNG slides")
|
||||
parser.add_argument("--output", required=True, help="Output PPTX path")
|
||||
parser.add_argument("--prompts", help="Directory containing per-slide prompt .md files (default: 03-prompts/ relative to slides parent)")
|
||||
parser.add_argument("--title", help="Presentation title")
|
||||
args = parser.parse_args()
|
||||
|
||||
merge_to_pptx(args.slides, args.output, args.prompts, args.title)
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = ["Pillow"]
|
||||
# ///
|
||||
"""Validate slide deck consistency."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def validate_slides(slides_dir: str, expected_count: int = None):
|
||||
"""Validate slide deck for consistency issues."""
|
||||
slides_path = Path(slides_dir)
|
||||
png_files = sorted(slides_path.glob("*.png"))
|
||||
|
||||
issues = []
|
||||
warnings = []
|
||||
info = []
|
||||
|
||||
if not png_files:
|
||||
issues.append(f"No PNG files found in {slides_dir}")
|
||||
return {"status": "FAILED", "issues": issues, "warnings": warnings, "info": info}
|
||||
|
||||
info.append(f"Found {len(png_files)} slides")
|
||||
|
||||
# Check expected count
|
||||
if expected_count and len(png_files) != expected_count:
|
||||
warnings.append(f"Expected {expected_count} slides, found {len(png_files)}")
|
||||
|
||||
# Check aspect ratios
|
||||
aspect_ratios = {}
|
||||
for png in png_files:
|
||||
img = Image.open(png)
|
||||
ratio = round(img.width / img.height, 2)
|
||||
aspect_ratios[ratio] = aspect_ratios.get(ratio, []) + [png.name]
|
||||
|
||||
if len(aspect_ratios) > 1:
|
||||
issues.append(f"Inconsistent aspect ratios: {aspect_ratios}")
|
||||
else:
|
||||
ratio = list(aspect_ratios.keys())[0]
|
||||
info.append(f"Consistent aspect ratio: {ratio} (16:9 ≈ 1.78)")
|
||||
if abs(ratio - 1.78) > 0.05:
|
||||
warnings.append(f"Aspect ratio {ratio} deviates from standard 16:9 (1.78)")
|
||||
|
||||
# Check naming convention
|
||||
expected_pattern = True
|
||||
for i, png in enumerate(png_files, 1):
|
||||
expected_prefix = f"{i:02d}-slide-"
|
||||
if not png.name.startswith(expected_prefix):
|
||||
warnings.append(f"Naming convention: {png.name} doesn't start with {expected_prefix}")
|
||||
expected_pattern = False
|
||||
|
||||
if expected_pattern:
|
||||
info.append("Naming convention: OK")
|
||||
|
||||
# Check for gaps in numbering
|
||||
numbers = []
|
||||
for png in png_files:
|
||||
try:
|
||||
num = int(png.name.split("-")[0])
|
||||
numbers.append(num)
|
||||
except ValueError:
|
||||
warnings.append(f"Cannot extract number from {png.name}")
|
||||
|
||||
if numbers:
|
||||
expected_set = set(range(min(numbers), max(numbers) + 1))
|
||||
actual_set = set(numbers)
|
||||
gaps = expected_set - actual_set
|
||||
if gaps:
|
||||
warnings.append(f"Missing slide numbers: {sorted(gaps)}")
|
||||
|
||||
# File size check
|
||||
oversized = [png for png in png_files if png.stat().st_size > 10 * 1024 * 1024] # 10MB
|
||||
if oversized:
|
||||
warnings.append(f"Oversized files (>10MB): {[p.name for p in oversized]}")
|
||||
|
||||
status = "FAILED" if issues else "PASSED" if not warnings else "PASSED_WITH_WARNINGS"
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"slide_count": len(png_files),
|
||||
"issues": issues,
|
||||
"warnings": warnings,
|
||||
"info": info
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Validate slide deck")
|
||||
parser.add_argument("--slides", required=True, help="Directory containing PNG slides")
|
||||
parser.add_argument("--expected-count", type=int, help="Expected number of slides")
|
||||
args = parser.parse_args()
|
||||
|
||||
result = validate_slides(args.slides, args.expected_count)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
Reference in New Issue
Block a user