
Present Paper
- 45 installs
- 236 repo stars
- Updated August 3, 2026
- aperivue/medsci-skills
Present-paper is a Claude Code skill that prepares academic presentations and PPTX speaker notes from a research paper through a 5-phase pipeline.
About
Present-paper is a Claude skill that prepares academic presentations from a research paper. It runs a five-phase pipeline: analyze the source paper, find supporting references, draft an audience-adapted speaker script, generate or augment a PPTX with speaker notes, and prepare Q&A. It supports journal club, grand rounds, seminar, conference, and lecture formats.
- 5-phase pipeline: paper analysis, supporting research, script writing, slide notes, Q&A prep
- Generates or augments PPTX with speaker notes
- Design references from Reynolds, Duarte, Knaflic, and Tufte
Present Paper by the numbers
- 45 all-time installs (skills.sh)
- Ranked #377 of 688 Office & Documents skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
present-paper capabilities & compatibility
- Capabilities
- make figures · meta analysis · manage refs
- Use cases
- presentations · research · documentation
What present-paper says it does
generates or augments PPTX with speaker notes, and prepares Q&A.
The skill walks through a 5-phase pipeline: paper analysis, supporting research, script writing, slide note injection, and Q&A preparation.
npx skills add https://github.com/aperivue/medsci-skills --skill present-paperAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 45 |
|---|---|
| repo stars | ★ 236 |
| Last updated | August 3, 2026 |
| Repository | aperivue/medsci-skills ↗ |
What it does
A researcher uses it to build a journal-club or grand-rounds slide deck with speaker notes and Q&A prep from a paper.
Who is it for?
Building a journal club, grand rounds, seminar, or lecture deck from a paper.
Skip if: Generating the manuscript figures (make-figures) or writing the paper itself (write-paper).
When should I use this skill?
You are preparing a journal club, grand rounds, seminar, conference talk, or lecture from a paper.
What you get
A slide-by-slide outline, an audience-adapted speaker script, a PPTX with speaker notes, and Q&A preparation.
- Slide outline
- Speaker script
- PPTX with speaker notes
By the numbers
- 5-phase pipeline
- 5 presentation-context templates
Files
Present-Paper Skill
Purpose
Prepare a polished academic presentation from a research paper. The skill walks through a 5-phase pipeline: paper analysis, supporting research, script writing, slide note injection, and Q&A preparation.
Use it when:
- preparing a journal club or seminar presentation
- presenting a paper for a graduate course
- preparing grand rounds or conference talks based on a published paper
- building speaker notes for an existing slide deck
---
Communication Rules
- Communicate with the user in their preferred language.
- Use English for medical, statistical, and methodological terminology.
- Add pronunciation guides for drug names and technical abbreviations in the user's language.
- Be direct about paper limitations, but frame them constructively.
---
Phase 0: Init & Outline
Step 0a — Load design references (read before drafting outline)
Before collecting inputs, the skill loads three reference files:
1. `references/slide_design_principles.md` — Reynolds (Presentation Zen) + Duarte (Slide:ology Glance Test™) + Knaflic (Storytelling with Data preattentive attributes) + Tufte (Cognitive Style of PowerPoint). Defines the 5 design principles, reading-time budgets per audience, cognitive-load ceilings, and the anti-patterns this skill is built to avoid. Read this first — it shifts the outline from "what content fits" to "what should the audience remember 10 seconds after each slide." 2. `references/medical_presentation_templates.md` — Section structure, slide counts, and design seeds for the 5 contexts: journal club, grand rounds, conference talk, lecture, and academic lecture multi-paper survey. Pick the matching template after Phase 0 inputs are collected, then customize. 3. `references/slide_visual_styles/` — visual style specs (color palette, typography, layout grid, slide-type templates) callable from any of the 5 context templates. Currently available: nature_lancet.md (Nature/Lancet aesthetic — white background, navy primary, coral accent, Inter/Pretendard). Default for academic lectures per ~/.claude/rules/academic-lecture-style.md. Paired with the generic builder templates/build_pptx_nature_lancet.py and the PDF figure extractor scripts/extract_pdf_figures.py.
These two files mirror the entry-point pattern used in make-figures/references/design_principles.md (Step 1 "Specify"). Both skills share the same Reynolds / Knaflic / Tufte foundations — slide-level (this skill) and figure-level (make-figures) are companions, not duplicates.
Required Inputs
Before starting, collect these from the user:
| Input | Why |
|---|---|
| Paper | PDF path, DOI, or PMID |
| Presentation time | Determines depth and slide count |
| Target audience | Specialty mix, knowledge level — controls terminology depth |
| Context | Course name, conference, journal club format, prior session topics |
| Extension section | Optional topic to include (e.g., AI directions, clinical implications). Default: none |
Paper Analysis
Read the paper and produce a structured analysis:
## Paper Analysis
### Citation
[Full citation with DOI]
### Background
- What gap does this paper address?
- What was known vs. unknown before this study?
### Study Design
- Type: [RCT / cohort / case series / meta-analysis / etc.]
- Subjects: [n, inclusion/exclusion]
- Methods: [key methodological choices]
- Primary outcome: [what was measured]
### Key Results
1. [Finding 1 with effect size and CI/p-value]
2. [Finding 2]
3. [Finding 3]
### Patient/Case Summary Table
[If applicable — structured table of individual cases or subgroups]
### Limitations
1. [Limitation 1]
2. [Limitation 2]
### Significance
- Why does this matter?
- What changes because of this paper?Slide Outline
Create a slide-by-slide outline with time allocation:
## Slide Outline ([N] slides, [M] minutes)
| # | Title | Time | Key Content |
|---|-------|------|-------------|
| 1 | Title slide | 0:30 | Paper citation, presenter |
| 2 | Context / Prior sessions | 1:00 | How this connects to prior knowledge |
| 3 | Background | 1:30 | The gap this paper fills |
| ... | ... | ... | ... |
| N | Take-home messages | 0:30 | 3-5 key points |Gate: User approves outline before proceeding.
---
Phase 1: Supporting Research
Search Strategy
Find references that strengthen the presentation:
1. Follow-up studies — Has the main finding been replicated or extended? 2. Clinical trial data — Large-scale data that contextualizes the findings 3. Review articles — Authoritative summaries that frame the topic 4. Contradicting evidence — Important for balanced Q&A preparation
Efficiency rule: Limit supporting references to 5-8 total. Only search categories that the approved outline (Phase 0) actually requires. Skip categories not needed for the presentation type (e.g., skip clinical trials for a methods-focused paper).
Selection Criteria
Do NOT summarize every paper found. Extract only:
- Specific data points needed for slides (incidence rates, OR/HR, AUC values)
- Findings that directly support or challenge the main paper
- Context that helps the audience understand significance
Output
## Verified References
### Main Paper
1. [Citation] — PMID: XXXXX, DOI: XX.XXXX/XXXXX
### Supporting References
2. [Citation] — PMID: XXXXX
→ Used for: [specific data point or context]
3. [Citation] — PMID: XXXXX
→ Used for: [specific data point or context]
### Key Data for Slides
- [Statistic 1]: [value] — Source: [Ref #]
- [Statistic 2]: [value] — Source: [Ref #]Every reference must have a verified DOI or PMID. Mark unverified references with [UNVERIFIED].
---
Phase 2: Script & Content
Speaker Script
Draft a complete speaker script with these requirements:
1. Language: User's preferred language for narration; English for technical terms 2. Audience adaptation: Adjust explanation depth based on Phase 0 audience profile
- For mixed audiences: add one-line plain-language explanations for specialty-specific terms
- Example: "FLAIR sequence — an MRI technique that suppresses fluid signal to highlight edema"
3. Pronunciation guide: Include native-language pronunciation for drug names, abbreviations
- Example: "lecanemab (leh-KAN-eh-mab)" or local equivalent
4. Timing markers: Note approximate time per slide 5. Transition phrases: Connect each slide to the narrative arc
Structure
## Speaker Script
### Slide 1: Title (0:30)
"[Opening — introduce yourself and the paper]"
### Slide 2: Context (1:00)
"[Connect to prior knowledge or clinical relevance]"
...
### Slide N: Take-home Messages (0:30)
"[Summarize 3-5 key points. Thank audience. Invite questions.]"Extension Section (Optional)
Only include if user requested in Phase 0. Examples:
- AI/computational research directions stemming from the paper
- Clinical practice implications
- Policy or guideline implications
- Connections to the user's own research
Gate: User reviews script before proceeding.
---
Phase 3: Slides & Notes
Two Modes
Mode A: Generate new slide deck
Generate a fully-editable PPTX from structured inline data using python-pptx. Two canonical template libraries:
${CLAUDE_SKILL_DIR}/references/generate_pptx_templates.py— generic T_lead /
T_text / T_table / T_image_right / etc. templates with smoke-tested main(). Use for journal club, grand rounds, conference talk, and short paper talks.
${CLAUDE_SKILL_DIR}/templates/build_pptx_nature_lancet.py— Nature/Lancet visual
style (white + navy + coral, Inter/Pretendard, 47-slide academic lecture proven). Use for academic lecture multi-paper survey (template #5). Functions: new_presentation, add_title_slide, add_toc_slide, add_section_divider, add_transition_slide, add_content_slide, add_glossary_slide, add_closing_slide, plus fix_app_xml() helper. Style spec: references/slide_visual_styles/nature_lancet.md.
For lecture decks pulling figures from PDFs (rather than from /make-figures output), use ${CLAUDE_SKILL_DIR}/scripts/extract_pdf_figures.py — pdftoppm + PIL crop with normalized (0–1) box coordinates. Supports both single-crop CLI and YAML batch config.
After raw extraction, run ${CLAUDE_SKILL_DIR}/scripts/trim_caption.py to auto-remove journal headers / figure captions / surrounding whitespace so that only the figure body remains — the Adobe-Acrobat-crop equivalent in automation. The script uses horizontal-projection segmentation plus text-band detection (height + density + gap + line-pattern signature) and preserves multi-panel figures intact:
python3 "${CLAUDE_SKILL_DIR}/scripts/trim_caption.py" \
--in-dir figures/extracted \
--out-dir figures/croppedHandles four common journal layouts: top running-head bar, bottom multi-line caption (sparse text), bottom caption fused with figure body (no clear gap, detected via narrow dark/light alternation), and multi-row tables with footnotes (footnote cut, table rows preserved). No tesseract / OCR dependency — Pillow + numpy only. Verified on 12-figure academic deck (80–95% height retention; captions, journal banners, and CellPress-style headers all removed). When the deck slot expects only the figure body (default for build_pptx_nature_lancet.py), point FIG_DIR at the cropped output dir.
Word-boundary aware markdown parser (mandatory for HLA-rich decks)
When the build script parses inline **bold** / *italic* markers in slide body or speaker notes, the italic rule must use word-boundary lookahead / lookbehind so asterisk-bearing scientific tokens (HLA alleles like DRB1*07:01, HLA-A*02:01, SNP IDs, footnote markers) are not eaten as italic delimiters:
import re
pattern = re.compile(
r"(\*\*(?:(?!\*\*).)+?\*\*" # bold; inner single * allowed
r"|(?<![A-Za-z0-9])\*[^*\n]+?\*(?![A-Za-z0-9]))" # italic (word-boundary)
)Two regex tricks together: 1. Italic with boundary: (?<![A-Za-z0-9]) and (?![A-Za-z0-9]) reject * adjacent to alphanumerics, so DRB1*07:01 is left intact. 2. *Bold tolerates inner single `**: (?:(?!\\).)+? allows *DRB104:02**` (HLA allele inside bold) to match as a single bold span.
Without these, a naive \*[^*]+\* italic pattern silently corrupts every HLA allele in the deck. Add the regex to add_styled() (or equivalent) in every Nature/Lancet-style build script.
Pronunciation auto-augment for non-native presenters
For decks where the presenter is uncomfortable with English pronunciation of acronyms, author names, drug names, or gene symbols, append a per-slide [ Pronunciation ] section to the speaker notes (audience sees nothing — only Presenter View). Use ${CLAUDE_SKILL_DIR}/scripts/inject_pronunciation_notes.py:
python3 "${CLAUDE_SKILL_DIR}/scripts/inject_pronunciation_notes.py" \
input.pptx output.pptx \
--dict pron_dict.yaml \
--header "[ 발음 ]" # or any header you likeThe script:
- Loads a YAML/JSON
PRON_DICT(term → [reading, full_name]) supplied by
the caller. The dict is domain-specific — assemble it for your audience (Korean readings, French readings, Spanish readings, etc.).
- Uses word-boundary regex
(?<![A-Za-z0-9_]) … (?![A-Za-z0-9_])so
short acronyms (e.g. AE, OR) only match when standalone, never inside other words.
- Recognizes allele-style tokens via a separate regex
(\b(?:HLA-)?[A-Z]{1,5}[0-9]?\*[0-9]{2}:[0-9]{2}\b by default) and synthesizes their reading from the base allele entry in the dict.
- Skips slides that already contain the header (idempotent — safe to re-run).
Realistic yield on a 47-slide academic deck: ~38 slides receive a section, ~300 total term entries, 5–10 per annotated slide. Transition and divider slides have empty notes and are auto-skipped.
Speaker notes statistics density
When the slide body already shows exact OR / 95% CI / p-value, the notes should NOT repeat the same numbers — the presenter ends up reading statistics aloud and the audience cannot keep up. Notes should be a narrative (key anchors + one-line "see the slide body for the exact numbers" reminder), not a numeric listing.
Quick measurement to spot dense slides during QC:
import re
text = slide.notes_slide.notes_text_frame.text.split(pron_header)[0]
n_char = len(text)
n_stat = len(re.findall(r"\b(?:OR|p|CI)\s*[=<>]?\s*\d|\d+\.\d+|\d+%|×10", text))
needs_compression = n_char > 1000 and n_stat >= 5Rule of thumb: 700–1,000 chars + 0–2 stat tokens is fine (30–60-second narrative). >1,000 chars + ≥5 stat tokens → compress to narrative tone and point at the slide body. Exact numbers belong in the slide body and footnotes (SSOT), not the notes.
Sharing-ready notes-stripped variant
After the presentation, when the deck is shared with the audience (e.g. a professor asking for the slides), the speaker notes typically contain presenter-only material — second-language narrative, pronunciation hints, self-referential reminders ("Prof. ○○ will likely ask about …"). Stripping notes is mandatory before circulation. Use ${CLAUDE_SKILL_DIR}/scripts/strip_notes_for_sharing.py:
python3 "${CLAUDE_SKILL_DIR}/scripts/strip_notes_for_sharing.py" \
presenter_v9.pptx share/<topic>_<initials>.pptxThe script:
- Clears every slide's
notes_text_frame(idempotent, slide body and
figures untouched).
- Re-writes
docProps/app.xmlwith the correctSlides=andNotes=
counts so PowerPoint Mac does not show its repair dialog (see also the app.xml canonical fix in pptx-mac-compatibility.md §5).
- Verifies that zero notes characters remain.
Recommended 3-file sharing package (filename pattern <topic>_<initials>):
<topic>_<initials>.pptx— notes-stripped variant for slide reuse<topic>_<initials>.pdf— same deck, PDF for environment-agnostic
preview (LibreOffice --convert-to pdf automatically drops the cleared notes pages)
<topic>_<initials>_references.zip— optional bundle of the reference
PDFs; if it exceeds the email attachment limit, send a Google Drive link.
In the cover email, mention the PPTX is included specifically so the recipient can reuse individual slides if useful.
Architecture
inline structured data (lists/dicts in build_*_slides())
↓ template functions (T_lead / T_text / T_table / ...)
editable PPTX with native text frames (selectable, restyleable in PowerPoint)Three rules that keep slides stable:
1. No markdown parsing. Every slide is a function call with explicit inline data. 2. No `cur_top` cumulative position tracking. Use the fixed coordinate zones below — cur_top accumulates rounding errors and breaks layout after ~10 slides. 3. No Marp. Marp renders to images; the deck becomes uneditable and reviewers cannot copy text or restyle.
Slide-type templates
| Template | Use for | Required fields |
|---|---|---|
T_lead | Title slide, section divider | title, subtitle?, extra? |
T_text | Bullet body (most common) | title, body_lines[], subtitle? |
T_table | Cohort tables, comparisons | title, headers[], rows[][], body_before? |
T_image_right | Body + figure on right | title, body_lines[], img_path, img_pct? (PNG ≥300dpi or vector PDF — see Figure source formats below) |
T_quote_slide | Verbatim citations, witness quotes | title, quotes[], body_after?, img_path? |
T_two_col | Compare/contrast | title, left_lines[], right_lines[] |
T_two_col_with_box | Compare + emphasis | as above + metaphor_col, metaphor_lines[] |
T_highlight_slide | Single key result | title, highlight_lines[], body_before? |
T_metaphor_body | Body + analogy footer | title, body_lines[], metaphor_lines[] |
T_table_two_col | Take-aways + numeric table | title, left_lines[], headers[], rows[][] |
Figure source formats (when consuming /make-figures output)
When the deck pulls figures from analysis/figures/ produced by /make-figures:
- Preferred for slides: PNG at ≥300 dpi. python-pptx
add_picture()handles this directly. Setimg_pct(templateT_image_right) so the figure occupies ≥40 % of slide width on a 13.33 × 7.5-in widescreen layout. - Vector source available: prefer PDF only if the slide will be projected at >1080p or printed as a handout — convert PDF → PNG at the target DPI (
pdftoppm -r 300 input.pdf out_prefix) before insertion, because python-pptx PDF embedding is unreliable across PowerPoint versions. - Forbidden: TIFF (Mac PowerPoint silently drops it — see Mac compatibility checklist below); JPEG for line art (compression artifacts on diagonal lines); raw SVG (PowerPoint Mac handles it inconsistently).
- Caption / legend: re-draft for spoken-narration context, not the journal legend verbatim. The journal legend assumes a reader; the slide caption assumes a listener with 5–10 seconds of attention.
Helpers (used by templates — usually you do not call directly)
| Helper | Role |
|---|---|
_text | Single text box with **bold** inline markup |
_multiline | Multi-line block with bullet (- , ✓ ) and ### subhead support |
_title_block | Title + teal underline + optional subtitle |
_table | Styled table (teal header row, alternating rows) |
_quote | Blockquote — teal left bar + light-blue background |
_highlight | Yellow rounded box + orange 2pt border |
_metaphor | Same shape as quote, lighter font |
_image | PIL aspect-preserving image insert (handles iPhone EXIF if you transpose first) |
_slidenum | Bottom-right page number |
Design tokens (defaults — change to fit institution/journal)
NAVY = #1B2A4A # title text, section divider background
TEAL = #0072B2 # subtitle, underline, table header bg, quote bar
ORANGE = #D55E00 # highlight box border
GRAY = #333333 # body text
FONT = 'Apple SD Gothic Neo' # use a Latin-only font on non-Korean decksFixed coordinate zones (16:9 = 13.333" × 7.5")
ML / MR = 0.8" MT = 0.5" CW = SW − ML − MR = 11.733"
TITLE_Y = 0.5" TITLE_H = 0.8"
SUB_Y = 1.3" SUB_H = 0.5"
BODY_Y ≈ 1.9" BODY_H ≈ 5.1"Build script responsibilities
A from-scratch generation script must:
- Convert TIFF images to PNG before
add_picture(Mac PowerPoint silently drops TIFF). - Apply EXIF transpose to iPhone photos before insertion.
- After inserting/removing slides, sync
docProps/app.xml(<Slides>,<Notes>,HeadingPairs,TitlesOfParts) to the actual count, or PowerPoint Mac will raise a recovery dialog on open. - If you copy
<a:srcRect>from another deck, copy the values verbatim — they are 1/1000-percent (cap 100000), never EMU. A unit conversion bug here crops 99% of the image off-slide. - Print slide count, notes count, file size, and editability check at the end.
Forbidden in Mode A
- ❌ Marp CLI for PPTX (always image-rendered, uneditable).
- ❌ Markdown auto-parsing into slides (layout drifts on every regeneration).
- ❌
cur_topcumulative top tracking (accumulates rounding error). - ❌ Direct iPhone photo insert without EXIF transpose (rotated 90° in PowerPoint).
- ❌ Using
python-pptxfrom-scratch rebuild to edit an existing deck — see Patch over Rebuild below.
Mac PowerPoint compatibility checklist
PowerPoint Mac is stricter than Windows / Keynote / LibreOffice on OOXML defects. Verify before delivering any deck destined for a Mac viewer:
| Defect | Detect | Fix |
|---|---|---|
| TIFF images | find ppt/media -iname '*.tif*' | sips -s format png in.tif --out out.png + replace .tif→.png in _rels/*.rels |
| `<a:sp3d>` in rPr | grep -l '<a:sp3d>' ppt/slides/*.xml | Regex-strip the <a:sp3d>...</a:sp3d> block (renders as red outline only on Mac) |
| `app.xml` count mismatch | <Slides> value + HeadingPairs count + TitlesOfParts size vs actual slide files | Sync all four fields to real count |
| `srcRect` corruption | Any value > 100000 (1/1000-percent cap) | Compare with original deck; restore verbatim |
Validation must run on PDF export AND Mac PowerPoint — neither alone catches all four. PDF misses sp3d outlines and srcRect corruption.
Patch over Rebuild — editing an existing PPTX
When the user supplies an existing deck and asks for surgical edits (textbox width, image crop, font swap, sp3d removal), prefer regex/sed patching of the unzipped XML over regenerating with python-pptx. From-scratch rebuild loses:
<a:srcRect>image crops<a:sp3d>/<a:scene3d>(when intentional)- Slide master / layout / theme details
app.xmlandcore.xmlmetadata
unzip -q original.pptx -d /tmp/work
python3 -c "
import re; from pathlib import Path
p = Path('/tmp/work/ppt/slides/slide23.xml')
s = p.read_text()
s = s.replace('cx=\"9504720\"', 'cx=\"11200000\"')
p.write_text(s)
"
cd /tmp/work && zip -rq ../patched.pptx . -x '*.DS_Store'python-pptx is reserved for (a) brand-new decks built via the templates above, or (b) appending speaker notes via slide.notes_slide.notes_text_frame.text. The skill's scripts/inject_speaker_notes.py is the canonical example of (b). It parses inline **bold** / *italic* into run-level styling by default (python-pptx stores text verbatim, so the markers would otherwise show literally in Presenter View — the failure mode pptx-speaker-notes.md warns against); pass --no-markdown for legacy plain text. A reproducible check lives at tests/test_speaker_notes_markdown.py.
Standard structure (10–15 min paper talk)
1. Title slide (T_lead) — paper citation + presenter 2. Background (T_text × 1–2) 3. Study design / Methods (T_text or T_two_col) 4. Key results with figures (T_image_right / T_table × 2–3) 5. Discussion (T_text) 6. Limitations (T_two_col_with_box works well) 7. Take-home (T_text or T_highlight_slide)
Output
Save to output/presentation.pptx. Speaker notes go into the notes pane only — never modify slide design when adding notes.
Step 3.5 — Slide critic (run before delivering deck)
After exporting the PPTX, run the slide critic rubric at references/critic_rubrics/slide.md. Score each slide and the deck-level Mac compatibility checks (Section F) as PASS / PARTIAL / FAIL. Produce concrete edits for every FAIL or PARTIAL item before treating the deck as ready.
Mandatory deck-level checks (cross-link with ~/.claude/rules/pptx-mac-compatibility.md):
# F.22 No TIFF
find ppt/media -iname '*.tif*' || true # must be empty
# F.23 No 3-D bevel
grep -l '<a:sp3d>' ppt/slides/*.xml # must be empty
# F.24 app.xml count sync
grep -c '<Slides>\|<Notes>' docProps/app.xml
ls ppt/slides/slide*.xml | wc -l # must match
# F.25 srcRect bounds (any value > 100000 = bug)
grep -oE '"[0-9]{6,}"' ppt/slides/*.xml | headRecord critic_pass: yes | partial | no and refine_rounds: N in _quick_review.md.
Mode B: Add notes to existing slides (more common)
- Read existing PPTX to understand slide structure and count
- Map speaker script sections to corresponding slides
- Generate
inject_notes.pyscript tailored to the specific presentation
Note Injection Script
Generate a tailored inject_notes.py following the pattern in ${CLAUDE_SKILL_DIR}/scripts/inject_speaker_notes.py. The generated script should contain only the notes dictionary customized for this presentation and the main injection loop from the template.
Critical Rule
Speaker notes are injected without modifying slide design, layout, text, or images. The script only touches the notes pane. Verify by comparing slide content before and after.
---
Phase 4: Q&A Preparation
Question Generation
Generate questions from multiple perspectives:
1. Methodology critics: "Why this design? Why not...?" 2. Domain experts: Deep technical questions about the specific field 3. Generalists: "What does this mean for clinical practice?" 4. Students/trainees: Clarification questions about unfamiliar concepts
Answer Structure
Every answer should follow the pattern:
Acknowledge → Evidence → Conclude
"That's an important limitation. [Acknowledge the concern honestly.]
However, [cite specific supporting evidence — author, year, finding].
So while [restate limitation], [conclude with the paper's contribution despite it]."Quick Review Sheet
A single-page reference for last-minute review:
## Quick Review
### Must-Know Numbers
| Metric | Value | Source |
|--------|-------|--------|
| [Key stat 1] | [value] | [Ref] |
| [Key stat 2] | [value] | [Ref] |
### Common Pitfalls
- Don't confuse [X] with [Y]
- [Classification A] and [Classification B] are independent frameworks
- Slide says [rounded value], precise value is [exact value]
### Key Takeaways (memorize these)
1. [Point 1]
2. [Point 2]
3. [Point 3]---
Output File Structure
All outputs go in the user's presentation directory:
{presentation_dir}/
├── _analysis.md # Phase 0: Paper analysis + outline
├── _references.md # Phase 1: Verified references + key data
├── _script.md # Phase 2: Speaker script
├── _qa_prep.md # Phase 4: Expected Q&A
├── _quick_review.md # Phase 4: Pre-presentation review sheet + critic_pass record
├── _slide_critic.md # Phase 3.5: Slide rubric scores per slide
├── inject_notes.py # Phase 3: Tailored note injection script
├── figures/ # Extracted paper figures (if needed)
└── reference/ # Supporting paper PDFs (if downloaded)Cross-skill / Cross-rule integration
This skill composes with adjacent skills and global rules:
| When | Use | Why |
|---|---|---|
| Need a figure on a slide (ROC, forest, KM, flow) | /make-figures first, then embed | Both skills share Reynolds/Knaflic/Tufte foundations; figure-level + slide-level companions |
| Manuscript reporting checklist parallel | /check-reporting for the same paper | Paper presentations often shadow manuscript revision; reporting-guideline gaps surface in Q&A |
| Visual abstract / Central Illustration | /make-figures visual-abstract templates | Then verify against ~/.claude/rules/journal-ai-image-policies.md (JACC prohibits, Radiology allows with disclosure) |
| PPTX edits to existing institutional template | ~/.claude/rules/pptx-mac-compatibility.md | Patch over rebuild; preserve master/layout/srcRect |
| Manuscript companion deck | ~/.claude/rules/manuscript-style-classical.md | Heading style, AI-Disclosure policy, em-dash discipline carry over to slides for senior MA reviewer audiences |
| References on slides | /verify-refs (audit-only) before delivery | Same anti-hallucination gate as manuscript references |
---
Constraints
- Never fabricate references. Every citation must be verified against PubMed, DOI, or the PDF itself.
- Never modify slide design when injecting notes. Notes and slides are separate concerns.
- Always ask audience first. Do not start drafting until the target audience is defined.
- Extension sections are opt-in. Do not add AI/clinical/policy sections unless explicitly requested.
- Respect presentation time. Script length must match allocated time (roughly 130-150 words per minute for academic presentations).
Anti-Hallucination
- Never fabricate references. All citations must be verified via
/search-litwith confirmed DOI or PMID. Mark unverified references as[UNVERIFIED - NEEDS MANUAL CHECK]. - Never invent clinical definitions, diagnostic criteria, or guideline recommendations. If uncertain, flag with
[VERIFY]and ask the user. - Never fabricate numerical results — compliance percentages, scores, effect sizes, or sample sizes must come from actual data or analysis output.
- If a reporting guideline item, journal policy, or clinical standard is uncertain, state the uncertainty rather than guessing.
Critic Rubric — Slide
Apply this rubric when the generated slide deck (PPTX, PDF export, or rendered preview) is ready for review. Walk every slide and mark each item below as PASS / PARTIAL / FAIL with a one-line justification.
After scoring, produce a list of concrete edits that would resolve every FAIL or PARTIAL item. Return the scored rubric + edit list to the user.
Sources for these rules: slide_design_principles.md (Reynolds, Duarte, Knaflic,Tufte) and ~/.claude/rules/pptx-mac-compatibility.md. Items marked medical aredrawn from RadioGraphics 2025 ("Winning at the Radiology Podium") and RSNA refresher
course conventions.
---
A. Message clarity (per slide)
1. One key message — the slide conveys exactly one take-home point. A slide with two key messages must be split. 2. Sentence-headline title — the title states the conclusion ("RFA reduced local recurrence by 38%"), not the topic ("Results"). 3. Glance Test ≤ 3 s — a viewer can grasp the slide's meaning within 3 seconds and return attention to the speaker. 4. Reading-time budget matches context — slide content density is appropriate for journal club / grand rounds / conference / lecture (per slide_design_principles.md §2 table).
B. Cognitive load
5. Body text ≤ 6 lines at the slide level (counting wrapped lines, not bullets). 6. Bullet nesting ≤ 2 levels — no 3-indent nested bullets. 7. Charts per slide ≤ 1 — two charts on one slide compete for attention. 8. No decorative animation — animations exist only when they encode a step (build-up of a flow diagram), not for transition flair. 9. No 3-D / drop-shadow / gradient fill unless the third dimension encodes data.
C. Visual hierarchy (Knaflic preattentive)
10. Size hierarchy — title > subtitle > body > caption (≥ 4 pt step between levels). 11. Color carries meaning — accent color used on the one element that matters; rest is neutral. Colors ≤ 3 + 1 accent. 12. Position priority — conclusion / take-home placed at slide top, not bottom. 13. Direct labels on chart series — legend only when ≥ 4 series; otherwise label each curve / bar / wedge directly.
D. Typography
14. Font count ≤ 2 throughout the deck (typically 1 for title, 1 for body). 15. Font size ≥ 18 pt body for slides; ≥ 24 pt for posters; ≥ 28 pt for room sizes
100 seats.
16. Single Korean font when the deck mixes Korean + English (Apple SD Gothic Neo or Pretendard); avoid system Times-fallback for CJK text. 17. Capitalization consistency — sentence case OR title case throughout, not mixed.
E. Native objects (editability)
18. Charts are native PowerPoint objects — double-clicking opens the data table, not an image preview. (Co-authors must be able to update numbers in revision.) 19. Tables are native PowerPoint tables — not pasted screenshots from the paper. 20. Flow diagrams are grouped shapes — not embedded make-figures PNG exports (PNG is acceptable only for visual abstracts and dataset-flow figures intended as one-shot graphics). 21. Images are PNG ≥ 300 dpi or vector PDF — TIFF prohibited (Mac PowerPoint silently drops TIFF — see pptx-mac-compatibility rule).
F. Mac PowerPoint compatibility (deck-level)
Run this once on the final deck before sending to a Mac viewer:
22. No TIFF in `ppt/media/` — find ppt/media -iname '*.tif*' returns empty. 23. No `<a:sp3d>` in slide XML — grep -l '<a:sp3d>' ppt/slides/*.xml returns empty (3-D bevel renders as red outline only on Mac). 24. `app.xml` count synced — <Slides>, <Notes>, HeadingPairs, and TitlesOfParts match the actual file count (mismatch triggers Mac PowerPoint recovery dialog). 25. No `<a:srcRect>` value > 100000 — values are in 1/1000-percent (cap 100000); a unit-conversion bug crops 99% of the image off-slide.
G. Medical presentation specifics
26. Slide pacing — approximately 1 slide per minute of presentation time. A 30-min talk uses 25–30 content slides (excluding title, recap, Q&A buffer). Lectures may pace slower (1 slide / 2 min) when teaching concepts. 27. Background slides ≤ 3 — extended background belongs in the discussion or in backup slides, not in the opening. 28. PICO frame — for evidence-based presentations (journal club, evidence reviews), Population / Intervention / Comparison / Outcome stated explicitly on one slide. 29. Limitations slide present — every paper-driven medical talk has a limitations slide; absence is grounds for FAIL. 30. Strengths / weaknesses 2-column — journal club decks must contrast study strengths and weaknesses (typically a 2-column slide). 31. Statistical vs clinical significance — when a p-value is shown, a follow-up framing of clinical significance (effect size, NNT, absolute risk reduction) is present on the same slide or the next. 32. Disclosure / COI on title slide (or slide 2) — institutional requirement for most medical conferences.
H. References and AI policy
33. Inline citation at point-of-use — [Smith 2024] placed on the slide where the fact appears, not consolidated on a single end-slide. 34. Reference list slide ≤ 1 — if mandated by venue, capped to one slide; otherwise move full bibliography to a handout or supplementary. 35. AI-image policy verified — if any visual abstract / Central Illustration is AI-generated, the target audience/journal AI policy is checked (~/.claude/rules/journal-ai-image-policies.md). JACC family prohibits without EIC permission; Radiology family allows with disclosure.
I. Q&A readiness (Phase 4 cross-check)
36. Backup slides exist for foreseeable methodology questions (≥ 3 anticipated questions covered). 37. Quick Review sheet present — 1-page reference of must-know numbers, common pitfalls, and key takeaways (per SKILL.md Phase 4).
---
Scoring output format
## Critic report (slide deck, round T)
### Slide-by-slide
| Slide # | Title | Item | Score | Note |
|---------|-------|------|-------|------|
| 1 | Title | A.32 disclosure | FAIL | No COI line |
| 5 | Methods | A.2 sentence-headline | PARTIAL | Reads "Methods" — convert to "Cohort: 142 RFA + adjuvant chemo" |
| 12 | Forest plot | C.13 direct labels | FAIL | Legend has 5 series but plot has only 3 |
| ... | | | | |
### Deck-level
| Item | Score | Note |
|------|-------|------|
| F.22 No TIFF | PASS | All images are PNG |
| F.23 No sp3d | FAIL | slide14.xml + slide17.xml have 3-D bevel — strip via regex |
| G.26 Pacing | PASS | 27 content slides for 30-min talk |
| G.29 Limitations | FAIL | Missing — add before take-home |
| ... |
### Required edits before next render
1. Slide 1: Add "Disclosure: No COI" line below presenter name.
2. Slide 5: Rewrite title from "Methods" to one-sentence summary of the cohort.
3. Slide 12: Replace 5-entry legend with direct labels on each curve.
4. Strip `<a:sp3d>` from slide14.xml and slide17.xml.
5. Insert Limitations slide between current slides 24 and 25.
### Overall verdict
[ ] PASS — ready for delivery
[ ] REFINE — items above must be fixed before next roundRecord critic_pass: yes | partial | no and refine_rounds: N in _quick_review.md for this presentation after the final round.
#!/usr/bin/env python3
"""Reference template library for editable academic PPTX generation.
Design tokens (NAVY/TEAL/ORANGE/GRAY, Apple SD Gothic Neo) and 16:9 fixed
coordinate zones are defaults — adapt to journal/institution conventions
as needed. NO markdown parsing, NO `cur_top` cumulative tracking (both
produce drift artifacts).
Each slide is defined as structured data → rendered by a type-specific
template function (T_lead / T_text / T_table / T_image_right / T_two_col
/ T_quote_slide / T_highlight_slide / T_metaphor_body / T_two_col_with_box
/ T_table_two_col).
`build_demo_slides()` at the bottom demonstrates every template with
generic academic placeholder content; copy and replace with your own
content function for a real deck.
Speaker-note injection: `parse_notes()` reads a markdown script with
`## Slide N:` headers and maps sections back to slide indices. For an
existing deck where you only want to add notes (no design regeneration),
use the sibling `inject_speaker_notes.py` instead.
Mac PowerPoint compatibility — verify before delivery:
1. TIFF images: `find ppt/media -iname '*.tif*'` → convert to PNG
2. `<a:sp3d>` 3D bevels in rPr render as red outlines on Mac → strip
3. `docProps/app.xml` Slides/Notes/HeadingPairs/TitlesOfParts must
match actual slide count
4. `<a:srcRect>` values are 1/1000-percent (cap 100000) — never EMU
See `references/workflow-checklist.md` and the user-level rule
`~/.claude/rules/pptx-mac-compatibility.md` for the full checklist.
"""
import os
import re
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn
# ── Design tokens ──────────────────────────────────────────────
NAVY = RGBColor(0x1B, 0x2A, 0x4A)
TEAL = RGBColor(0x00, 0x72, 0xB2)
ORANGE = RGBColor(0xD5, 0x5E, 0x00)
GRAY = RGBColor(0x33, 0x33, 0x33)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
LGRAY = RGBColor(0x99, 0x99, 0x99)
BG_BLUE = RGBColor(0xF0, 0xF7, 0xFF)
BG_YELLOW = RGBColor(0xFF, 0xF8, 0xE1)
SUB_BLUE = RGBColor(0x7B, 0xB8, 0xE0)
FONT = 'Apple SD Gothic Neo'
SW = Inches(13.333) # slide width
SH = Inches(7.5) # slide height
ML = Inches(0.8) # margin left
MR = Inches(0.8) # margin right
MT = Inches(0.5) # margin top
CW = SW - ML - MR # content width
# Fixed vertical zones
TITLE_Y = MT
TITLE_H = Inches(0.8)
SUB_Y = TITLE_Y + TITLE_H
SUB_H = Inches(0.5)
BODY_Y = SUB_Y + SUB_H + Inches(0.1) # ~1.9in from top
BODY_H = SH - BODY_Y - Inches(0.5) # fill to bottom margin
# ── Low-level helpers ──────────────────────────────────────────
def _bg(slide, color):
slide.background.fill.solid()
slide.background.fill.fore_color.rgb = color
def _text(slide, x, y, w, h, txt, sz=20, color=GRAY, bold=False,
align=PP_ALIGN.LEFT, auto_fit=False):
"""Add a text box. Returns the shape."""
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
if auto_fit:
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
p = tf.paragraphs[0]
p.alignment = align
_runs(p, txt, sz, color, bold)
return tb
def _runs(para, txt, sz, color, bold):
"""Add text with **bold** inline markup support."""
parts = re.split(r'(\*\*.*?\*\*)', txt)
first = True
for part in parts:
if not part:
continue
is_b = part.startswith('**') and part.endswith('**')
clean = part.strip('*') if is_b else part
run = para.runs[0] if first and para.runs else para.add_run()
first = False
run.text = clean
run.font.size = Pt(sz)
run.font.color.rgb = NAVY if is_b else color
run.font.bold = bold or is_b
run.font.name = FONT
def _multiline(slide, x, y, w, h, lines, sz=20, color=GRAY, auto_fit=True):
"""Add multi-line text box with bullet support."""
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
if auto_fit:
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
first = True
for line in lines:
if not line.strip():
continue
if first:
p = tf.paragraphs[0]
first = False
else:
p = tf.add_paragraph()
p.space_before = Pt(6)
is_bullet = line.startswith('- ') or line.startswith('✓ ')
txt = line.lstrip('-✓ ').strip() if is_bullet else line.strip()
if line.strip().startswith('### '):
_runs(p, line.strip()[4:], sz + 2, TEAL, True)
else:
if is_bullet:
p.level = 0
_runs(p, txt, sz, color, False)
return tb
def _title_block(slide, title, subtitle=None, width=None):
"""Standard title + teal underline + optional subtitle."""
w = width or CW
_text(slide, ML, TITLE_Y, w, TITLE_H, title, sz=28, color=NAVY, bold=True)
# Teal underline
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
ML, TITLE_Y + Inches(0.7), w, Inches(0.05))
bar.fill.solid()
bar.fill.fore_color.rgb = TEAL
bar.line.fill.background()
if subtitle:
_text(slide, ML, SUB_Y, w, SUB_H, subtitle, sz=22, color=TEAL)
def _table(slide, headers, rows, x, y, w, row_h=Inches(0.45)):
"""Add a styled table."""
nr = len(rows) + 1
nc = len(headers)
if nc == 0:
return
shape = slide.shapes.add_table(nr, nc, x, y, w, row_h * nr)
tbl = shape.table
for j, h in enumerate(headers):
c = tbl.cell(0, j)
c.text = h.strip('*')
_cell_style(c, 15, WHITE, True, TEAL)
for i, row in enumerate(rows):
for j, val in enumerate(row):
if j >= nc:
break
c = tbl.cell(i + 1, j)
c.text = val.strip('*')
bg = RGBColor(0xF5, 0xF5, 0xF5) if i % 2 == 0 else WHITE
bld = val.strip().startswith('**')
_cell_style(c, 14, GRAY, bld, bg)
def _cell_style(cell, sz, color, bold, bg):
for p in cell.text_frame.paragraphs:
for r in p.runs:
r.font.size = Pt(sz)
r.font.color.rgb = color
r.font.bold = bold
r.font.name = FONT
cell.vertical_anchor = MSO_ANCHOR.MIDDLE
tcPr = cell._tc.get_or_add_tcPr()
sf = tcPr.makeelement(qn('a:solidFill'), {})
sf.append(sf.makeelement(qn('a:srgbClr'), {'val': str(bg)}))
tcPr.append(sf)
def _quote(slide, txt, x, y, w, h, sz=20):
"""Blockquote with teal left bar + blue background."""
# Bar
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, Inches(0.08), h)
bar.fill.solid(); bar.fill.fore_color.rgb = TEAL
bar.line.fill.background()
# Background
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
x + Inches(0.08), y, w - Inches(0.08), h)
bg.fill.solid(); bg.fill.fore_color.rgb = BG_BLUE
bg.line.fill.background()
# Text
_text(slide, x + Inches(0.3), y + Inches(0.1),
w - Inches(0.5), h - Inches(0.2), txt, sz=sz, auto_fit=True)
def _highlight(slide, lines, x, y, w, h):
"""Yellow highlight box with orange border."""
box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, w, h)
box.fill.solid(); box.fill.fore_color.rgb = BG_YELLOW
box.line.color.rgb = ORANGE; box.line.width = Pt(2)
_multiline(slide, x + Inches(0.3), y + Inches(0.2),
w - Inches(0.6), h - Inches(0.4), lines, sz=18, auto_fit=True)
def _metaphor(slide, lines, x, y, w, h):
"""Light-blue metaphor box with teal left bar."""
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, Inches(0.08), h)
bar.fill.solid(); bar.fill.fore_color.rgb = TEAL
bar.line.fill.background()
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
x + Inches(0.08), y, w - Inches(0.08), h)
bg.fill.solid(); bg.fill.fore_color.rgb = BG_BLUE
bg.line.fill.background()
_multiline(slide, x + Inches(0.3), y + Inches(0.15),
w - Inches(0.6), h - Inches(0.3), lines, sz=17, auto_fit=True)
def _image(slide, path, x, y, w, h):
"""Add image preserving aspect ratio, centered in box."""
if not os.path.exists(path):
print(f' WARNING: {path} not found')
return
from PIL import Image as PILImage
with PILImage.open(path) as img:
iw, ih = img.size
aspect = iw / ih
tw, th = int(w), int(h)
if tw / th > aspect:
fw, fh = int(th * aspect), th
else:
fw, fh = tw, int(tw / aspect)
ax = x + (tw - fw) // 2
ay = y + (th - fh) // 2
slide.shapes.add_picture(path, ax, ay, fw, fh)
def _slidenum(slide, n, total):
_text(slide, SW - Inches(1.2), SH - Inches(0.45), Inches(1.0), Inches(0.3),
f'{n} / {total}', sz=10, color=LGRAY, align=PP_ALIGN.RIGHT)
# ── Slide type templates ───────────────────────────────────────
def T_lead(prs, title, subtitle='', extra=''):
"""Section divider: navy bg, centered white text."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_bg(s, NAVY)
_text(s, ML, Inches(2.2), CW, Inches(1.2), title,
sz=40, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
if subtitle:
_text(s, ML, Inches(3.6), CW, Inches(0.8), subtitle,
sz=24, color=SUB_BLUE, align=PP_ALIGN.CENTER)
if extra:
_text(s, ML, Inches(4.6), CW, Inches(1.0), extra,
sz=18, color=LGRAY, align=PP_ALIGN.CENTER)
return s
def T_text(prs, title, body_lines, subtitle=None):
"""Title + bullet/text body. Most common type."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
_multiline(s, ML, BODY_Y, CW, BODY_H, body_lines, sz=20)
return s
def T_table(prs, title, headers, rows, subtitle=None, body_before=None):
"""Title + optional body text + table."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
tbl_y = BODY_Y
if body_before:
_multiline(s, ML, BODY_Y, CW, Inches(1.5), body_before, sz=20)
tbl_y = BODY_Y + Inches(1.6)
_table(s, headers, rows, ML, tbl_y, CW)
return s
def T_image_right(prs, title, body_lines, img_path, img_pct=30, subtitle=None):
"""Title + body on left, image on right."""
s = prs.slides.add_slide(prs.slide_layouts[6])
frac = img_pct / 100
tw = int(SW * (1 - frac) - ML - Inches(0.3))
iw = int(SW * frac)
ix = SW - iw
_title_block(s, title, subtitle, width=tw)
_multiline(s, ML, BODY_Y, tw, BODY_H, body_lines, sz=20)
_image(s, img_path, ix, Inches(0.2), iw, SH - Inches(0.4))
return s
def T_quote_slide(prs, title, quotes, body_after=None, img_path=None,
img_pct=30, subtitle=None):
"""Title + blockquote(s). Optionally with image."""
s = prs.slides.add_slide(prs.slide_layouts[6])
if img_path:
frac = img_pct / 100
tw = int(SW * (1 - frac) - ML - Inches(0.3))
iw = int(SW * frac)
ix = SW - iw
_title_block(s, title, subtitle, width=tw)
_image(s, img_path, ix, Inches(0.2), iw, SH - Inches(0.4))
else:
tw = CW
_title_block(s, title, subtitle)
# Distribute quotes evenly in body area
n = len(quotes)
gap = Inches(0.15)
q_h = int((BODY_H - gap * (n - 1)) / n) if n else BODY_H
for i, q in enumerate(quotes):
qy = BODY_Y + (q_h + gap) * i
_quote(s, q, ML, qy, tw, q_h, sz=18)
if body_after:
_text(s, ML, SH - Inches(1.2), tw, Inches(0.6),
body_after, sz=16, color=GRAY)
return s
def T_two_col(prs, title, left_lines, right_lines, subtitle=None):
"""Two-column layout."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
cw = int((CW - Inches(0.5)) / 2)
gap = Inches(0.5)
_multiline(s, ML, BODY_Y, cw, BODY_H, left_lines, sz=18)
_multiline(s, ML + cw + gap, BODY_Y, cw, BODY_H, right_lines, sz=18)
return s
def T_two_col_with_box(prs, title, left_lines, right_lines, subtitle=None,
metaphor_col=None, metaphor_lines=None):
"""Two-column with metaphor/highlight box in one column."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
cw = int((CW - Inches(0.5)) / 2)
gap = Inches(0.5)
# Left column
if metaphor_col == 'left' and metaphor_lines:
_metaphor(s, metaphor_lines, ML, BODY_Y, cw, Inches(2.5))
_multiline(s, ML, BODY_Y + Inches(2.7), cw, BODY_H - Inches(2.7),
left_lines, sz=18)
else:
_multiline(s, ML, BODY_Y, cw, BODY_H, left_lines, sz=18)
# Right column
rx = ML + cw + gap
if metaphor_col == 'right' and metaphor_lines:
_metaphor(s, metaphor_lines, rx, BODY_Y, cw, Inches(2.5))
_multiline(s, rx, BODY_Y + Inches(2.7), cw, BODY_H - Inches(2.7),
right_lines, sz=18)
else:
_multiline(s, rx, BODY_Y, cw, BODY_H, right_lines, sz=18)
return s
def T_highlight_slide(prs, title, highlight_lines, subtitle=None,
body_before=None):
"""Slide with prominent highlight box."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
hy = BODY_Y
if body_before:
_multiline(s, ML, BODY_Y, CW, Inches(1.0), body_before, sz=20)
hy = BODY_Y + Inches(1.2)
_highlight(s, highlight_lines, ML, hy, CW, SH - hy - Inches(0.5))
return s
def T_metaphor_body(prs, title, body_lines, metaphor_lines, subtitle=None):
"""Body text + metaphor box below."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
mid = BODY_Y + BODY_H * 0.55
_multiline(s, ML, BODY_Y, CW, mid - BODY_Y - Inches(0.1),
body_lines, sz=20)
_metaphor(s, metaphor_lines, ML, mid, CW, SH - mid - Inches(0.5))
return s
def T_table_two_col(prs, title, left_lines, headers, rows, subtitle=None):
"""Left text + right table."""
s = prs.slides.add_slide(prs.slide_layouts[6])
_title_block(s, title, subtitle)
cw = int((CW - Inches(0.5)) / 2)
gap = Inches(0.5)
_multiline(s, ML, BODY_Y, cw, BODY_H, left_lines, sz=18)
_table(s, headers, rows, ML + cw + gap, BODY_Y, cw)
return s
# ── Speaker notes ──────────────────────────────────────────────
def parse_notes(path):
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
# Match either `## Slide N:` (English) or legacy `--- 슬라이드 N: ... ---`
pattern = r'(?:^##\s*Slide\s+(\d+)\s*:|---\s*슬라이드\s*(\d+)\s*:.*?---)'
matches = list(re.finditer(pattern, content, flags=re.MULTILINE))
notes = {}
for i, m in enumerate(matches):
num = int(m.group(1) or m.group(2))
start = m.end()
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
raw = content[start:end].strip()
# Clean
lines = []
for line in raw.split('\n'):
s = line.strip()
if s.startswith('## Part ') or s == '---' or s.startswith('```'):
continue
if s.startswith('*(') and s.endswith(')*'):
lines.append(f'[{s[2:-2].strip()}]')
else:
lines.append(s.replace('**', '').replace('*', ''))
text = re.sub(r'\n{3,}', '\n\n', '\n'.join(lines).strip())
if text:
notes[num] = text
return notes
# ── Demo: showcase every template ───────────────────────────────
def build_demo_slides(prs):
"""Generic academic demo — one slide per template type.
Replace this function with your own content builder. Keep all content
as inline structured data (lists, dicts) — never parse markdown.
"""
slides = []
# 1. Title (T_lead)
slides.append(T_lead(
prs,
'Editable Academic PPTX — Template Showcase',
'A python-pptx reference deck',
'Speaker · Affiliation · Date'))
# 2. Section divider (T_lead)
slides.append(T_lead(
prs,
'Section 1: Background',
'Setting up the question'))
# 3. Bullet body (T_text)
slides.append(T_text(
prs,
'Background',
['- Disease X affects an estimated **N patients** worldwide.',
'- Standard imaging modality: **modality Y**.',
'- Existing limitations:',
' - Inter-reader variability',
' - Long acquisition time',
' - Reader fatigue on volume studies'],
subtitle='Why this study matters'))
# 4. Two-column compare (T_two_col)
slides.append(T_two_col(
prs,
'Conventional vs. Proposed Approach',
['### Conventional',
'- Manual segmentation',
'- ~30 min per case',
'- Reader-dependent'],
['### Proposed',
'- Deep-learning assisted',
'- ~3 min per case',
'- Standardized output'],
subtitle='Workflow comparison'))
# 5. Quote slide (T_quote_slide)
slides.append(T_quote_slide(
prs,
'Prior Evidence',
['"AI-assisted reading reduced inter-reader variability '
'by 40% in a multi-center study." — Author et al., Journal Y, 2024',
'"External validation remains the principal gap in clinical '
'translation of imaging AI." — Reviewer A, 2025']))
# 6. Table (T_table)
slides.append(T_table(
prs,
'Cohort Characteristics',
['Variable', 'Training (n=800)', 'Internal test (n=200)', 'External (n=300)'],
[['Age, mean (SD)', '62 (11)', '63 (10)', '60 (12)'],
['Female, n (%)', '420 (52.5)', '105 (52.5)', '160 (53.3)'],
['Disease prevalence', '38%', '40%', '35%'],
['**Imaging vendor mix**', 'A/B/C', 'A/B/C', 'D/E']],
subtitle='Demographics across splits'))
# 7. Image-right (T_image_right) — pass empty path to skip image safely
slides.append(T_image_right(
prs,
'Methods Pipeline',
['- DICOM ingest with anonymization',
'- Preprocessing: resampling, normalization',
'- Model: 3D U-Net (or transformer variant)',
'- Output: probability map + binary mask',
'- QA: radiologist override loop'],
img_path='figures/pipeline.png', # replace with real asset
img_pct=35,
subtitle='From DICOM to decision'))
# 8. Highlight slide (T_highlight_slide)
slides.append(T_highlight_slide(
prs,
'Primary Result',
['**AUC = 0.92 (95% CI 0.89–0.95)** on external test set',
'Sensitivity 0.88 · Specificity 0.91',
'Non-inferior to senior reader (Δ AUC = 0.01, p = 0.78)'],
subtitle='Diagnostic performance',
body_before=['On the prospectively collected external cohort '
'(n = 300, 5 sites):']))
# 9. Metaphor body (T_metaphor_body)
slides.append(T_metaphor_body(
prs,
'Interpretation',
['- Performance preserved across vendor and protocol shifts.',
'- Calibration check: ECE = 0.04 → clinically usable.',
'- Failure modes concentrated in motion-degraded studies.'],
['Think of the model as a **second reader** — fast, consistent, '
'never tired — but still requiring a final human sign-off on '
'edge cases.'],
subtitle='What it means in practice'))
# 10. Two-col with metaphor box (T_two_col_with_box)
slides.append(T_two_col_with_box(
prs,
'Limitations and Mitigations',
['### Limitations',
'- Single-modality input',
'- Retrospective external set',
'- No outcome data yet'],
['### Planned next steps',
'- Multi-modal fusion',
'- Prospective registration',
'- 12-month outcome follow-up'],
metaphor_col='right',
metaphor_lines=['**Open question** — is diagnostic accuracy a '
'sufficient endpoint, or do we need clinical '
'utility evidence?'],
subtitle='Honest accounting'))
# 11. Table-two-col (T_table_two_col)
slides.append(T_table_two_col(
prs,
'Comparison with Published Models',
['### Take-aways',
'- Comparable AUC at lower compute',
'- Better calibration on minority subgroup',
'- Released checkpoint + DICOM-SR exporter'],
['Model', 'AUC', 'Params'],
[['Baseline', '0.85', '11M'],
['Prior SOTA', '0.91', '78M'],
['**Ours**', '**0.92**', '**24M**']],
subtitle='Where this work sits'))
# 12. Take-home (T_text)
slides.append(T_text(
prs,
'Take-Home Messages',
['1. The model generalized to a **5-site external cohort** '
'(AUC 0.92).',
'2. Performance was **non-inferior to a senior reader**.',
'3. **Calibration and failure-mode transparency** were the '
'features that earned clinician trust — not raw AUC.',
'4. Next: prospective validation tied to clinical outcomes.'],
subtitle='Three points to remember'))
return slides
# ── Main ────────────────────────────────────────────────────────
def main():
prs = Presentation()
prs.slide_width = SW
prs.slide_height = SH
print('Building demo slides...')
slides = build_demo_slides(prs)
total = len(slides)
print(f' {total} slides created')
# Slide numbers (skip lead/section-divider slides with solid bg)
for i, slide in enumerate(slides):
bg_type = slide.background.fill.type
is_lead = bg_type is not None and 'SOLID' in str(bg_type)
if not is_lead:
_slidenum(slide, i + 1, total)
# Optional speaker-note injection — uncomment if a script file exists
# notes = parse_notes('speaker_script.md')
# for i, slide in enumerate(slides):
# num = i + 1
# if num in notes:
# slide.notes_slide.notes_text_frame.text = notes[num]
out = 'demo_slide_deck.pptx'
prs.save(out)
sz = os.path.getsize(out) / 1024
print(f'\nDone: {out}')
print(f' Slides: {total}')
print(f' Size: {sz:.0f} KB, Editable: YES')
if __name__ == '__main__':
main()
Medical Presentation Templates
Section structure, slide counts, and design tone for the four medical presentation contexts handled by this skill. Each template is a starting outline — not a rigid template. Adapt to specific paper, audience, and time slot.
Sources: RadioGraphics 2025 ("Winning at the Radiology Podium," DOI 10.1148/rg.250003);
RSNA Refresher Course Committee guidelines; UAB Physician Scientist Development Office
journal club framework; ASHP journal club template; medsci-skills internal patterns from
2026-04 grand rounds and journal club decks.
>
Triggered from: SKILL.md Phase 0 (Outline) — pick the template matching the
presentation context, then customize. Cross-reference slide_design_principles.md (§2reading-time budget table) and critic_rubrics/slide.md Section G.---
1. Journal club (paper-driven, peer trainees)
Audience: residents / fellows + 1–2 faculty. Same-specialty cohort. Time: 20–30 min total (10–15 min present + remainder discussion). Tone: Methodologically rigorous, critical, peer-collegial. Slide count: ~20–25 (1 slide / 30–60 s).
Section structure
| # | Section | Slides | Purpose |
|---|---|---|---|
| 1 | Title + COI | 1 | Paper citation, presenter, date, disclosure |
| 2 | Why this paper | 1 | Clinical question that led to selection |
| 3 | Background | 1–2 | Existing evidence, what's known vs unknown |
| 4 | Research question (PICO) | 1 | Population / Intervention / Comparison / Outcome |
| 5 | Study design | 1–2 | Schema diagram of arms, randomization, follow-up |
| 6 | Methods | 1–2 | Cohort, eligibility, primary outcome, statistical plan |
| 7 | Results | 3–5 | Primary outcome figure, key tables, subgroup if relevant |
| 8 | Strengths / Weaknesses | 1 | 2-column slide (per critic_rubrics/slide.md G.30) |
| 9 | Limitations | 1 | Author-acknowledged + presenter-identified |
| 10 | Statistical vs clinical significance | 1 | Effect size + NNT / absolute risk reduction |
| 11 | Clinical implications | 1 | How this changes (or doesn't change) practice |
| 12 | Take-home messages | 1 | 3 bullet points, no more |
| 13 | References / Q&A | 1 | Inline citations + key supporting refs |
Design seed
- Color: Institutional accent (1) + neutral grays (2). No 3-D, no decorative animation.
- Typography: Sans-serif (Helvetica / Calibri / Pretendard for KR). 28 pt title /
18–20 pt body.
- Figures: Use figures from the paper directly (cite slide-corner). Do not re-render
unless adding emphasis annotations.
- Speaker note density: 5–10 s of spoken content per slide.
Pre-presentation checklist
- [ ] Faculty review of paper selection (per UAB framework — verify educational
significance, relevance for practice, validity).
- [ ] Read paper independently before reading the abstract (avoid author-framing bias).
- [ ] Allocate ~50% of total time to discussion, not lecture.
- [ ] Verify
submission_safe: trueis not needed (journal club ≠ submission).
---
2. Grand rounds (mixed specialty / mixed seniority)
Audience: Multi-specialty, mixed trainee + attending. May include non-radiologists for imaging-themed rounds. Time: 45–60 min total (35–45 min present + remainder Q&A). Tone: Educational, broadly accessible, single-take-home framing. Slide count: ~30–45 (1 slide / 60–90 s, with worked examples).
Section structure
| # | Section | Slides | Purpose |
|---|---|---|---|
| 1 | Title + COI + Acknowledgments | 1–2 | Title, presenter affiliation, disclosure, mentor credit |
| 2 | Learning objectives | 1 | 3–4 specific objectives (verb + noun pattern) |
| 3 | Hook / clinical case | 1–2 | A real (de-identified) case that motivates the topic |
| 4 | Why this matters now | 1 | Burden / incidence / unmet clinical need |
| 5 | Background (concept review) | 2–3 | Pathophysiology / imaging principles, accessible level |
| 6 | Current standard of care | 1–2 | Where the field is today, what the gap is |
| 7 | Recent evidence | 5–8 | 2–3 key papers; one slide per take-home |
| 8 | Worked imaging examples | 4–8 | Side-by-side cases illustrating each teaching point |
| 9 | Practice algorithm / decision tree | 1–2 | Synthesis: how to apply in daily practice |
| 10 | Limitations of current evidence | 1 | Honest framing of what we still don't know |
| 11 | Future directions | 1 | Trials in progress, AI/computational angles if relevant |
| 12 | Take-home messages | 1 | Map back to learning objectives |
| 13 | Return to opening case | 1 | Show how the talk's content alters the case management |
| 14 | Q&A / References | 1 | Inline cites throughout; reference handout if requested |
Design seed
- Color: Institutional palette + 1 accent for highlights. Avoid >3 colors per slide.
- Typography: Title 32 pt / body 20–22 pt (room may seat ≥ 100).
- Figures: Mix of paper figures, original imaging, and SMART Servier anatomical icons.
AI-image disclosure if anything is AI-generated.
- Worked examples: Build-up animations acceptable (sequential reveal of imaging
findings) — but only when the build encodes a teaching step.
Pre-presentation checklist
- [ ] Learning objectives written with measurable verbs (avoid "understand"; prefer
"identify," "differentiate," "recommend").
- [ ] Imaging consent / de-identification verified for all cases.
- [ ] Disclosure slide includes industry relationships, advisory roles, prior publications
cited.
- [ ] Mentor pre-review completed (typical 1–2 weeks before).
---
3. Conference talk (RSNA / ECR / KCR / society meeting)
Audience: Specialist peers; high domain expertise. Time: 7–12 min present + 2–3 min Q&A (oral session standard). Tone: Tight, data-dense, focused on contribution. Slide count: ~10–14 for 10-min talk; ~14–18 for 12-min (1 slide / 45–60 s).
Section structure
| # | Section | Slides | Purpose |
|---|---|---|---|
| 1 | Title + Disclosure | 1 | Title, authors, affiliation, COI in 1 slide |
| 2 | Background + Gap | 1–2 | Tight: 1 sentence on context, 1 on gap |
| 3 | Hypothesis / Aim | 1 | Stated as a sentence-headline |
| 4 | Methods | 1–2 | Cohort flow diagram + key methodological choice |
| 5 | Primary results | 2–3 | One figure per result; sentence-headline title |
| 6 | Subgroup / sensitivity | 1 | Only if it's the most defensible secondary finding |
| 7 | Limitations | 1 | Brief — acknowledge top 2–3 |
| 8 | Conclusions | 1 | 1 sentence; mirrors abstract conclusion |
| 9 | Acknowledgments | 1 | Funding, collaborators, departmental support |
Design seed
- Color: One accent for primary result; everything else neutral.
- Typography: ≥ 24 pt body throughout (auditorium projection).
- Figures: Re-render journal figures for slide context (drop methodological detail,
enlarge axis labels, direct-label series). RSNA / ECR projection rooms project at 1080p — vector PDFs preferred.
- Speaker pace: 130–150 words/min for academic delivery; rehearse to ensure last
slide arrives at 9:30 of a 10-min slot.
Pre-presentation checklist
- [ ] Abstract was accepted for oral (not poster) — confirm slot type and time.
- [ ] Audience response system not used (oral sessions don't typically include ARS).
- [ ] Submitted disclosure to society (society disclosures often separate from manuscript
COI).
- [ ] Backup slide stack for top-3 anticipated questions ready.
---
4. Lecture / didactic (residents, course material)
Audience: Trainees in structured curriculum (radiology residents, fellows, medical students). Time: 50–60 min (full lecture slot). Tone: Pedagogical, build-up, recap-friendly. Slide count: ~30–40 (1 slide / 90–120 s, with worked examples and recap slides).
Section structure
| # | Section | Slides | Purpose |
|---|---|---|---|
| 1 | Title + Lecture position in curriculum | 1 | "Lecture 4 of 12: Body MRI series" |
| 2 | Learning objectives | 1 | Mapped to curriculum competencies |
| 3 | Recap of prior lecture | 1–2 | Where we were last time |
| 4 | Today's roadmap | 1 | 3–5 sections previewed |
| 5 | Section 1: Concept | 3–5 | Build-up + worked example + check-for-understanding |
| 6 | Section 2: Concept | 3–5 | Same pattern |
| 7 | ... | ||
| 8 | Synthesis / decision algorithm | 2–3 | How sections connect into clinical workflow |
| 9 | Self-assessment questions | 2–3 | 3–5 multiple-choice with answer reveal |
| 10 | Take-home messages | 1 | Map back to learning objectives |
| 11 | Pre-reading for next lecture | 1 | Specific paper / textbook chapter |
| 12 | References + further reading | 1 | Curated list, not exhaustive |
Design seed
- Color: Course palette consistent across all lectures in series.
- Typography: Title 28 pt / body 20 pt. Build-up animations acceptable for stepwise
concept introduction.
- Figures: Mix of textbook diagrams, original imaging, SMART Servier icons. Reusable
asset library across lecture series (per pptx-mac-compatibility rule — single asset source).
- Recap pattern: At each section end, a 1-slide recap before moving on. Helps
trainees with weaker prior knowledge.
- Self-assessment: Use audience-response polling if classroom supports it; otherwise
show MCQ → pause → reveal answer + explanation slide.
Pre-presentation checklist
- [ ] Lecture position in curriculum confirmed (not duplicating prior lecture content).
- [ ] Self-assessment questions mapped to learning objectives.
- [ ] Pre-reading assignment matches the lecture's prerequisites.
- [ ] Course-level branding consistent (logo, color, font).
---
5. Academic lecture — multi-paper survey (graduate course)
Audience: Graduate students from mixed disciplines (basic science, clinical, imaging, AI). Cross-disciplinary background; a single specialty's jargon will not land for everyone. Time: 30–45 min present + 5–10 min Q&A. Tone: Narrative arc across multiple papers; one lighthouse paper per section anchors the discussion, surrounding papers add nuance. Slide count: ~35–50 (1 slide / 45–75 s, with section dividers + transitions).
Section structure — 4-act narrative
| # | Section | Slides | Purpose |
|---|---|---|---|
| 1 | Title slide | 1 | Course position, instructor, date, presenter, affiliation |
| 2 | Outline (TOC) | 1 | 4 sections with time budgets visible |
| 3 | Glossary (optional) | 1 | Tier 1: disease/concept · Tier 2: method/stats abbreviations |
| 4 | Section 1 divider | 1 | "History" / "Background" — full-bleed deep navy |
| 5 | Section 1 content | 4–6 | Founding papers, original observations, classical concepts |
| 6 | Transition slide | 1 | Large coral quotes + one-sentence question motivating next act |
| 7 | Section 2 divider | 1 | "Concept" / "Classification" |
| 8 | Section 2 content | 5–8 | Modern framework, definitions, what we now know |
| 9 | Transition slide | 1 | Bridge to mechanism |
| 10 | Section 3 divider | 1 | "Mechanism" — typically the longest, anchored by 1–2 lighthouse papers |
| 11 | Section 3 content | 10–14 | Lighthouse paper figures, mechanistic details, 2024–2026 updates |
| 12 | Transition slide | 1 | Bridge to clinical translation |
| 13 | Section 4 divider | 1 | "Clinical translation" |
| 14 | Section 4 content | 6–10 | Diagnostic application, therapy, ongoing trials |
| 15 | Wrap-up / Take-home | 1–2 | 3 bullets max, mirrored to outline |
| 16 | Acknowledgments / Q&A | 1 | Funding, mentor thanks, contact |
Design seed
- Visual style: Nature/Lancet — see
slide_visual_styles/nature_lancet.md - Body language: English only (per
~/.claude/rules/academic-lecture-style.md§1) - Speaker notes: narrative, in the user's preferred language (English by default; a Korean narrative register is supported for Korean presenters), 150–300 chars / slide
- Section dividers: full-bleed
#121D36background + coral accent strip +{N} MIN
badge bottom-right
- Transition slides: large coral curly quotes (72pt) flanking a 36pt white question
- Lighthouse paper anchor: each section names one anchor paper in the divider
subtitle; section content slides reference it in eyebrow text
Pre-presentation checklist
- [ ] All
--em-dashes verified rendering correctly on Mac PowerPoint - [ ] TIFF images converted to PNG (per
pptx-mac-compatibility.md§1) - [ ]
<a:sp3d>3D bevel = 0 (perpptx-mac-compatibility.md) - [ ]
app.xmlSlides/Notes counts patched (perpptx-mac-compatibility.md§5) - [ ] Speaker notes parsed for
**bold**/*italic*markdown (per §4) - [ ] 40-min timing rehearsal completed (longest section flagged)
- [ ] Glossary slide present if audience is multidisciplinary
- [ ] Each section has ≥1 lighthouse paper figure (not text-only)
- [ ] PDF export visually verified (figure positions, font fallback)
- [ ] Mac PowerPoint actual-rendering check (PDF alone misses some defects)
Reference implementation
- Build script:
templates/build_pptx_nature_lancet.py(this skill) —add_title_slide,
add_toc_slide, add_glossary_slide, add_section_divider, add_transition_slide, add_content_slide, add_closing_slide + fix_app_xml helper
- Style spec:
slide_visual_styles/nature_lancet.md - Example deck: 47-slide academic lecture (4-section narrative across 17 lighthouse papers
+ supporting references), Mac PowerPoint verified
---
Mapping to SKILL.md phases
| Template element | SKILL.md phase | Notes |
|---|---|---|
| Audience definition | Phase 0 | Required input — controls template choice |
| Time slot | Phase 0 | Determines slide count target |
| Section structure | Phase 0 → outline | Use template as starting outline; customize |
| Speaker script density | Phase 2 | 130–150 wpm for academic talks |
| Slide design tokens | Phase 3 | See slide_design_principles.md §4 |
| Q&A backup slides | Phase 4 | Per template's anticipated-question profile |
| Pre-presentation checklist | Phase 4 | Run before final delivery |
Cross-references
slide_design_principles.md— design foundations (Reynolds, Duarte, Knaflic, Tufte)slide_visual_styles/nature_lancet.md— Nature/Lancet color/typography/layout spectemplates/build_pptx_nature_lancet.py— generic builder for Template #5critic_rubrics/slide.md— per-slide quality scoringworkflow-checklist.md— end-to-end Phase 0 → 4 production checklist~/.claude/rules/academic-lecture-style.md— English body + Korean notes + Nature/Lancet (global default)~/.claude/rules/pptx-mac-compatibility.md— TIFF / sp3d / app.xml / srcRect / Inches EMU / markdown notes~/.claude/rules/multidisciplinary-presentation.md— glossary slide + intuition box pattern~/.claude/rules/journal-ai-image-policies.md— AI-image policy (visual abstract /
Central Illustration cases)
Effective Slide Design for Medical Presentations
Primary sources (cite when invoking specific principles):
- Reynolds G., Presentation Zen: Simple Ideas on Presentation Design and Delivery, 3rd ed.
New Riders, 2019. — Simplicity / Clarity / Restraint / Harmony.
- Duarte N., Slide:ology: The Art and Science of Creating Great Presentations. O'Reilly, 2008.
— Glance Test™ (3-second rule); slides as "glance media."
- Knaflic CN., Storytelling with Data: A Data Visualization Guide for Business Professionals.
Wiley, 2015. — Preattentive attributes (size, color, position).
- Tufte ER., The Cognitive Style of PowerPoint: Pitching Out Corrupts Within, 2nd ed.
Graphics Press, 2006. — Chartjunk; data-ink ratio; bullet-point critique.
>
Medical-presentation specific:
- Bhargava R, et al. "Winning at the Radiology Podium: First-timer's Guide to Crafting and
Delivering a Memorable Conference Presentation." RadioGraphics 2025; DOI 10.1148/rg.250003.
- RSNA Refresher Course Committee guidelines (case-based, audience-response interactive format).
>
Triggered from: SKILL.md Phase 0 (Outline) and Phase 3 (Slides). Read this file before
sketching the slide outline — it shifts focus from "what content fits" to "what should the
audience remember 10 seconds after each slide."
Most slide-design guidance focuses on aesthetic templates (color, font, layout). This file adds a communication-first layer: how the audience parses a slide under time pressure, what they retain, and how the slide supports the speaker rather than replacing them.
---
The 5 design principles (read in order)
1. Identify the key message (most important)
Before opening PowerPoint or python-pptx, write one sentence describing what the slide must convey. If you cannot, the slide is premature. Examples:
- "Adjuvant RFA reduces local recurrence vs. surgery alone (HR 0.62)."
- "FLAIR sequence highlights the lesion that T2 missed."
- "The proposed CAD model maintains AUC across three external sites."
Pin that sentence at the top of the slide outline (Phase 0 of SKILL.md). Every bullet, figure, color, and animation exists to support that one sentence; anything that does not should be removed or moved to a separate slide.
A slide with two key messages is two slides.
2. Audience-aware reading-time budget
Different presentation contexts allow different inspection time per slide. Set the reading-time budget in Phase 0, then design backwards.
| Context | Reading time | Implication |
|---|---|---|
| Journal club (peer trainees) | 30–60 s/slide narrated | Methodology detail acceptable; figures from paper allowed |
| Grand rounds (mixed audience) | 20–40 s/slide narrated | Plain-language framing; one teaching point per slide |
| Conference talk (RSNA / ECR) | 15–30 s/slide narrated | Direct labels mandatory; data visuals over text |
| Lecture / didactic (residents) | 60–120 s/slide narrated | Worked examples; build-ups acceptable; recap slides |
| Visual abstract (no narration) | 5–10 s total | One panel; minimal text; readable thumbnail |
The same paper presented at journal club and at RSNA needs two different decks — the RSNA version usually drops half of what the journal-club version contains.
3. The Glance Test (Duarte) — 3-second rule
After each slide is drafted, perform the Glance Test™:
*Can the audience grasp the slide's meaning within 3 seconds, then return their attention
to the speaker?*
If a slide fails the test, the audience is reading the slide instead of listening to the speaker. People cannot read and listen simultaneously; one channel wins, and on slides the reading channel wins. Slides are glance media, not reading documents.
Failure modes that break the Glance Test:
- ≥ 6 lines of body text (audience starts reading paragraph-style).
- Two competing visuals (eye does not know where to land).
- A title that does not state the conclusion (forces reading the body to find the point).
Fix patterns:
- Convert the slide title from a topic ("Methods") to a sentence-summary ("Inclusion required
≥ 1 cm tumor on MRI").
- Split into 2 slides if the message is genuinely two-part.
- Replace bullets with one annotated figure.
4. Reduce cognitive load (Reynolds + Tufte)
Working memory holds ~7 items, and a slide must compete with the speaker's voice. Design under that ceiling.
| Element | Slide ceiling | Reason |
|---|---|---|
| Body text lines | ≤ 6 | Beyond 6, audience reads instead of listening |
| Distinct colors | ≤ 3 + 1 accent | More colors fragment attention |
| Fonts | ≤ 2 (title + body) | Mixed fonts break institutional brand and look unfinished |
| Bullet levels | ≤ 2 | Deep nesting (3 indents) signals reading-document |
| Chart types per slide | 1 | Two charts compete for attention |
| Animations | only purposeful | Decorative motion = noise |
Tufte's data-ink ratio applies: every pixel should encode either content or essential context. 3-D bars, drop shadows, gradient fills, and rotated axis labels add ink without data — remove them. The Columbia Space Shuttle disaster slide is the canonical counterexample: the O-ring failure data was buried in small type on a crowded slide.
5. Native objects, not images
Charts, tables, and diagrams must be editable native PowerPoint objects, not exported images. This is non-negotiable in academic medicine because:
- Co-authors edit numbers in revision rounds.
- Mac PowerPoint renders TIFF/SVG inconsistently (see
pptx-mac-compatibilityrule). - Reviewers / session chairs may project the deck on lower-resolution displays where
rasterized charts pixelate.
Specifically:
- Excel-linked charts → double-click opens the data table.
- Tables → native PowerPoint tables, not screenshots.
- Flow diagrams → grouped shapes, not a
make-figuresPNG export embedded as image. - Single fonts (Pretendard / Apple SD Gothic Neo for Korean decks; Helvetica/Calibri for
English-only) — declared once at the deck level, not per-slide.
Allowed exceptions: photographs, anatomical illustrations (SMART Servier, NIAID BioArt), medical imaging frames (CT/MRI slices) — all rasterized by nature.
---
Preattentive attributes (Knaflic) — directing audience attention
Preattentive attributes are visual properties the brain processes automatically (in <250 ms, before conscious attention). Use them to guide the audience to the part of the slide that matters.
| Attribute | Use for | Example |
|---|---|---|
| Size | Hierarchy: title > subtitle > body | Take-home line at 32 pt; body at 18 pt |
| Color saturation | Highlight 1 element vs. background | Key result row in orange; comparators in gray |
| Position | Left-to-right reading order; top = priority | Conclusion at top of slide, not bottom |
| Bold weight | Emphasize the key number in a sentence | "AUC = 0.91 (95% CI 0.87–0.94)" |
| Enclosure | Group related elements | Box around the "key finding" panel |
| Spatial separation | Group vs. distinguish | Whitespace between Methods and Results panels |
Two purposes: (1) draw immediate attention to a key element; (2) build a visual hierarchy that walks the eye through the content.
Anti-pattern: applying preattentive emphasis to everything — when 5 things are bold, red, and boxed, none of them stand out.
---
Slide-anti-patterns (cumulative — drawn from this skill's critic rubric)
These compose with the more granular checks in critic_rubrics/slide.md:
- Topic-titled slide — title says "Results" instead of stating the result. Fix: convert
to a sentence-headline ("RFA reduced local recurrence by 38%").
- Wall-of-text — > 6 body lines, full sentences. Fix: replace with one annotated figure
+ the take-home sentence at top.
- Bullet-cascade — 3-level nested bullets. Fix: split into 2 slides or use a 2-column
layout.
- Chart junk — 3-D bar, gradient fill, rotated axis label, secondary y-axis with no
shared scale. Fix: 2-D, single y-axis, direct labels.
- Image-of-table — pasted screenshot of a table from the paper. Fix: rebuild as native
PowerPoint table; keep the paper-table image for backup slides only.
- Reference-flood — citation list slide listing 15 references at 8 pt. Fix: cite
inline at point-of-use ([Smith 2024]); full bibliography in handout/supplementary.
- Logo-tax — institutional logos on every slide masking content. Fix: title slide and
closing slide only; corner-only on intermediate slides if mandated.
- AI-image without disclosure — AI-generated illustration on a slide destined for a
journal that prohibits AI imagery. Fix: see journal-ai-image-policies rule before building visual abstract / Central Illustration.
---
Cognitive load checklist (Phase 3 quick scan)
Before exporting to PPTX, walk every slide:
- [ ] One-sentence key message stated at slide top (sentence-headline).
- [ ] Body text ≤ 6 lines.
- [ ] Glance test ≤ 3 seconds.
- [ ] Colors ≤ 3 + 1 accent; carry meaning, not decoration.
- [ ] Fonts ≤ 2 throughout deck; ≥ 18 pt body for slides; ≥ 24 pt for posters.
- [ ] Charts/tables are native objects (not images).
- [ ] No 3-D / drop-shadow / gradient unless data-driven.
- [ ] Direct labels on chart series; legends only when ≥ 4 series.
- [ ] Same slide works in grayscale (run a
convert -colorspace Graytest). - [ ] AI-image policy verified for target audience/journal context.
If two or more boxes are unchecked, return to Phase 0 outline before continuing.
---
Cross-references
critic_rubrics/slide.md— quantitative critic checks per slidemedical_presentation_templates.md— section structure for journal club, grand rounds,
conference, lecture
workflow-checklist.md— end-to-end Phase 0 → 4 production checklistmake-figures/references/design_principles.md— figure-level design (this skill is the
slide-level companion; both share Reynolds/Knaflic/Tufte foundations)
~/.claude/rules/pptx-mac-compatibility.md— TIFF, sp3d, app.xml, srcRect defects~/.claude/rules/journal-ai-image-policies.md— AI-image policy (JACC prohibits, Radiology
allows with disclosure)
~/.claude/rules/manuscript-style-classical.md— heading style for slides paired with
manuscript submission
Slide Visual Style — Nature / Lancet
Design spec for academic lecture, journal club, and conference-talk slides modeled on the Nature / Lancet journal aesthetic. White background, navy primary, restrained coral accent, hairline dividers, two-font sans-serif system.
Triggered from: SKILL.md Phase 3 (Slides & Notes). Pair with
references/medical_presentation_templates.md Template #5 "Academic lecture(multi-paper)". Reference build script: templates/build_pptx_nature_lancet.py.---
1. Color palette
| Token | Hex | Usage |
|---|---|---|
NAVY | #1B2A4E | Primary text on white, section title, headline |
NAVY_LIGHT | #3F5A8C | Secondary navy (sub-headings, accents on dark bg) |
TEXT | #21252B | Body text |
TEXT_SUB | #4A525C | Sub-text, captions, subtitles |
MUTED | #8A929E | Footer, page brand, low-emphasis labels |
HAIRLINE | #CCD0D6 | Thin dividers, image borders |
HIGHLIGHT | #B83E3A | Coral accent — reserved for 1–2 emphasis points per slide |
WHITE | #FFFFFF | Backgrounds, dark-bg text |
BG_SOFT | #F8F9FB | Subtle panel background (optional) |
DIVIDER_BG | #121D36 | Section-divider slide background (deep navy) |
Rule of restraint: a content slide uses white + navy + 1 coral accent. Three colors maximum. Avoid full-bleed bright bars, gradients, drop shadows beyond the single 6pt offset on figure tiles.
2. Typography
| Role | Font (Latin) | Font (EastAsia / Korean) | Size | Weight |
|---|---|---|---|---|
| Slide title | Inter | Pretendard | 28–32 pt | Bold |
| Subtitle / sentence-headline | Inter | Pretendard | 15–18 pt | Italic |
| Body bullet (main) | Inter | Pretendard | 18–20 pt | Regular |
| Body bullet (sub) | Inter | Pretendard | 15–16 pt | Regular |
| Eyebrow text | Inter | Pretendard | 10–14 pt | Bold, letter-spaced 300–400 |
| Section title (divider) | Inter | Pretendard | 48–52 pt | Bold |
| Transition quote | Inter | Pretendard | 36 pt | Bold |
| Page-brand footer | Inter | Pretendard | 9 pt | Regular, letter-spaced 300 |
| Speaker notes | Inter | Pretendard | 13 pt | Regular |
Font install (macOS):
brew install --cask font-pretendard font-interEastAsia attribute must be set on every run that may contain Korean text — Mac PowerPoint falls back to Times New Roman otherwise. See ~/.claude/rules/pptx-mac-compatibility.md §6.
3. Layout grid (16:9, 13.333" × 7.5")
| Region | Position | Content |
|---|---|---|
| Eyebrow | x=0.7", y=0.32", w=8", h=0.4" | All-caps topic label (10–14pt, coral or muted) |
| Title | x=0.7", y=0.75", w=12.0", h=1.1" | Slide title + optional subtitle |
| Hairline | x=0.7", y=2.05", w=0.6", h≈0.02" | Coral hairline (separator) |
| Body (no figure) | x=0.7", y=2.4", w=12.0", h=4.6" | Bullets full width |
| Body (with figure) | x=0.7", y=2.4", w=6.8", h=4.6" | Bullets left half |
| Figure (right half) | x=7.9", y=2.4", w=5.0", h=4.0" | Centered within tile |
| Figure caption | x=7.9", y=fig+0.10", w=5.0", h=0.6" | "Figure · {caption}" centered |
| Footer (page brand) | x=0.7", y=7.05", w=4.0", h=0.35" | "2026 · NEUROGENETICS" pattern |
| Footnote | x=0.7", y=7.05", w=12.0", h=0.35" | Right-aligned source ref |
Margins: 0.7" left/right, 0.32" top eyebrow, 0.35" bottom footer. Title-body separation enforced by the 0.6" coral hairline (never a 0.05"-tall bar).
4. Slide type templates
4a. Title slide
- Left navy bar (40k EMU wide × 3.5" tall) at x=0.7"
- "REVIEW LECTURE" eyebrow (14pt, coral, letter-spaced 300)
- 48pt navy bold title
- 18pt italic subtitle (TEXT_SUB)
- Bottom block: 16pt bold navy line ("Course · Professor · Date"), 13pt sub line
("Presenter Name · Affiliation"), separated by 2" navy hairline above
4b. Section divider
- Full-bleed deep navy background (
DIVIDER_BG) - 1.8"-tall coral accent strip at x=1.2", y=3.0", 15k EMU wide
- "SECTION {N}" 18pt coral eyebrow letter-spaced 400
- 52pt white bold section title
- 20pt italic light-navy (
#C0CBDC) subtitle - Bottom-right "{N} MIN" badge (13pt muted)
4c. Transition slide
- Full-bleed deep navy background
- Large coral curly quotes (72pt) flanking a 36pt white bold single-sentence question
- Center-anchored vertically
4d. Content slide
- Eyebrow (small caps topic label) → title + subtitle → coral hairline → bullets
(left if figure, full-width if no figure) → figure tile (shadow offset 0.06", hairline border) → optional figure caption → footnote (right) + page brand (left)
- Bullet markers:
- Main:
▪(14pt coral bold) + 20pt body text - Sub (lines prefixed with 2 spaces):
—(15pt muted) + 16pt sub text - Inline
**bold**and*italic*markdown is parsed into per-run styling (see
pptx-mac-compatibility.md §4 for the parser pattern)
4e. TOC / outline slide
- Eyebrow "OUTLINE"
- Title "Outline" + sentence subtitle
- Coral hairline
- N rows, each: 22pt coral section number (or
·for wrap-up), 22pt navy bold section
title, 13pt sub-text English summary, 12pt muted time badge (right-aligned). Hairline divider between rows.
4f. Glossary slide (optional, for multidisciplinary audiences)
- Tier 1 (top, 4–7 items): disease/concept abbreviations with one-line context
- Tier 2 (bottom, 2-column, 8–12): method/statistics abbreviations with short defs
- See
~/.claude/rules/multidisciplinary-presentation.md§1
4g. Closing slide
- Title "Take-home messages" or "Conclusions"
- 3 bullet maximum
- Optional contact / acknowledgments block at bottom
5. Figure handling
- Aspect-ratio preserving fit inside the figure tile (5.0" × 4.0" max)
- Shadow rectangle (hairline gray) offset +0.06" / +0.06" before image
- Image border: hairline gray (~6000 EMU)
- Caption format:
Figure · {caption}(coral "Figure" eyebrow, italic muted caption)
EMU pitfall: compute width/height in inches before wrapping in Inches(). Never Inches(fig_w / aspect_ratio) when fig_w is already a pixel value — see pptx-mac-compatibility.md §7.
6. Speaker notes density
- Narrative speaker notes in the user's preferred language (a Korean narrative register is supported for Korean presenters, per
~/.claude/rules/academic-lecture-style.md§1) - About 150–300 characters per slide (30–60 seconds spoken)
- Run-level markdown parser for
**bold**/*italic*(see
pptx-mac-compatibility.md §4)
- 13pt Pretendard
7. Mac compatibility checklist (run before delivery)
| Check | Command |
|---|---|
| TIFF images | find ppt/media -iname '*.tif*' → must be empty |
| 3D bevel | grep -l '<a:sp3d>' ppt/slides/*.xml → must be empty |
| app.xml count sync | `unzip -p out.pptx docProps/app.xml \ |
| Raw markdown in notes | `unzip -p out.pptx ppt/notesSlides/notesSlide3.xml \ |
| Korean glyph in notes | `unzip -p out.pptx ppt/notesSlides/notesSlide3.xml \ |
Always validate on Mac PowerPoint + PDF export. PDF alone misses some defects (see pptx-mac-compatibility.md §1–§3).
8. Reference implementation
Canonical build script: templates/build_pptx_nature_lancet.py (in this skill).
Example output: 47-slide academic lecture deck (4-section narrative across multiple lighthouse papers + supporting references), Mac PowerPoint verified.
Cross-references
~/.claude/rules/academic-lecture-style.md— global style rule (English body + Korean notes + this design)~/.claude/rules/pptx-mac-compatibility.md— Mac compatibility (TIFF / sp3d / app.xml / Inches EMU / markdown notes)~/.claude/rules/multidisciplinary-presentation.md— glossary slide + intuition box patternreferences/medical_presentation_templates.mdTemplate #5 — Academic lecture (multi-paper)references/slide_design_principles.md— Reynolds / Duarte / Knaflic / Tufte foundations
Presentation Preparation Checklist
Use this checklist to verify completeness at each phase.
---
Phase 0: Init & Outline
Paper Analysis
- [ ] Full citation with DOI/PMID recorded
- [ ] Background gap clearly identified
- [ ] Study design classified (RCT, cohort, case series, etc.)
- [ ] Sample size and key inclusion/exclusion criteria noted
- [ ] Primary and secondary outcomes listed
- [ ] Key results with effect sizes and confidence intervals
- [ ] Limitations enumerated (minimum 3)
- [ ] Clinical/scientific significance stated
Audience Assessment
- [ ] Target audience specialties identified
- [ ] Knowledge level estimated (student / trainee / specialist / mixed)
- [ ] Specialty-specific terms that need plain-language explanation listed
- [ ] Prior session topics noted (for coursework presentations)
Outline
- [ ] Slide count matches presentation time (roughly 1 slide per minute)
- [ ] Time allocated per slide
- [ ] Narrative arc: context → gap → methods → results → implications
- [ ] Opening connects to audience's existing knowledge
- [ ] Closing has clear take-home messages (3-5 maximum)
---
Phase 1: Supporting Research
Search Completeness
- [ ] Follow-up studies searched (has finding been replicated?)
- [ ] Contradicting evidence searched (balance for Q&A)
- [ ] Review articles checked (authoritative framing)
- [ ] Clinical trial data checked (real-world context)
Citation Verification
- [ ] Every reference has DOI or PMID
- [ ] No fabricated citations
- [ ] Unverifiable references marked [UNVERIFIED]
Data Extraction
- [ ] Only presentation-relevant data extracted (not full paper summaries)
- [ ] Data source (reference number) linked to each statistic
- [ ] Numbers cross-checked against original papers
---
Phase 2: Script & Content
Audience Adaptation
- [ ] Technical terms explained at appropriate depth for audience
- [ ] Mixed-audience terms have one-line plain-language additions
- [ ] No unexplained abbreviations on first use
- [ ] Pronunciation guides for drug names and key abbreviations
Script Quality
- [ ] Script matches allocated time (130-150 words/min for academic talks)
- [ ] Transitions between slides are explicit
- [ ] Opening hooks audience attention
- [ ] Closing reinforces key messages
- [ ] Limitations acknowledged honestly (not hidden)
Optional Extension
- [ ] Extension section included only if user requested
- [ ] Extension connects naturally to main content
- [ ] Extension has concrete evidence (not speculation)
---
Phase 3: Slides & Notes
Note Injection
- [ ] Notes mapped to correct slide numbers
- [ ] Slide content NOT modified (only notes pane)
- [ ] Script verified by running inject script on a copy first
- [ ] Output file opened and spot-checked in presentation software
Content Consistency
- [ ] Numbers in notes match numbers on slides
- [ ] If slides round numbers (e.g., 12% vs 10.9%), notes use precise values
- [ ] Slide titles match script section references
---
Phase 4: Q&A Preparation
Question Coverage
- [ ] Methodology questions (study design choices, limitations)
- [ ] Domain expert questions (deep technical details)
- [ ] Generalist questions (clinical relevance, so-what)
- [ ] Student questions (concept clarification)
Answer Quality
- [ ] Each answer follows: Acknowledge → Evidence → Conclude
- [ ] Limitations acknowledged, not deflected
- [ ] Supporting evidence cited with author/year
- [ ] Answers are concise (30-60 seconds speaking time each)
Quick Review Sheet
- [ ] Key numbers in a single table
- [ ] Common confusion points listed
- [ ] Classification frameworks distinguished (if multiple exist)
- [ ] Precise values noted where slides use rounded numbers
#!/usr/bin/env python3
"""Extract figures from PDF pages via pdftoppm + PIL crop (normalized box coords).
Two usage modes:
1. **CLI single-crop**: extract one or more figures from specific pages of a PDF
using normalized (0–1) crop boxes.
extract_pdf_figures.py paper.pdf \\
--page 5 --crop 0.10,0.05,0.95,0.55 --out fig01.png
extract_pdf_figures.py paper.pdf \\
--pages 5,6 \\
--crops "0.10,0.05,0.95,0.55;0.05,0.05,0.95,0.50" \\
--out fig01.png,fig02.png
2. **YAML config batch**: extract many figures across multiple PDFs declaratively.
extract_pdf_figures.py --config figures.yaml --out-dir extracted/
YAML schema:
dpi: 250 # default 250; override per item with item.dpi
pdf_dir: /abs/path/to/pdfs/ # optional base path resolved per item.pdf
items:
- name: fig01_concept_overview
pdf: 01_paper.pdf # joined with pdf_dir if relative
page: 4 # 1-indexed
crop: [0.06, 0.08, 0.97, 0.52] # [left, top, right, bottom] normalized
caption: "Conceptual overview (Author YYYY Fig 1)" # optional, only logged
Requires: pdftoppm (poppler-utils — `brew install poppler`), Pillow.
Optional: PyYAML for --config mode (`pip install pyyaml`).
Cross-references:
- /present-paper Phase 3 (Slides & Notes)
- references/slide_visual_styles/nature_lancet.md §5 (figure handling)
- ~/.claude/rules/pptx-mac-compatibility.md §7 (Inches EMU pitfall)
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Iterable
try:
from PIL import Image
except ImportError:
sys.stderr.write("ERROR: Pillow is required. Install: pip install Pillow\n")
raise
def _parse_crop(s: str) -> tuple[float, float, float, float]:
"""Parse 'l,t,r,b' string into normalized box tuple. Validates 0 <= l<r <=1, t<b."""
parts = s.split(",")
if len(parts) != 4:
raise ValueError(f"crop must be 4 comma-separated floats, got: {s!r}")
l, t, r, b = (float(x) for x in parts)
for label, v in (("left", l), ("top", t), ("right", r), ("bottom", b)):
if not 0.0 <= v <= 1.0:
raise ValueError(f"crop {label}={v} not in [0,1]")
if not (l < r and t < b):
raise ValueError(f"crop must satisfy left<right and top<bottom; got {s!r}")
return (l, t, r, b)
def _render_page(pdf: Path, page: int, dpi: int, tmp_dir: Path) -> Path:
"""Render a single PDF page to PNG using pdftoppm; return resulting file path."""
if not pdf.exists():
raise FileNotFoundError(f"PDF not found: {pdf}")
out_prefix = tmp_dir / f"page_{page}"
cmd = [
"pdftoppm",
"-png",
"-r", str(dpi),
"-f", str(page),
"-l", str(page),
str(pdf),
str(out_prefix),
]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(
f"pdftoppm failed for {pdf.name} page {page}:\n{proc.stderr.strip()}"
)
# pdftoppm writes "<prefix>-<padded_page>.png" — width varies with total pages.
matches = sorted(tmp_dir.glob(f"page_{page}-*.png"))
if not matches:
raise RuntimeError(
f"pdftoppm produced no output for {pdf.name} page {page} "
f"(expected page_{page}-*.png in {tmp_dir})"
)
return matches[0]
def _crop_image(src: Path, box: tuple[float, float, float, float], dst: Path) -> tuple[int, int]:
"""Crop src image by normalized box and save to dst. Returns (w, h) of cropped image."""
img = Image.open(src)
W, H = img.size
l, t, r, b = box
cropped = img.crop((int(W * l), int(H * t), int(W * r), int(H * b)))
dst.parent.mkdir(parents=True, exist_ok=True)
cropped.save(dst, "PNG", optimize=True)
return cropped.size
def extract_one(pdf: Path, page: int, crop: tuple[float, float, float, float],
out: Path, dpi: int = 250) -> Path:
"""Single-figure extraction. Returns the output path."""
with tempfile.TemporaryDirectory() as td:
page_png = _render_page(pdf, page, dpi, Path(td))
w, h = _crop_image(page_png, crop, out)
print(f"OK {out.name:50s} ({w}x{h}) {pdf.name} page {page}")
return out
def extract_batch(items: Iterable[dict], out_dir: Path, default_dpi: int = 250,
pdf_dir: Path | None = None) -> list[Path]:
"""Batch extraction from YAML config. Caches rendered pages per (pdf, page, dpi).
Each item dict accepts:
name (str, required): output filename stem (.png appended)
pdf (str, required): PDF path (joined with pdf_dir if relative)
page (int, required): 1-indexed PDF page number
crop (list[float], required): [left, top, right, bottom] normalized
dpi (int, optional): override default_dpi
caption (str, optional): logged only
"""
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
results: list[Path] = []
with tempfile.TemporaryDirectory() as td:
td_path = Path(td)
cache: dict[tuple[str, int, int], Path] = {}
for item in items:
try:
name = item["name"]
pdf_str = item["pdf"]
page = int(item["page"])
crop = tuple(float(x) for x in item["crop"])
if len(crop) != 4:
raise ValueError("crop must have 4 elements")
dpi = int(item.get("dpi", default_dpi))
except (KeyError, ValueError, TypeError) as e:
print(f"SKIP item {item!r}: {e}")
continue
pdf = Path(pdf_str)
if pdf_dir is not None and not pdf.is_absolute():
pdf = pdf_dir / pdf
cache_key = (str(pdf), page, dpi)
if cache_key not in cache:
try:
cache[cache_key] = _render_page(pdf, page, dpi, td_path)
except (FileNotFoundError, RuntimeError) as e:
print(f"SKIP {name}: {e}")
continue
dst = out_dir / f"{name}.png"
try:
w, h = _crop_image(cache[cache_key], crop, dst)
except ValueError as e:
print(f"SKIP {name}: {e}")
continue
caption = item.get("caption", "")
tail = f" {caption}" if caption else ""
print(f"OK {dst.name:50s} ({w}x{h}) {pdf.name} page {page}{tail}")
results.append(dst)
print(f"\nTotal: {len(results)} figures in {out_dir}")
return results
def _cli() -> int:
p = argparse.ArgumentParser(
description="Extract figures from PDF pages (pdftoppm + PIL crop).",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
p.add_argument("pdf", nargs="?", help="PDF input (single-crop mode); omit for --config mode")
p.add_argument("--page", type=int, help="single-crop: 1-indexed page number")
p.add_argument("--pages", help="single-crop: comma-separated pages (paired with --crops)")
p.add_argument("--crop", help="single-crop: 'l,t,r,b' normalized box")
p.add_argument("--crops", help="single-crop: ';'-separated 'l,t,r,b' boxes paired with --pages")
p.add_argument("--out", help="single-crop: output path (or comma-separated paths)")
p.add_argument("--out-dir", help="batch mode: output directory")
p.add_argument("--dpi", type=int, default=250, help="render DPI (default 250)")
p.add_argument("--config", help="batch mode: YAML config path")
args = p.parse_args()
# Batch (YAML) mode
if args.config:
try:
import yaml # type: ignore
except ImportError:
sys.stderr.write("ERROR: --config requires PyYAML. Install: pip install pyyaml\n")
return 2
cfg_path = Path(args.config)
if not cfg_path.exists():
sys.stderr.write(f"ERROR: config not found: {cfg_path}\n")
return 2
cfg = yaml.safe_load(cfg_path.read_text()) or {}
items = cfg.get("items", [])
out_dir = Path(args.out_dir or cfg.get("out_dir", "extracted"))
dpi = int(args.dpi if args.dpi != 250 else cfg.get("dpi", 250))
pdf_dir = cfg.get("pdf_dir")
pdf_dir = Path(pdf_dir) if pdf_dir else None
extract_batch(items, out_dir, default_dpi=dpi, pdf_dir=pdf_dir)
return 0
# Single-crop mode (single or paired)
if not args.pdf:
p.error("pdf positional argument required for single-crop mode (or use --config)")
pdf = Path(args.pdf)
if args.crops or args.pages:
if not (args.crops and args.pages and args.out):
p.error("--pages, --crops, and --out must all be set together")
pages = [int(x) for x in args.pages.split(",")]
crops = [_parse_crop(c) for c in args.crops.split(";")]
outs = [Path(o.strip()) for o in args.out.split(",")]
if not (len(pages) == len(crops) == len(outs)):
p.error(
f"counts mismatch: {len(pages)} pages, {len(crops)} crops, {len(outs)} outs"
)
for pg, cb, op in zip(pages, crops, outs):
extract_one(pdf, pg, cb, op, dpi=args.dpi)
return 0
if not (args.page is not None and args.crop and args.out):
p.error("single crop requires --page, --crop, --out")
box = _parse_crop(args.crop)
extract_one(pdf, args.page, box, Path(args.out), dpi=args.dpi)
return 0
if __name__ == "__main__":
sys.exit(_cli())
#!/usr/bin/env python3
"""Append a pronunciation guide section to every slide's speaker notes.
Designed for non-native presenters who want a per-slide reading reference
without affecting the audience-facing view. Scans each slide's notes text
for tokens defined in a YAML/JSON ``PRON_DICT`` file (term → reading +
full-name), uses word-boundary regex to avoid false positives, and writes
a "[ 발음 ]" (or user-defined) section at the bottom of the notes.
Also auto-matches allele-style tokens (HLA-DRB1*07:01, HLA-A*02:01, …)
that match a configurable regex and synthesizes a reading by combining
the base reading from PRON_DICT with "스타 NN콜론NN".
This script is invocation-agnostic: it modifies a PPTX in place (or to a
new path) and does not assume any specific lecture topic. The PRON_DICT
file is supplied by the caller and is the only language/domain config.
Usage
-----
PRON_DICT is YAML or JSON. Each key is the term as it appears in the
notes text. Each value is a 2-tuple [reading, full_name].
```yaml
# pron_dict.yaml
GWAS: ["지와스", "Genome-wide association study"]
HLA: ["에이치-엘-에이", "Human leukocyte antigen"]
LGI1: ["엘-지-아이-원", "Leucine-rich glioma-inactivated 1"]
Perriot: ["페리오", "프랑스, t 묵음"]
```
```bash
python3 inject_pronunciation_notes.py input.pptx output.pptx \
--dict pron_dict.yaml --header "[ 발음 ]"
```
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import sys
from pathlib import Path
from pptx import Presentation
from pptx.util import Pt
DEFAULT_ALLELE_RE = r"\b(?:HLA-)?[A-Z]{1,5}[0-9]?\*[0-9]{2}:[0-9]{2}(?:N|L|S|Q)?\b"
DEFAULT_HEADER = "[ Pronunciation ]"
def load_dict(path: Path) -> dict:
if path.suffix.lower() in (".yaml", ".yml"):
try:
import yaml # type: ignore
except ImportError:
raise SystemExit("PyYAML required for YAML dict; pip install pyyaml")
with path.open() as f:
raw = yaml.safe_load(f)
elif path.suffix.lower() == ".json":
with path.open() as f:
raw = json.load(f)
else:
raise SystemExit(f"unsupported dict format: {path.suffix}")
out = {}
for term, value in raw.items():
if isinstance(value, (list, tuple)) and len(value) >= 1:
reading = value[0]
fullname = value[1] if len(value) > 1 else ""
elif isinstance(value, str):
reading, fullname = value, ""
else:
continue
out[term] = (reading, fullname)
return out
def find_terms(text: str, pron_dict: dict, allele_re: str | None):
"""Return [(term, reading, fullname)] for every dict key appearing in *text*."""
if not text:
return []
hits = []
seen = set()
for term, (reading, fullname) in pron_dict.items():
pat = r"(?<![A-Za-z0-9_])" + re.escape(term) + r"(?![A-Za-z0-9_])"
if re.search(pat, text):
if term not in seen:
hits.append((term, reading, fullname))
seen.add(term)
if allele_re:
allele_pattern = re.compile(allele_re)
alleles = sorted(set(allele_pattern.findall(text)))
for a in alleles:
if a in seen:
continue
base = a.replace("HLA-", "").split("*")[0]
tail = a.split("*", 1)[1] if "*" in a else ""
base_reading = pron_dict.get(base, (base.lower(), ""))[0]
reading = f"{base_reading} star {tail.replace(':', ' colon ')}"
hits.append((a, reading, "HLA allele"))
seen.add(a)
return hits
def inject_notes(slide, terms, header: str):
if not terms:
return False
tf = slide.notes_slide.notes_text_frame
# blank separator
p = tf.add_paragraph()
r = p.add_run(); r.text = " "
r.font.size = Pt(11)
# header
p = tf.add_paragraph()
r = p.add_run(); r.text = header
r.font.bold = True
r.font.size = Pt(12)
# each term: "▪ term — reading · fullname"
for term, reading, fullname in terms:
p = tf.add_paragraph()
r = p.add_run(); r.text = f"▪ {term}"
r.font.bold = True
r.font.size = Pt(11)
r2 = p.add_run(); r2.text = f" — {reading}"
r2.font.size = Pt(11)
if fullname:
r3 = p.add_run(); r3.text = f" · {fullname}"
r3.font.italic = True
r3.font.size = Pt(11)
return True
def main():
ap = argparse.ArgumentParser()
ap.add_argument("src", type=Path)
ap.add_argument("dst", type=Path)
ap.add_argument("--dict", required=True, type=Path,
help="path to pron_dict.yaml or pron_dict.json")
ap.add_argument("--header", default=DEFAULT_HEADER,
help="section header text (default: %(default)s)")
ap.add_argument("--allele-regex", default=DEFAULT_ALLELE_RE,
help="regex for allele-style tokens (set to empty string to disable)")
args = ap.parse_args()
if not args.src.exists():
print(f"source not found: {args.src}", file=sys.stderr)
sys.exit(1)
if args.src != args.dst:
shutil.copy(args.src, args.dst)
pron_dict = load_dict(args.dict)
print(f"loaded {len(pron_dict)} terms from {args.dict}")
allele_re = args.allele_regex if args.allele_regex else None
prs = Presentation(args.dst)
n_injected = 0
n_terms_total = 0
for slide in prs.slides:
if not slide.has_notes_slide:
continue
body = slide.notes_slide.notes_text_frame.text
if args.header in body:
continue # already injected on a previous run
terms = find_terms(body, pron_dict, allele_re)
if inject_notes(slide, terms, args.header):
n_injected += 1
n_terms_total += len(terms)
prs.save(args.dst)
print(f"injected pronunciation on {n_injected} slides ({n_terms_total} term entries)")
print(f"OK: {args.dst}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Inject speaker notes into PowerPoint presentation slides.
This script adds or replaces speaker notes in a PPTX file without modifying
slide content, layout, or design. Notes are defined as a dictionary mapping
slide numbers (1-indexed) to note text.
Inline ``**bold**`` / ``*italic*`` in a note is parsed into run-level styling so the
asterisks do not show literally in Presenter View (python-pptx stores text verbatim).
Use --no-markdown for the legacy plain-text behavior.
Usage:
python inject_speaker_notes.py input.pptx
python inject_speaker_notes.py input.pptx -o output.pptx
python inject_speaker_notes.py input.pptx --append
python inject_speaker_notes.py input.pptx --dry-run
python inject_speaker_notes.py input.pptx --no-markdown
Requirements:
pip install python-pptx
License: MIT
"""
import argparse
import re
import sys
from pathlib import Path
try:
from pptx import Presentation
except ImportError:
print("Error: python-pptx is required. Install with: pip install python-pptx")
sys.exit(1)
# ---------------------------------------------------------------------------
# Speaker notes dictionary
# Map slide number (1-indexed) to note text.
# Empty string or missing key = skip that slide.
# ---------------------------------------------------------------------------
notes: dict[int, str] = {
# 1: """Speaker note for slide 1.""",
# 2: """Speaker note for slide 2.""",
}
# ---------------------------------------------------------------------------
# Inline-markdown rendering for notes.
# python-pptx stores ``notes_text_frame.text = "**bold**"`` VERBATIM, so the
# asterisks show literally in Presenter View. Parse ``**bold**`` / ``*italic*``
# (non-nested) into run-level styling instead. (Opt out with --no-markdown.)
# ---------------------------------------------------------------------------
_MD_INLINE = re.compile(r"(\*\*[^*\n]+\*\*|\*[^*\n]+\*)")
def _add_markdown_line(paragraph, line: str) -> None:
"""Emit one note line as styled runs, parsing **bold** / *italic*.
Non-nested by design (matches the ``add_styled_note_line`` convention in
``pptx-speaker-notes.md``): malformed/mixed markers such as ``***x***`` render
partially rather than nesting — text is never dropped and never raises.
"""
if not line:
paragraph.add_run().text = ""
return
for part in _MD_INLINE.split(line):
if not part:
continue
run = paragraph.add_run()
if part.startswith("**") and part.endswith("**") and len(part) > 4:
run.text = part[2:-2]
run.font.bold = True
elif (part.startswith("*") and part.endswith("*")
and not part.startswith("**") and len(part) > 2):
run.text = part[1:-1]
run.font.italic = True
else:
run.text = part
def _render_notes_markdown(tf, text: str, append: bool) -> None:
"""Write text into the notes text frame with inline-markdown run styling.
Preserves the line structure (one paragraph per line). With ``append`` and
existing notes, inserts a ``---`` separator paragraph first.
"""
lines = text.split("\n")
if append and tf.text.strip():
sep = tf.add_paragraph()
sep.add_run().text = "---"
for ln in lines:
_add_markdown_line(tf.add_paragraph(), ln)
else:
tf.clear() # leaves a single empty paragraph
_add_markdown_line(tf.paragraphs[0], lines[0])
for ln in lines[1:]:
_add_markdown_line(tf.add_paragraph(), ln)
def inject_notes(
input_path: str,
output_path: str | None = None,
append: bool = False,
dry_run: bool = False,
markdown: bool = True,
) -> None:
"""Inject speaker notes into a PPTX file.
Args:
input_path: Path to input PPTX file.
output_path: Path to output PPTX file. Defaults to input with _notes suffix.
append: If True, append to existing notes instead of replacing.
dry_run: If True, print what would be done without saving.
"""
input_file = Path(input_path)
if not input_file.exists():
print(f"Error: {input_file} not found")
sys.exit(1)
if output_path is None:
output_file = input_file.with_stem(input_file.stem + "_notes")
else:
output_file = Path(output_path)
prs = Presentation(str(input_file))
total_slides = len(prs.slides)
updated = 0
for i, slide in enumerate(prs.slides, 1):
if i not in notes or not notes[i]:
continue
if dry_run:
preview = notes[i][:80].replace("\n", " ")
print(f" Slide {i:2d}: would {'append' if append else 'set'} → {preview}...")
updated += 1
continue
if not slide.has_notes_slide:
slide.notes_slide # creates notes slide
tf = slide.notes_slide.notes_text_frame
if markdown:
_render_notes_markdown(tf, notes[i], append)
elif append and tf.text.strip():
tf.text = tf.text + "\n\n---\n\n" + notes[i]
else:
tf.text = notes[i]
updated += 1
if dry_run:
print(f"\nDry run: {updated}/{total_slides} slides would be updated")
return
prs.save(str(output_file))
print(f"Done: {output_file} ({updated}/{total_slides} slides updated)")
def main() -> None:
parser = argparse.ArgumentParser(
description="Inject speaker notes into PowerPoint slides",
epilog="Notes are defined in the 'notes' dictionary in this script.",
)
parser.add_argument("input", help="Input PPTX file")
parser.add_argument(
"-o", "--output",
help="Output PPTX file (default: input with _notes suffix)",
)
parser.add_argument(
"--append",
action="store_true",
help="Append to existing notes instead of replacing",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print what would be done without saving",
)
parser.add_argument(
"--no-markdown",
action="store_true",
help="Disable inline-markdown parsing (write **bold**/*italic* verbatim — legacy)",
)
args = parser.parse_args()
if not notes:
print("Warning: notes dictionary is empty. Edit this script to add notes.")
print("Example:")
print(' notes = {')
print(' 1: """Your note for slide 1.""",')
print(' 2: """Your note for slide 2.""",')
print(' }')
sys.exit(0)
inject_notes(args.input, args.output, args.append, args.dry_run,
markdown=not args.no_markdown)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Strip all speaker notes from a PPTX file (sharing-ready variant).
Use case: after preparing an academic lecture deck where the speaker notes
contain presenter-only material (foreign-language narrative, pronunciation
hints, self-referential reminders), generate a clean copy whose notes are
empty so the deck can be circulated to the audience or to a senior reviewer
without leaking presenter-only content.
The slide body, figures, layout, and pronunciation/asterisk-bearing strings
(e.g. HLA alleles) are preserved exactly — only the notes_text_frame of
every slide is cleared. Also re-writes ``docProps/app.xml`` so the
PowerPoint Mac repair dialog is not triggered.
Usage:
python3 strip_notes_for_sharing.py INPUT.pptx OUTPUT.pptx
"""
from __future__ import annotations
import argparse
import shutil
import sys
import zipfile
from pathlib import Path
from pptx import Presentation
def strip_notes(src: Path, dst: Path) -> dict:
if src == dst:
raise SystemExit("source and destination must differ")
shutil.copy(src, dst)
prs = Presentation(dst)
cleared = 0
for slide in prs.slides:
if slide.has_notes_slide:
tf = slide.notes_slide.notes_text_frame
if tf.text.strip():
cleared += 1
tf.clear()
prs.save(dst)
return {"cleared": cleared, "total_slides": len(prs.slides)}
def fix_app_xml(pptx_path: Path) -> dict:
"""Re-write docProps/app.xml so Slides/Notes counts match reality.
python-pptx leaves Slides=0 on save, which PowerPoint Mac flags as a
repair condition. Match canonical pattern in pptx-mac-compatibility.md §5.
"""
prs = Presentation(pptx_path)
n_slides = len(prs.slides)
with zipfile.ZipFile(pptx_path, "r") as z:
n_notes = sum(
1 for x in z.namelist()
if x.startswith("ppt/notesSlides/notesSlide") and x.endswith(".xml")
)
titles = []
for slide in prs.slides:
title = ""
for shape in slide.shapes:
if shape.has_text_frame and shape.text_frame.text.strip():
title = shape.text_frame.text.strip().split("\n")[0][:60]
break
titles.append(title or "Untitled")
def esc(t: str) -> str:
return t.replace("&", "&").replace("<", "<").replace(">", ">")
title_items = "".join(f"<vt:lpstr>{esc(t)}</vt:lpstr>" for t in titles)
new_app_xml = (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" '
'xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">'
"<TotalTime>1</TotalTime><Words>0</Words>"
"<Application>Microsoft Macintosh PowerPoint</Application>"
"<PresentationFormat>Widescreen</PresentationFormat><Paragraphs>0</Paragraphs>"
f"<Slides>{n_slides}</Slides><Notes>{n_notes}</Notes>"
"<HiddenSlides>0</HiddenSlides><MMClips>0</MMClips><ScaleCrop>false</ScaleCrop>"
'<HeadingPairs><vt:vector size="4" baseType="variant">'
"<vt:variant><vt:lpstr>Theme</vt:lpstr></vt:variant>"
"<vt:variant><vt:i4>1</vt:i4></vt:variant>"
"<vt:variant><vt:lpstr>Slide Titles</vt:lpstr></vt:variant>"
f"<vt:variant><vt:i4>{n_slides}</vt:i4></vt:variant>"
"</vt:vector></HeadingPairs>"
f'<TitlesOfParts><vt:vector size="{n_slides + 1}" baseType="lpstr">'
f"<vt:lpstr>Office Theme</vt:lpstr>{title_items}"
"</vt:vector></TitlesOfParts>"
"<Manager></Manager><Company></Company><LinksUpToDate>false</LinksUpToDate>"
"<SharedDoc>false</SharedDoc><HyperlinkBase></HyperlinkBase>"
"<HyperlinksChanged>false</HyperlinksChanged><AppVersion>14.0000</AppVersion>"
"</Properties>"
)
tmp = pptx_path.with_suffix(".tmp.pptx")
with zipfile.ZipFile(pptx_path, "r") as zin, \
zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.namelist():
zout.writestr(item, new_app_xml if item == "docProps/app.xml" else zin.read(item))
shutil.move(tmp, pptx_path)
return {"slides": n_slides, "notes": n_notes}
def verify(pptx_path: Path) -> int:
"""Return the total number of non-whitespace characters left in any notes."""
prs = Presentation(pptx_path)
total = 0
for slide in prs.slides:
if slide.has_notes_slide:
total += len(slide.notes_slide.notes_text_frame.text.strip())
return total
def main():
ap = argparse.ArgumentParser()
ap.add_argument("src", type=Path)
ap.add_argument("dst", type=Path)
ap.add_argument("--no-app-xml-fix", action="store_true",
help="skip the app.xml rewrite (PowerPoint Mac may show repair dialog)")
args = ap.parse_args()
if not args.src.exists():
print(f"source not found: {args.src}", file=sys.stderr)
sys.exit(1)
info = strip_notes(args.src, args.dst)
print(f"cleared notes on {info['cleared']} / {info['total_slides']} slides")
if not args.no_app_xml_fix:
xml_info = fix_app_xml(args.dst)
print(f"app.xml patched: Slides={xml_info['slides']}, Notes={xml_info['notes']}")
leftover = verify(args.dst)
if leftover > 0:
print(f"WARNING: {leftover} chars of notes still remain", file=sys.stderr)
sys.exit(2)
print(f"OK: {args.dst}")
if __name__ == "__main__":
main()
schema_version: 2
name: present-paper
layer: C
owner_domain: presentation
maturity: official
when_to_use: "Prepare academic presentations (journal club, grand rounds, seminar, lecture/teaching decks): analyze source, find references, draft audience-adapted scripts, and generate/augment PPTX with speaker notes."
when_NOT_to_use: "Drafting a manuscript (use write-paper); building figures for a paper (use make-figures)."
inputs:
- "source paper(s) / topic"
- "audience and format"
outputs:
- "PPTX deck"
- "speaker notes"
- "Q&A prep"
deterministic_scripts:
- scripts/inject_speaker_notes.py
- scripts/trim_caption.py
- scripts/extract_pdf_figures.py
- scripts/inject_pronunciation_notes.py
- scripts/strip_notes_for_sharing.py
side_effects:
- writes_pptx_artifacts
downstream_consumers:
- none
forbidden_actions:
- fabricate_findings_not_in_source
- leave_speaker_notes_in_shared_deck
# v2.1 quality card
purpose: "Turn source papers into an audience-adapted deck with speaker notes, Mac-compatible PPTX, and a sharing-stripped variant."
safety_boundaries:
- "Slide claims trace to the source material; findings are not invented for narrative effect."
- "A notes-stripped variant is produced for sharing so private speaker notes never leak."
known_limitations:
- "Figure cropping and notes parsing are heuristic; verify the built PPTX in PowerPoint."
- "Mac OOXML quirks require the bundled compatibility checks; not every host renders identically."
validation_commands:
- "unzip the .pptx and confirm 0 markdown-raw notes / 0 TIFF / app.xml counts synced"
- "python3 scripts/strip_notes_for_sharing.py before sharing"
evidence_surface: bundled_script
#!/usr/bin/env python3
"""Reproducible test for inject_speaker_notes.py inline-markdown rendering.
Builds a 1-slide PPTX, injects a note containing **bold** / *italic*, and asserts the
asterisks are gone and the runs are styled. --no-markdown must keep the text verbatim.
Skips cleanly (exit 0) when python-pptx is not installed. Network-free.
python3 skills/present-paper/tests/test_speaker_notes_markdown.py
"""
import importlib.util
import sys
import tempfile
from pathlib import Path
try:
from pptx import Presentation
except ImportError:
print("python-pptx not installed — SKIP (compile-only)")
sys.exit(0)
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "inject_speaker_notes.py"
spec = importlib.util.spec_from_file_location("isn", SCRIPT)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
tmp = Path(tempfile.mkdtemp())
prs = Presentation()
prs.slides.add_slide(prs.slide_layouts[6])
src = tmp / "in.pptx"
prs.save(str(src))
m.notes = {1: "Plain **bold** and *italic* here.\nSecond line."}
out = tmp / "in_notes.pptx"
m.inject_notes(str(src), str(out), markdown=True)
tf = Presentation(str(out)).slides[0].notes_slide.notes_text_frame
runs = [(r.text, r.font.bold, r.font.italic) for p in tf.paragraphs for r in p.runs]
out2 = tmp / "legacy.pptx"
m.inject_notes(str(src), str(out2), markdown=False)
tf2 = Presentation(str(out2)).slides[0].notes_slide.notes_text_frame
checks = [
("no literal '**' in rendered notes", "**" not in tf.text),
("no literal '*' in rendered notes", "*" not in tf.text),
("a bold run 'bold' exists", any(t == "bold" and b for t, b, i in runs)),
("an italic run 'italic' exists", any(t == "italic" and i for t, b, i in runs)),
("line structure preserved (>=2 paragraphs)", len(tf.paragraphs) >= 2),
("--no-markdown keeps '**bold**' verbatim", "**bold**" in tf2.text),
]
fail = 0
for name, ok in checks:
print(("ok " if ok else "FAIL ") + name)
fail += 0 if ok else 1
print("ALL SPEAKER-NOTES MARKDOWN TESTS PASSED" if not fail else f"{fail} FAILED")
sys.exit(1 if fail else 0)
Related skills
FAQ
What are the five phases?
Paper analysis, supporting research, script writing, slide-note injection, and Q&A preparation.
What design principles does it use?
It loads references drawn from Reynolds (Presentation Zen), Duarte, Knaflic (Storytelling with Data), and Tufte to control cognitive load per slide.