skilldrop-cli 0.13.5 → 0.13.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/bin/skilldrop.js +17 -5
- package/catalogue.json +19 -1
- package/contracts/catalogue.schema.json +1 -0
- package/model-routing.json +8 -0
- package/package.json +1 -1
- package/packs/design/pack.json +22 -0
- package/packs/design/skills/brand-kit/SKILL.md +105 -0
- package/packs/design/skills/brand-kit/evals/eval_queries.json +30 -0
- package/packs/design/skills/brand-kit/evals/evals.json +18 -0
- package/packs/design/skills/brand-kit/examples/northwind-brand.md +48 -0
- package/packs/design/skills/brand-kit/manifest.json +44 -0
- package/packs/design/skills/brand-kit/scripts/check_brand.py +114 -0
- package/packs/design/skills/brand-kit/templates/brand.json +54 -0
- package/packs/{stakeholder-comms → design}/skills/deck-builder/SKILL.md +27 -9
- package/packs/{stakeholder-comms → design}/skills/deck-builder/manifest.json +2 -2
- package/packs/{stakeholder-comms → design}/skills/deck-builder/scripts/build_deck.py +105 -3
- package/packs/{stakeholder-comms → design}/skills/deck-builder/templates/deck-spec.json +4 -1
- package/packs/design/skills/marketing-flyer/SKILL.md +113 -0
- package/packs/design/skills/marketing-flyer/evals/eval_queries.json +30 -0
- package/packs/design/skills/marketing-flyer/evals/evals.json +18 -0
- package/packs/design/skills/marketing-flyer/examples/community-health-fair.md +68 -0
- package/packs/design/skills/marketing-flyer/manifest.json +39 -0
- package/packs/design/skills/marketing-flyer/reference.md +54 -0
- package/packs/design/skills/marketing-flyer/scripts/build_flyer.py +323 -0
- package/packs/design/skills/marketing-flyer/templates/flyer-spec.json +26 -0
- package/packs/stakeholder-comms/pack.json +1 -1
- package/profiles.json +1 -1
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/evals/eval_queries.json +0 -0
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/evals/evals.json +0 -0
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/examples/exec-board-update.md +0 -0
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/examples/templated-brand-deck.md +0 -0
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/reference.md +0 -0
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/requirements.txt +0 -0
- /package/packs/{stakeholder-comms → design}/skills/deck-builder/templates/palettes.json +0 -0
- /package/packs/{stakeholder-comms → design}/skills/slide-outliner/SKILL.md +0 -0
- /package/packs/{stakeholder-comms → design}/skills/slide-outliner/evals/eval_queries.json +0 -0
- /package/packs/{stakeholder-comms → design}/skills/slide-outliner/evals/evals.json +0 -0
- /package/packs/{stakeholder-comms → design}/skills/slide-outliner/manifest.json +0 -0
- /package/packs/{stakeholder-comms → design}/skills/slide-outliner/templates/deck-outline.md +0 -0
|
@@ -7,10 +7,11 @@ description: Generate a real PowerPoint (.pptx) file from content, audience, and
|
|
|
7
7
|
|
|
8
8
|
This skill produces an **actual editable `.pptx` file**, not an outline. It runs `python-pptx`
|
|
9
9
|
to materialise slides — either on a brand template's own masters and layouts, or on the skill's
|
|
10
|
-
built-in design
|
|
10
|
+
built-in design in the user's brand: their colours, logo and fonts.
|
|
11
11
|
|
|
12
|
-
It pairs with
|
|
13
|
-
- **[`
|
|
12
|
+
It pairs with three others:
|
|
13
|
+
- **[`brand-kit`](../brand-kit/SKILL.md)** — captures the brand once as a `brand.json` this skill reads, so nobody answers the brand questions for every deck
|
|
14
|
+
- **[`audience-profile`](../../../stakeholder-comms/skills/audience-profile/SKILL.md)** — decides how dense, how many, which sections
|
|
14
15
|
- **[`slide-outliner`](../slide-outliner/SKILL.md)** — drafts per-slide content before this skill builds it
|
|
15
16
|
|
|
16
17
|
For a single fast pipeline, run all three in order: profile → outline → build.
|
|
@@ -26,9 +27,11 @@ with the design silently defaulted.
|
|
|
26
27
|
> Before I build, confirm these — reply "go" to take the defaults:
|
|
27
28
|
> 1. **Audience** — `exec` · `board` · `technical` · `sales` · `internal` · `investor` · `partner` · `customer`
|
|
28
29
|
> 2. **Format** — live presentation / read-ahead / async share, **how many minutes**, and **16:9** (default) or **4:3**
|
|
29
|
-
> 3. **
|
|
30
|
-
> - a
|
|
31
|
-
> - a **
|
|
30
|
+
> 3. **Brand** — the deck should look like it came from your team. Send any of:
|
|
31
|
+
> - a **`brand.json`** from brand-kit, which covers everything below in one file;
|
|
32
|
+
> - a **template**: path to a corporate `.pptx` / `.potx`, and the deck inherits its masters, fonts, colours, logo and slide size;
|
|
33
|
+
> - your **logo** as a PNG (and a white version for dark slides), **brand colours** as hex values for `primary` / `secondary` / `accent` / `background` / `text`, and your **heading and body fonts**;
|
|
34
|
+
> - or, with no brand, a named **palette**: `corporate-blue` (default for B2B) · `monochrome` · `vibrant` · `dark-mode` · `editorial` · `forest` · `sunset`
|
|
32
35
|
> 4. **Anything to embed** — numbers to chart, a table, diagram PNGs
|
|
33
36
|
|
|
34
37
|
Rules for reading the answers:
|
|
@@ -44,10 +47,15 @@ Rules for reading the answers:
|
|
|
44
47
|
[`reference.md`](reference.md#color-palette-principles) — say which roles were filled in.
|
|
45
48
|
- **Template beats palette** for chrome; the palette still colours charts, tables, big numbers
|
|
46
49
|
and quote bars, so ask for brand hexes even when a template is supplied.
|
|
50
|
+
- **Brand material beats a named palette.** If the user sent a logo or brand colours, never fall
|
|
51
|
+
back to `corporate-blue`. If they have brand material but no `brand.json`, suggest running
|
|
52
|
+
`brand-kit` first, so the next deck and any flyer match this one.
|
|
53
|
+
- **Logos must be PNG or JPG.** PowerPoint can't place SVG or EPS: ask for a PNG export, and
|
|
54
|
+
never build with the logo silently missing.
|
|
47
55
|
|
|
48
56
|
**Non-interactive runs** (subagent, CI, headless): audience, format, time and palette degrade to
|
|
49
57
|
`[assumption]` lines stated at the top of the response — default `exec`, live, 16:9,
|
|
50
|
-
`corporate-blue
|
|
58
|
+
`corporate-blue`, or the `brand.json` when one is in the input. Never invent a logo or brand colour. Missing *content* does not degrade: emit `BLOCKED: need deck content
|
|
51
59
|
(outline, doc, or point list)` and build nothing.
|
|
52
60
|
|
|
53
61
|
### 2. Plan the slide list against the audience archetype
|
|
@@ -61,7 +69,7 @@ Rules for reading the answers:
|
|
|
61
69
|
| `internal` | 5–10 | Mixed | Context • Proposal • Discussion topics • Action items |
|
|
62
70
|
| `investor` | 10–15 | Polished, narrative | Problem • Market • Product • Traction • Team • Ask |
|
|
63
71
|
|
|
64
|
-
Full archetype spec: [`audience-profile`](
|
|
72
|
+
Full archetype spec: [`audience-profile`](../../../stakeholder-comms/skills/audience-profile/SKILL.md). Slide-count budgets per
|
|
65
73
|
format and time: [`reference.md`](reference.md#slide-count-budgets).
|
|
66
74
|
|
|
67
75
|
### 3. Build a deck spec
|
|
@@ -77,6 +85,7 @@ A JSON document — full schema, with every layout and the template block, in
|
|
|
77
85
|
"template": "./brand/acme-master.potx",
|
|
78
86
|
"layout_map": { "title": "Title Slide", "content": "Title and Content" },
|
|
79
87
|
"palette": { "primary": "#1A2A6C", "accent": "#FDBB2D" },
|
|
88
|
+
"brand": "./brand/brand.json",
|
|
80
89
|
"slides": [
|
|
81
90
|
{ "layout": "title", "title": "...", "subtitle": "...", "presenter": "...", "date": "..." },
|
|
82
91
|
{ "layout": "content", "title": "...", "bullets": ["..."], "notes": "..." },
|
|
@@ -129,6 +138,13 @@ python3 "${CLAUDE_SKILL_DIR}/scripts/build_deck.py" /tmp/deck-spec.json -o ./out
|
|
|
129
138
|
cd path/to/deck-builder && python3 scripts/build_deck.py /tmp/deck-spec.json -o ./out/deck.pptx
|
|
130
139
|
```
|
|
131
140
|
|
|
141
|
+
Add `--brand ./brand/brand.json` to build in the user's brand (or set `"brand"` in the spec).
|
|
142
|
+
The brand fills whatever the spec leaves out: palette colours, the template, logo files and
|
|
143
|
+
fonts. On the built-in design the logo goes on every slide: large on the cover, small in the
|
|
144
|
+
lower-left corner elsewhere, and the on-dark version on dark section and closing slides. The
|
|
145
|
+
brand's heading and body fonts are applied to titles, text, tables and charts. A template
|
|
146
|
+
already carries its own logo and fonts, so they come from the template instead.
|
|
147
|
+
|
|
132
148
|
Add `--strict` to fail the build on a missing or unreadable image instead of drawing a
|
|
133
149
|
placeholder box — use it in CI, not in a conversation.
|
|
134
150
|
|
|
@@ -157,7 +173,8 @@ A generated deck is a strong first draft. Always tell the user to:
|
|
|
157
173
|
|
|
158
174
|
## Quality bar
|
|
159
175
|
|
|
160
|
-
- **Design is chosen by the user, never silently defaulted** — a template
|
|
176
|
+
- **Design is chosen by the user, never silently defaulted** — a `brand.json`, a template, brand colours and logo, or a named palette, confirmed in the setup block.
|
|
177
|
+
- **The user's brand shows on every slide** — their logo (PNG) and colours, and their fonts when they gave them. A deck built from brand material that comes out in `corporate-blue` has failed.
|
|
161
178
|
- **A supplied template is actually inherited** — `layout_map` covers `title`, `section`, `content` and `closing` at minimum. A branded deck rendered on the blank layout has thrown away the branding.
|
|
162
179
|
- **Title slide isn't blank** — real title, subtitle, presenter, date.
|
|
163
180
|
- **Numbers are charted, not bulleted** — three or more comparable figures belong in a `chart` or a `table`, not a bullet list.
|
|
@@ -171,6 +188,7 @@ A generated deck is a strong first draft. Always tell the user to:
|
|
|
171
188
|
## Anti-patterns to avoid
|
|
172
189
|
|
|
173
190
|
- ❌ Ignoring a template the user already named, or asking for a palette after they supplied one. The template is the answer to "what should this look like".
|
|
191
|
+
- ❌ Asking for brand colours when a `brand.json` was supplied, or building without the logo because it was an SVG. Ask for a PNG export instead.
|
|
174
192
|
- ❌ Mapping every logical layout onto the template's one title-and-bullets layout. That is a branded deck that still looks generated.
|
|
175
193
|
- ❌ Generating without confirming audience. The same content for execs and engineers is a different deck.
|
|
176
194
|
- ❌ Inventing a statistic to fill a `big_number`, or a series to fill a chart. No source, no chart.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deck-builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Generate a real PowerPoint (.pptx) file from content, audience, and either a brand template or a color palette — with charts, tables and images, not only bullets. Slide count, density and layout are tuned to the audience: execs, boards, technical reviewers, sales prospects, investors, internal teams. Use when the user wants an actual editable .pptx rather than an outline, wants their corporate template or brand colours applied, or says \"build the deck\", \"make it a pptx\", \"use our template\".",
|
|
5
5
|
"entrypoint": "SKILL.md",
|
|
6
6
|
"deps": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"pip": ["python-pptx==1.0.2"]
|
|
9
9
|
},
|
|
10
10
|
"env": { "required": [], "optional": [] },
|
|
11
|
-
"related": ["audience-profile", "slide-outliner", "doc-critique"],
|
|
11
|
+
"related": ["brand-kit", "audience-profile", "slide-outliner", "doc-critique"],
|
|
12
12
|
"install": {
|
|
13
13
|
"pip": "python3 -m pip install -r requirements.txt"
|
|
14
14
|
},
|
|
@@ -9,6 +9,9 @@ The spec schema is documented in templates/deck-spec.json. Top-level fields:
|
|
|
9
9
|
colors and slide size are inherited
|
|
10
10
|
- layout_map: logical layout -> template layout name or index (template mode)
|
|
11
11
|
- palette: a hex-color block, or a name resolvable from templates/palettes.json
|
|
12
|
+
- brand: path to a brand.json (the brand-kit skill writes one) or the same object
|
|
13
|
+
inline. It supplies anything the spec leaves out: palette colours, template,
|
|
14
|
+
logo files and fonts. --brand on the command line overrides it.
|
|
12
15
|
- slide_numbers: true/false (default: true when the deck has more than 10 slides)
|
|
13
16
|
- slides: list of slide specs, each with a `layout` field
|
|
14
17
|
|
|
@@ -16,7 +19,7 @@ Supported layouts: title, section, content, two_column, big_number, quote,
|
|
|
16
19
|
image, table, chart, closing.
|
|
17
20
|
|
|
18
21
|
Usage:
|
|
19
|
-
python3 build_deck.py <spec.json> [-o output.pptx] [--strict]
|
|
22
|
+
python3 build_deck.py <spec.json> [-o output.pptx] [--strict] [--brand brand.json]
|
|
20
23
|
"""
|
|
21
24
|
|
|
22
25
|
from __future__ import annotations
|
|
@@ -73,6 +76,12 @@ RASTER_OK = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".tiff"}
|
|
|
73
76
|
WARNINGS: list[str] = []
|
|
74
77
|
|
|
75
78
|
|
|
79
|
+
# Set from brand.json in build(): fonts for the built-in design, and the logo files placed on
|
|
80
|
+
# its slides. A supplied template already carries its own fonts and logo, so these are only
|
|
81
|
+
# used when a slide renders on the built-in design.
|
|
82
|
+
BRAND = {"heading_font": None, "body_font": None, "logo": None, "logo_on_dark": None}
|
|
83
|
+
|
|
84
|
+
|
|
76
85
|
def warn(msg: str) -> None:
|
|
77
86
|
WARNINGS.append(msg)
|
|
78
87
|
sys.stderr.write(f"warning: {msg}\n")
|
|
@@ -405,6 +414,9 @@ def add_textbox(slide, text: str, *, left, top, width, height,
|
|
|
405
414
|
run.font.size = size
|
|
406
415
|
run.font.bold = bold
|
|
407
416
|
run.font.color.rgb = hex_color(color)
|
|
417
|
+
font = BRAND["heading_font"] if (bold or size >= Pt(24)) else BRAND["body_font"]
|
|
418
|
+
if font:
|
|
419
|
+
run.font.name = font
|
|
408
420
|
|
|
409
421
|
|
|
410
422
|
def add_bullets(slide, bullets, *, left, top, width, height,
|
|
@@ -419,6 +431,8 @@ def add_bullets(slide, bullets, *, left, top, width, height,
|
|
|
419
431
|
for run in p.runs:
|
|
420
432
|
run.font.size = size
|
|
421
433
|
run.font.color.rgb = hex_color(color)
|
|
434
|
+
if BRAND["body_font"]:
|
|
435
|
+
run.font.name = BRAND["body_font"]
|
|
422
436
|
|
|
423
437
|
|
|
424
438
|
def add_rect(slide, *, left, top, width, height, fill: str) -> None:
|
|
@@ -503,6 +517,8 @@ def place_table(slide, columns, rows, region: Region, c: Canvas, palette: dict)
|
|
|
503
517
|
run.font.bold = True
|
|
504
518
|
run.font.size = c.pt(body_pt + 1)
|
|
505
519
|
run.font.color.rgb = hex_color("#FFFFFF")
|
|
520
|
+
if BRAND["heading_font"]:
|
|
521
|
+
run.font.name = BRAND["heading_font"]
|
|
506
522
|
|
|
507
523
|
for i, row in enumerate(rows, start=1):
|
|
508
524
|
for j in range(n_cols):
|
|
@@ -514,6 +530,8 @@ def place_table(slide, columns, rows, region: Region, c: Canvas, palette: dict)
|
|
|
514
530
|
for p in cell.text_frame.paragraphs:
|
|
515
531
|
for run in p.runs:
|
|
516
532
|
run.font.size = c.pt(body_pt)
|
|
533
|
+
if BRAND["body_font"]:
|
|
534
|
+
run.font.name = BRAND["body_font"]
|
|
517
535
|
run.font.color.rgb = hex_color(palette["text"])
|
|
518
536
|
|
|
519
537
|
|
|
@@ -543,6 +561,8 @@ def place_chart(slide, chart_spec: dict, region: Region, c: Canvas, palette: dic
|
|
|
543
561
|
region.width, region.height, data)
|
|
544
562
|
chart = gfx.chart
|
|
545
563
|
chart.font.size = c.pt(13)
|
|
564
|
+
if BRAND["body_font"]:
|
|
565
|
+
chart.font.name = BRAND["body_font"]
|
|
546
566
|
chart.font.color.rgb = hex_color(palette["text"])
|
|
547
567
|
|
|
548
568
|
is_pie = kind in ("pie", "doughnut")
|
|
@@ -934,9 +954,81 @@ def layout_numbers_slides(layout) -> bool:
|
|
|
934
954
|
# ----------------------------------------------------------------- driver
|
|
935
955
|
|
|
936
956
|
|
|
937
|
-
def
|
|
957
|
+
def load_brand(ref, base: Path):
|
|
958
|
+
"""brand.json path or inline object -> (dict, directory its relative paths resolve from)."""
|
|
959
|
+
if not ref:
|
|
960
|
+
return {}, base
|
|
961
|
+
if isinstance(ref, dict):
|
|
962
|
+
return ref, base
|
|
963
|
+
path = (base / ref).expanduser() if not Path(ref).is_absolute() else Path(ref)
|
|
964
|
+
if not path.exists():
|
|
965
|
+
warn(f"brand file not found: {path}; building without it")
|
|
966
|
+
return {}, base
|
|
967
|
+
try:
|
|
968
|
+
return json.loads(path.read_text()), path.parent
|
|
969
|
+
except json.JSONDecodeError as e:
|
|
970
|
+
warn(f"brand file {path} is not valid JSON ({e}); building without it")
|
|
971
|
+
return {}, base
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def apply_brand(spec: dict, brand: dict, brand_dir: Path, spec_dir: Path) -> None:
|
|
975
|
+
"""Fill what the spec leaves out from the brand. The spec always wins: a deck can still
|
|
976
|
+
override one colour or swap the template without editing the brand file."""
|
|
977
|
+
colors = {k: v for k, v in (brand.get("colors") or {}).items()
|
|
978
|
+
if k in ("primary", "secondary", "accent", "background", "text") and v}
|
|
979
|
+
if colors and not spec.get("palette"):
|
|
980
|
+
spec["palette"] = colors
|
|
981
|
+
elif colors and isinstance(spec.get("palette"), dict):
|
|
982
|
+
spec["palette"] = {**colors, **spec["palette"]}
|
|
983
|
+
tpl = (brand.get("templates") or {}).get("pptx") or brand.get("template")
|
|
984
|
+
if tpl and not spec.get("template"):
|
|
985
|
+
spec["template"] = str((brand_dir / tpl).resolve()) if not Path(tpl).is_absolute() else tpl
|
|
986
|
+
fonts = brand.get("fonts") or {}
|
|
987
|
+
BRAND["heading_font"] = (fonts.get("heading") or {}).get("family") if isinstance(fonts.get("heading"), dict) else fonts.get("heading")
|
|
988
|
+
BRAND["body_font"] = (fonts.get("body") or {}).get("family") if isinstance(fonts.get("body"), dict) else fonts.get("body")
|
|
989
|
+
logo = brand.get("logo") or {}
|
|
990
|
+
for key, field in (("logo", "primary"), ("logo_on_dark", "on_dark")):
|
|
991
|
+
ref = logo.get(field) if isinstance(logo, dict) else (logo if field == "primary" else None)
|
|
992
|
+
if not ref:
|
|
993
|
+
continue
|
|
994
|
+
lp = (brand_dir / ref) if not Path(ref).is_absolute() else Path(ref)
|
|
995
|
+
if lp.suffix.lower() not in (".png", ".jpg", ".jpeg", ".gif", ".bmp"):
|
|
996
|
+
warn(f"logo {lp.name} is {lp.suffix or 'not an image'}; PowerPoint needs PNG or JPG. Export the logo as PNG.")
|
|
997
|
+
elif not lp.exists():
|
|
998
|
+
warn(f"logo not found: {lp}")
|
|
999
|
+
else:
|
|
1000
|
+
BRAND[key] = str(lp)
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def is_dark(hex_str: str) -> bool:
|
|
1004
|
+
h = hex_str.lstrip("#")
|
|
1005
|
+
r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4))
|
|
1006
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b < 0.45
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def place_logo(slide, c: "Canvas", palette: dict, logical: str) -> None:
|
|
1010
|
+
"""The brand logo on the built-in design: large on the cover, small in a fixed corner on
|
|
1011
|
+
every other slide, using the on-dark variant on dark slides when there is one."""
|
|
1012
|
+
if not BRAND["logo"] and not BRAND["logo_on_dark"]:
|
|
1013
|
+
return
|
|
1014
|
+
dark_bg = logical in ("section", "closing") and is_dark(palette["primary"]) or \
|
|
1015
|
+
(logical == "title" and is_dark(palette["background"]))
|
|
1016
|
+
path = (BRAND["logo_on_dark"] or BRAND["logo"]) if dark_bg else (BRAND["logo"] or BRAND["logo_on_dark"])
|
|
1017
|
+
if logical == "title":
|
|
1018
|
+
slide.shapes.add_picture(path, c.x(0.75), c.y(0.6), height=c.y(0.7))
|
|
1019
|
+
elif logical == "closing":
|
|
1020
|
+
pic = slide.shapes.add_picture(path, 0, c.h - c.y(1.25), height=c.y(0.6))
|
|
1021
|
+
pic.left = int((c.w - pic.width) / 2)
|
|
1022
|
+
else:
|
|
1023
|
+
slide.shapes.add_picture(path, c.x(0.5), c.h - c.y(0.55), height=c.y(0.32))
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool, brand_ref=None) -> None:
|
|
938
1027
|
spec = json.loads(spec_path.read_text())
|
|
939
1028
|
spec_dir = spec_path.parent
|
|
1029
|
+
brand, brand_dir = load_brand(brand_ref or spec.get("brand"), spec_dir)
|
|
1030
|
+
if brand:
|
|
1031
|
+
apply_brand(spec, brand, brand_dir, spec_dir)
|
|
940
1032
|
palette = resolve_palette(spec.get("palette"), script_dir)
|
|
941
1033
|
|
|
942
1034
|
slides = spec.get("slides", [])
|
|
@@ -970,6 +1062,8 @@ def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool) ->
|
|
|
970
1062
|
render_templated(slide, canvas, palette, slide_spec, ctx, logical)
|
|
971
1063
|
else:
|
|
972
1064
|
RENDERERS[logical](slide, canvas, palette, slide_spec, ctx)
|
|
1065
|
+
if not using_template: # a template's masters already carry its logo
|
|
1066
|
+
place_logo(slide, canvas, palette, logical)
|
|
973
1067
|
|
|
974
1068
|
if slide_spec.get("notes"):
|
|
975
1069
|
set_notes(slide, slide_spec["notes"])
|
|
@@ -982,6 +1076,10 @@ def build(spec_path: Path, out_path: Path, script_dir: Path, *, strict: bool) ->
|
|
|
982
1076
|
prs.save(str(out_path))
|
|
983
1077
|
|
|
984
1078
|
mode = "template" if using_template else "built-in design"
|
|
1079
|
+
if brand:
|
|
1080
|
+
mode += ", brand: " + ", ".join(x for x, on in (("colours", bool(brand.get("colors"))),
|
|
1081
|
+
("logo", bool(BRAND["logo"] or BRAND["logo_on_dark"])),
|
|
1082
|
+
("fonts", bool(BRAND["heading_font"] or BRAND["body_font"]))) if on)
|
|
985
1083
|
print(f"wrote {out_path} ({len(slides)} slides, {mode})")
|
|
986
1084
|
if WARNINGS:
|
|
987
1085
|
print(f"{len(WARNINGS)} warning(s) — see stderr; fix them before presenting")
|
|
@@ -995,6 +1093,9 @@ def main(argv) -> int:
|
|
|
995
1093
|
help="Output .pptx path (default: alongside the spec)")
|
|
996
1094
|
ap.add_argument("--strict", action="store_true",
|
|
997
1095
|
help="Fail on a missing/unsupported image instead of drawing a placeholder")
|
|
1096
|
+
ap.add_argument("--brand", metavar="BRAND_JSON",
|
|
1097
|
+
help="brand.json (from the brand-kit skill): colours, template, logo, fonts. "
|
|
1098
|
+
"Fills anything the spec leaves out")
|
|
998
1099
|
ap.add_argument("--list-layouts", metavar="TEMPLATE",
|
|
999
1100
|
help="Print a template's layout names and indices, then exit "
|
|
1000
1101
|
"(use them to write layout_map)")
|
|
@@ -1024,7 +1125,8 @@ def main(argv) -> int:
|
|
|
1024
1125
|
out_path = (Path(args.output).expanduser().resolve()
|
|
1025
1126
|
if args.output else spec_path.with_suffix(".pptx"))
|
|
1026
1127
|
|
|
1027
|
-
build(spec_path, out_path, Path(__file__).resolve().parent, strict=args.strict
|
|
1128
|
+
build(spec_path, out_path, Path(__file__).resolve().parent, strict=args.strict,
|
|
1129
|
+
brand_ref=str(Path(args.brand).expanduser().resolve()) if args.brand else None)
|
|
1028
1130
|
return 0
|
|
1029
1131
|
|
|
1030
1132
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Full deck-spec schema. The build_deck.py script consumes this. Top-level fields: title, audience, aspect, template, layout_map, palette, slide_numbers, slides[].",
|
|
2
|
+
"_comment": "Full deck-spec schema. The build_deck.py script consumes this. Top-level fields: title, audience, aspect, template, layout_map, palette, brand, slide_numbers, slides[].",
|
|
3
3
|
|
|
4
4
|
"title": "ProjectX Migration — Board Update",
|
|
5
5
|
"audience": "board",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"closing": "Closing"
|
|
23
23
|
},
|
|
24
24
|
|
|
25
|
+
"_brand_comment": "Path to a brand.json from the brand-kit skill, or the same object inline. It fills anything this spec leaves out: palette colours, template, logo files (logo.primary, logo.on_dark — PNG or JPG) and fonts (fonts.heading.family, fonts.body.family). --brand on the command line overrides it.",
|
|
26
|
+
"brand": "./brand/brand.json",
|
|
27
|
+
|
|
25
28
|
"palette": {
|
|
26
29
|
"primary": "#1A2A6C",
|
|
27
30
|
"secondary": "#4A5A8C",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: marketing-flyer
|
|
3
|
+
description: Generate a print-ready, on-brand marketing flyer — event, launch, open day, offer, hiring — as one self-contained HTML file (Letter, A4, A5, or square and story sizes for social) that saves to PDF, built from the user's own logo, colours, fonts and content, with one call to action, a short headline, contrast checked, and no invented dates, prices or contact details. Use when the user wants a flyer, poster, one-pager, handout, leaflet or event announcement "in our branding", or says "make a flyer for…".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# marketing-flyer
|
|
7
|
+
|
|
8
|
+
You make a flyer that looks like it came from the user's own team, because it's built from
|
|
9
|
+
their brand: their logo, their colours, their fonts, their words. The output is one HTML file
|
|
10
|
+
with the images embedded, sized for print, that saves to PDF from any browser. When a
|
|
11
|
+
`brand.json` from [`brand-kit`](../brand-kit/SKILL.md) exists, use it, and the flyer will match
|
|
12
|
+
the user's decks from [`deck-builder`](../deck-builder/SKILL.md).
|
|
13
|
+
|
|
14
|
+
## How to respond
|
|
15
|
+
|
|
16
|
+
1. **Ask for everything in one message.** Take what's already been given, and suggest
|
|
17
|
+
defaults so the user can reply "go".
|
|
18
|
+
|
|
19
|
+
> Before I build the flyer, send these. Anything you skip, I'll use the default shown:
|
|
20
|
+
> 1. **Brand:** a `brand.json` from brand-kit, *or* your logo file (and a white version for
|
|
21
|
+
> dark backgrounds), brand colours as hex codes, and heading and body fonts.
|
|
22
|
+
> *Default: no brand, which looks generic, so please send at least a logo and one colour.*
|
|
23
|
+
> 2. **What it's for, and who reads it:** an event, a launch, an offer, hiring, a service
|
|
24
|
+
> 3. **The words:** a headline (ten words or fewer), a one-line subhead, up to four key
|
|
25
|
+
> points, and details such as date, time, place and price
|
|
26
|
+
> 4. **One call to action:** what the reader should do, and the web address, phone or email for it.
|
|
27
|
+
> A QR code image too, if you have one.
|
|
28
|
+
> 5. **Images:** a photo for the top of the flyer, if any, with a line describing it
|
|
29
|
+
> 6. **Size:** `letter` · `a4` · `a5` · `square` (Instagram/LinkedIn) · `story` · and
|
|
30
|
+
> portrait or landscape. *Default: letter, portrait.*
|
|
31
|
+
> 7. **Layout:** `hero` (photo across the top) · `split` (photo down one side) ·
|
|
32
|
+
> `event` (a big date block) · `minimal`. *Default: event when there's a date, otherwise hero.*
|
|
33
|
+
|
|
34
|
+
**Content is the hard input.** Never invent a date, time, price, address, phone number,
|
|
35
|
+
web address or statistic. If the user gave only a topic, ask for the details. A flyer with
|
|
36
|
+
a wrong date is worse than no flyer.
|
|
37
|
+
|
|
38
|
+
2. **Write the copy to fit a flyer.** Rewrite long text into the flyer's shape and show the
|
|
39
|
+
user what you changed:
|
|
40
|
+
- A headline of ten words or fewer that names the thing, such as "Free community health
|
|
41
|
+
fair", not "You're invited!".
|
|
42
|
+
- A subhead that says who it's for, or why it's worth coming.
|
|
43
|
+
- At most four points, each one line, so they can be read in passing.
|
|
44
|
+
- Exactly **one** call to action, worded as a verb ("Save your spot", "Call to book"). Two
|
|
45
|
+
calls to action means the reader does neither.
|
|
46
|
+
|
|
47
|
+
Use the brand's voice from `brand.json` when it has one: words to use, words to avoid.
|
|
48
|
+
|
|
49
|
+
3. **Write the spec** in the shape of [`templates/flyer-spec.json`](templates/flyer-spec.json),
|
|
50
|
+
with paths relative to the spec file.
|
|
51
|
+
|
|
52
|
+
4. **Build it.**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Claude Code
|
|
56
|
+
python3 "${CLAUDE_SKILL_DIR}/scripts/build_flyer.py" flyer-spec.json -o flyer.html --brand ./brand/brand.json --pdf flyer.pdf
|
|
57
|
+
# Other IDEs (from the skill folder)
|
|
58
|
+
python3 scripts/build_flyer.py flyer-spec.json -o flyer.html --brand ./brand/brand.json --pdf flyer.pdf
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`--pdf` and `--png` use a local Chrome or Chromium when one is installed. Without one, the
|
|
62
|
+
user opens `flyer.html` and prints it to PDF at 100% scale with margins set to none.
|
|
63
|
+
Use `--png` for the `square` and `story` sizes, which are posted as images.
|
|
64
|
+
|
|
65
|
+
5. **Read the output and every warning.** The script prints three contrast figures: text,
|
|
66
|
+
the header band, and the call to action. It picks white or the brand's text colour on each
|
|
67
|
+
filled band, whichever reads better. It warns about a long headline, more than four
|
|
68
|
+
points, more than one call to action, a missing logo or image, and no brand. Fix what you
|
|
69
|
+
can, and report the rest to the user with what it means.
|
|
70
|
+
|
|
71
|
+
6. **Tell the user how to check it before printing:** read every date, time and address
|
|
72
|
+
aloud against the source, check the web address works, and print one test copy.
|
|
73
|
+
|
|
74
|
+
**Non-interactive runs** (subagent, CI, headless): missing *content* does not get a default.
|
|
75
|
+
Emit `BLOCKED: need the flyer's details: <what is missing>` and build nothing. Missing brand
|
|
76
|
+
falls back to defaults with an `[assumption]` line at the top; never make up a logo.
|
|
77
|
+
|
|
78
|
+
## Useful references in this skill
|
|
79
|
+
|
|
80
|
+
- [`reference.md`](reference.md) — sizes and print rules, layouts and when to use each, copy budgets, contrast
|
|
81
|
+
- [`templates/flyer-spec.json`](templates/flyer-spec.json) — the spec, every field shown
|
|
82
|
+
- [`scripts/build_flyer.py`](scripts/build_flyer.py) — the builder (stdlib only): HTML, plus PDF and PNG through headless Chrome
|
|
83
|
+
- [`examples/community-health-fair.md`](examples/community-health-fair.md) — worked example: an email from the user becomes a flyer spec and a branded flyer
|
|
84
|
+
|
|
85
|
+
## Quality bar
|
|
86
|
+
|
|
87
|
+
- **It is in the user's brand:** their logo, their colours as hex codes, their fonts. Default colours only when the user has no brand material, and then said out loud.
|
|
88
|
+
- **Every fact comes from the user.** Dates, times, places, prices and contact details are copied, not composed.
|
|
89
|
+
- **One call to action,** worded as a verb, with the address or number that makes it actionable.
|
|
90
|
+
- **A headline of ten words or fewer, at most four points.** Anything longer belongs on the linked page.
|
|
91
|
+
- **Text is readable everywhere:** body text at least 4.5:1 against the background, and the script's band and call-to-action figures reported.
|
|
92
|
+
- **The file is the whole flyer.** Images are embedded, with no links to local files that break when it's emailed.
|
|
93
|
+
|
|
94
|
+
## When to use this skill
|
|
95
|
+
|
|
96
|
+
- ✅ An event, a launch, an open day, an offer, a hiring drive, a service announcement
|
|
97
|
+
- ✅ A one-page handout or leaflet in the company's branding
|
|
98
|
+
- ✅ A square or story-sized image for social posts that matches the printed flyer
|
|
99
|
+
|
|
100
|
+
## When NOT to use this skill
|
|
101
|
+
|
|
102
|
+
- ❌ A slide deck; use `deck-builder`
|
|
103
|
+
- ❌ A multi-page brochure or a report, which this doesn't lay out
|
|
104
|
+
- ❌ Designing a brand from scratch; capture an existing one with `brand-kit`
|
|
105
|
+
|
|
106
|
+
## Anti-patterns to avoid
|
|
107
|
+
|
|
108
|
+
- ❌ **"You're invited!" as the headline.** Name the event, the offer or the product.
|
|
109
|
+
- ❌ **Three buttons** ("Register", "Learn more", "Follow us"). Pick the one action that matters.
|
|
110
|
+
- ❌ **Filling a missing date with "TBC"** or a plausible guess. Ask, or build nothing.
|
|
111
|
+
- ❌ **White text on a pale accent** because the brand's buttons are white. The script flips it; don't override it.
|
|
112
|
+
- ❌ **A paragraph of body copy.** No one reads a paragraph on a noticeboard.
|
|
113
|
+
- ❌ **Sending the HTML with images linked from the user's disk.** Always build with the script, which embeds them.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"query": "Make a flyer for our open day in our brand colours",
|
|
4
|
+
"should_trigger": true
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
"query": "Design a one-page handout announcing the new clinic",
|
|
8
|
+
"should_trigger": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"query": "I need a poster for the charity run, with our logo",
|
|
12
|
+
"should_trigger": true
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"query": "Create an Instagram-sized announcement image that matches our flyer",
|
|
16
|
+
"should_trigger": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"query": "Build the board deck as a pptx",
|
|
20
|
+
"should_trigger": false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"query": "Write the press release for the launch",
|
|
24
|
+
"should_trigger": false
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"query": "Set up our brand colours and fonts so we can reuse them",
|
|
28
|
+
"should_trigger": false
|
|
29
|
+
}
|
|
30
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "marketing-flyer",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Make a flyer for our community health fair using ./brand/brand.json. It's Saturday 14 November, 10am to 2pm, at the Riverside Park pavilion, free. There'll be blood pressure and glucose checks, flu shots with no appointment, a kids' corner and cooking demos. People should register at northwindhealth.example/fair. Letter size.",
|
|
7
|
+
"assertions": [
|
|
8
|
+
"Uses the supplied brand.json, so the flyer carries the brand logo and colours",
|
|
9
|
+
"The headline is ten words or fewer and names the event",
|
|
10
|
+
"There is exactly one call to action, worded as a verb, with northwindhealth.example/fair",
|
|
11
|
+
"The date, time, place and price match the prompt exactly; nothing is invented (no phone number, no extra dates)",
|
|
12
|
+
"No more than four points",
|
|
13
|
+
"Runs build_flyer.py and reports the contrast figures and any warnings",
|
|
14
|
+
"Picks the event layout, or explains choosing another, because the date is the key fact"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Worked example — an email about a health fair becomes a branded flyer
|
|
2
|
+
|
|
3
|
+
## Input given to the skill
|
|
4
|
+
|
|
5
|
+
> Can you make a flyer for our community health fair? Use our brand kit in ./brand/. It's
|
|
6
|
+
> Saturday 14 November, 10am to 2pm, at the Riverside Park pavilion, and it's free. We'll have
|
|
7
|
+
> free blood pressure and glucose checks, flu shots (no appointment), a kids' corner and some
|
|
8
|
+
> healthy cooking demos. Oh and a raffle. People should register at
|
|
9
|
+
> northwindhealth.example/fair so we know numbers. Letter size please, we'll print it at the office.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
**Copy, fitted to a flyer.** Here is what I changed from your email:
|
|
14
|
+
|
|
15
|
+
- **Headline:** "Free community health fair" (5 words)
|
|
16
|
+
- **Subhead:** "Checkups, flu shots and healthy cooking — for the whole family"
|
|
17
|
+
- **Points (3):** free blood pressure and glucose checks · flu shots, no appointment needed ·
|
|
18
|
+
kids' corner and healthy cooking demos. *The raffle is left off: four points is the most a
|
|
19
|
+
flyer carries, and the other three bring people in. Say if you want it instead of the
|
|
20
|
+
cooking demos.*
|
|
21
|
+
- **Call to action:** "Save your spot" → northwindhealth.example/fair
|
|
22
|
+
- **Layout:** `event`, because the date is the fact people need first
|
|
23
|
+
|
|
24
|
+
**Spec** (`flyer-spec.json`):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"brand": "./brand/brand.json",
|
|
29
|
+
"size": "letter",
|
|
30
|
+
"layout": "event",
|
|
31
|
+
"headline": "Free community health fair",
|
|
32
|
+
"subhead": "Checkups, flu shots and healthy cooking — for the whole family",
|
|
33
|
+
"points": ["Free blood pressure and glucose checks", "Flu shots, no appointment needed", "Kids' corner and healthy cooking demos"],
|
|
34
|
+
"details": [
|
|
35
|
+
{ "label": "When", "value": "Sat 14 Nov, 10am–2pm" },
|
|
36
|
+
{ "label": "Where", "value": "Riverside Park pavilion" },
|
|
37
|
+
{ "label": "Cost", "value": "Free" }
|
|
38
|
+
],
|
|
39
|
+
"cta": { "text": "Save your spot", "url": "northwindhealth.example/fair" },
|
|
40
|
+
"image": { "path": "./riverside-park.png", "alt": "Families at Riverside Park" },
|
|
41
|
+
"footer": "© 2026 Northwind Health"
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Build:**
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ python3 scripts/build_flyer.py flyer-spec.json -o community-health-fair.html --brand ./brand/brand.json --pdf community-health-fair.pdf
|
|
49
|
+
wrote ./community-health-fair.html (event, letter; brand: yes; logo: yes)
|
|
50
|
+
contrast: text 14.8:1 · band 8.2:1 · call to action 7.2:1
|
|
51
|
+
wrote ./community-health-fair.pdf
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**The flyer:** Northwind's green band across the top with the white logo. A large amber date
|
|
55
|
+
block reading *Sat 14 Nov, 10am–2pm* sits beside the Georgia headline. Below are the park
|
|
56
|
+
photo, the three points with amber markers, Where and Cost, and an amber "Save your spot" panel
|
|
57
|
+
with the web address in dark text, because white on that amber would be hard to read. The
|
|
58
|
+
footer carries the website and the copyright line.
|
|
59
|
+
|
|
60
|
+
**Before you print:** read the date, time and place aloud against your email, open
|
|
61
|
+
northwindhealth.example/fair to check it works, and print one test copy.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
**Why this is a good output:** every fact is copied from the email, and the one cut (the raffle)
|
|
66
|
+
is named and offered back instead of silently dropped. There's one call to action. The layout
|
|
67
|
+
puts the date first because that's what a passer-by needs. The contrast figures are reported,
|
|
68
|
+
and the panel text uses the colour that reads.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "marketing-flyer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate a print-ready, on-brand marketing flyer — event, launch, open day, offer, hiring — as one self-contained HTML file (Letter, A4, A5, or square and story sizes for social) that saves to PDF, built from the user's own logo, colours, fonts and content, with one call to action, a short headline, contrast checked, and no invented dates, prices or contact details. Use when the user wants a flyer, poster, one-pager, handout, leaflet or event announcement \"in our branding\", or says \"make a flyer for…\".",
|
|
5
|
+
"entrypoint": "SKILL.md",
|
|
6
|
+
"deps": {
|
|
7
|
+
"npm": [],
|
|
8
|
+
"pip": []
|
|
9
|
+
},
|
|
10
|
+
"env": {
|
|
11
|
+
"required": [],
|
|
12
|
+
"optional": []
|
|
13
|
+
},
|
|
14
|
+
"related": [
|
|
15
|
+
"brand-kit",
|
|
16
|
+
"deck-builder"
|
|
17
|
+
],
|
|
18
|
+
"handoff": [
|
|
19
|
+
{
|
|
20
|
+
"to": "brand-kit",
|
|
21
|
+
"when": "the user has brand material but no brand.json",
|
|
22
|
+
"purpose": "Capture the logo, colours and fonts once, so the flyer and later decks match.",
|
|
23
|
+
"fallback": "If brand-kit is absent, ask for the logo path and brand hex codes in the setup block and put them in the spec's brand object inline."
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"tags": [
|
|
27
|
+
"flyer",
|
|
28
|
+
"poster",
|
|
29
|
+
"marketing",
|
|
30
|
+
"print",
|
|
31
|
+
"pdf",
|
|
32
|
+
"brand",
|
|
33
|
+
"design"
|
|
34
|
+
],
|
|
35
|
+
"model": {
|
|
36
|
+
"tier": "standard",
|
|
37
|
+
"rationale": "Writes short copy to fit and fills a spec for a deterministic builder. Bounded generation."
|
|
38
|
+
}
|
|
39
|
+
}
|