5 changed files with 25 additions and 97 deletions
+10 -62
View File
@@ -22,55 +22,6 @@ and layouts exactly as designed.
--- ---
## WORKFLOW — ALWAYS FOLLOW THIS PROCESS
Before generating any slides, complete these 3 steps **in order**. Do not skip straight to generating.
### Step 1: Brief the presentation
Ask the user these questions **in a single message** before doing anything else:
1. **Doel** — Wat is het doel van deze presentatie? (informeren / overtuigen / updaten / inspireren)
2. **Kernboodschap** — Als het publiek één ding onthoudt, wat moet dat zijn?
3. **Publiek** — Voor wie is dit? (intern team, MT, externe partner, all-hands)
4. **Taal** — Nederlands of Engels?
5. **Beschikbare bronnen** — Welke databronnen mogen we gebruiken voor de inhoud?
For the last question, check which connectors/MCPs are active and mention them explicitly as options:
- **Slack** → recente updates, team-aankondigingen, discussies ophalen
- **Gmail** → e-mails, klantcommunicatie, recente updates
- **Google Drive** → documenten, rapporten, bestaand materiaal
- **Ahrefs / GSC** → SEO data, keyword performance, traffic metrics
- **Contentful** → productcontent, beschrijvingen
- **Datadog / analytics** → performance data en metrics
### Step 2: Propose a slide structure
Based on the brief, **propose a numbered slide plan** and wait for approval before generating anything. Example format:
```
Voorgestelde opzet:
1. Titelslide
2. Agenda
3. Sectie-divider — Q1 Financiën
4. Big number — Omzethighlight
5. Chart KPI cards — Financieel overzicht
6. Section-divider — AI & Automation
7. Before/after — Contentproductie
8. Impact statement
9. Afsluiting
Klopt deze opzet? Wil je slides toevoegen, weglaten of verplaatsen?
```
**Do not generate until the user approves the structure.**
### Step 3: Generate
Only after approval: set up the generator, build the slide plan, and run `generate()`.
---
## SETUP (run once per session) ## SETUP (run once per session)
Before generating, ensure the generator and baseline are available: Before generating, ensure the generator and baseline are available:
@@ -119,7 +70,7 @@ generate([
("Team Strategy & Leadership", "Author Name / Team"), ("Team Strategy & Leadership", "Author Name / Team"),
]), ]),
("closing", []), ("closing", []),
], "/sessions/brave-bold-cerf/mnt/outputs/MyPresentation.pptx") ], "/Users/derkdissselhof/Library/Application Support/Claude/local-agent-mode-sessions/a2d0567c-5a2d-4037-a100-87a5e74b9a55/3f471e31-d031-4ffe-bb3a-282799a05800/local_0cbc5cf3-74a0-4400-80d7-33c648a960dc/outputs/MyPresentation.pptx")
``` ```
--- ---
@@ -270,7 +221,7 @@ Light gray background with 4 equal feature/USP cards.
| `Vier redenen om voor ons te kiezen` | Slide title | | `Vier redenen om voor ons te kiezen` | Slide title |
| `Laagste prijs` | Card title line 1 (all 4 cards identical in baseline) | | `Laagste prijs` | Card title line 1 (all 4 cards identical in baseline) |
| `garantie` | Card title line 2 (all 4 cards identical in baseline) | | `garantie` | Card title line 2 (all 4 cards identical in baseline) |
| `All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.` | Card body (all 4 identical in baseline) | | `All-inclusive prijzen zonder verborgen kosten...` | Card body (all 4 identical in baseline) |
| `30%` | Card stat (all 4 identical in baseline) | | `30%` | Card stat (all 4 identical in baseline) |
| `goedkoper dan traditioneel` | Card stat label (all 4 identical in baseline) | | `goedkoper dan traditioneel` | Card stat label (all 4 identical in baseline) |
@@ -284,19 +235,19 @@ cards share identical placeholder text, pass `n=1..4` to target each card indivi
# Card 1 # Card 1
("Laagste prijs", "Armeense lunch", 1), ("Laagste prijs", "Armeense lunch", 1),
("garantie", "Tolma & Ghapama", 1), ("garantie", "Tolma & Ghapama", 1),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", "Sona nodigde iedereen uit voor een Armeense lunch op 10 april.", 1), ("All-inclusive prijzen zonder verborgen kosten...", "Sona nodigde iedereen uit voor een Armeense lunch op 10 april.", 1),
# Card 2 # Card 2
("Laagste prijs", "Paas taart", 2), ("Laagste prijs", "Paas taart", 2),
("garantie", "van Marte", 2), ("garantie", "van Marte", 2),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", "Marte verraste het team met een zelfgemaakte paastaart.", 2), ("All-inclusive prijzen zonder verborgen kosten...", "Marte verraste het team met een zelfgemaakte paastaart.", 2),
# Card 3 # Card 3
("Laagste prijs", "Chinese snacks", 3), ("Laagste prijs", "Chinese snacks", 3),
("garantie", "van Irene", 3), ("garantie", "van Irene", 3),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", "Irene bracht authentieke Chinese snacks mee voor het hele kantoor.", 3), ("All-inclusive prijzen zonder verborgen kosten...", "Irene bracht authentieke Chinese snacks mee voor het hele kantoor.", 3),
# Card 4 # Card 4
("Laagste prijs", "Boek tip", 4), ("Laagste prijs", "Boek tip", 4),
("garantie", "van Dennis", 4), ("garantie", "van Dennis", 4),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", "Dennis deelde zijn favoriete boek: Zero to One van Peter Thiel.", 4), ("All-inclusive prijzen zonder verborgen kosten...", "Dennis deelde zijn favoriete boek: Zero to One van Peter Thiel.", 4),
# Remove stat elements (replace with empty or relevant values) # Remove stat elements (replace with empty or relevant values)
("30%", ""), ("30%", ""),
("goedkoper dan traditioneel", ""), ("goedkoper dan traditioneel", ""),
@@ -368,15 +319,12 @@ White slide comparing two states side by side (gray left panel, dark right panel
| `nodig` | Left stat 3 label | | `nodig` | Left stat 3 label |
| `Geautomatiseerd` | Right panel title | | `Geautomatiseerd` | Right panel title |
| `Claude Skills handelen keyword research, copy generation en Contentful publishing af. Mens reviewt alleen het eindresultaat.` | Right panel body | | `Claude Skills handelen keyword research, copy generation en Contentful publishing af. Mens reviewt alleen het eindresultaat.` | Right panel body |
| `15min` | Right stat 1 number | | `15min` | Right stat 1 |
| `per product per markt` | Right stat 1 label (**appears twice** — the global replacement above also covers this) | | `500+` | Right stat 2 |
| `500+` | Right stat 2 number |
| `producten per week` | Right stat 2 label | | `producten per week` | Right stat 2 label |
| `0.5 FTE` | Right stat 3 number | | `0.5 FTE` | Right stat 3 |
| `menselijke review` | Right stat 3 label | | `menselijke review` | Right stat 3 label |
> **Important:** `per product per markt` is used as the label for **both** left stat 1 and right stat 1. A single global replacement `("per product per markt", "your new label")` updates both sides at once. If you want different labels per side, replace right stat 1 first with a unique temporary value, then do the global replacement.
>
> Note: `⬤ VOORHEEN` and `⬤ NU MET AI SKILLS` are colored dot labels — best left as-is. > Note: `⬤ VOORHEEN` and `⬤ NU MET AI SKILLS` are colored dot labels — best left as-is.
--- ---
@@ -547,7 +495,7 @@ plan = [
] ]
# 3. Generate # 3. Generate
generate(plan, "/sessions/brave-bold-cerf/mnt/outputs/MyPresentation.pptx") generate(plan, "/Users/derkdissselhof/Library/Application Support/Claude/local-agent-mode-sessions/a2d0567c-5a2d-4037-a100-87a5e74b9a55/3f471e31-d031-4ffe-bb3a-282799a05800/local_0cbc5cf3-74a0-4400-80d7-33c648a960dc/outputs/MyPresentation.pptx")
``` ```
--- ---
@@ -266,11 +266,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
{slide_order[i][1] for i in range(total) if i not in wanted} {slide_order[i][1] for i in range(total) if i not in wanted}
) )
used_media = collect_used_media(src, drop_paths) used_media = collect_used_media(src, drop_paths)
# Also add media from copy sources (they reference same files as originals)
# (already captured above since originals' rels are not in drop_paths)
# Collect media for copied slides (same source rels → same media)
# Already included in used_media since source slides are in keep_paths.
out_buf = BytesIO() out_buf = BytesIO()
with zipfile.ZipFile(out_buf, 'w', zipfile.ZIP_DEFLATED) as dst: with zipfile.ZipFile(out_buf, 'w', zipfile.ZIP_DEFLATED) as dst:
@@ -312,7 +307,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
if item == 'ppt/presentation.xml': if item == 'ppt/presentation.xml':
data = src.read(item).decode('utf-8') data = src.read(item).decode('utf-8')
# Extract existing sldId elements → map rId → element
rid_to_elem = {} rid_to_elem = {}
existing_ids = [] existing_ids = []
for m in re.finditer(r'<p:sldId\b[^/]*/>', data): for m in re.finditer(r'<p:sldId\b[^/]*/>', data):
@@ -325,7 +319,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
next_sld_id = max(existing_ids, default=256) + 1 next_sld_id = max(existing_ids, default=256) + 1
# Build sldId elements for copies
for copy_rid, (src_info, _, _, copy_num) in copies.items(): for copy_rid, (src_info, _, _, copy_num) in copies.items():
elem = ( elem = (
f'<p:sldId id="{next_sld_id}"' f'<p:sldId id="{next_sld_id}"'
@@ -334,7 +327,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
rid_to_elem[copy_rid] = elem rid_to_elem[copy_rid] = elem
next_sld_id += 1 next_sld_id += 1
# Reorder sldIdLst to match plan_order
ordered = ''.join( ordered = ''.join(
rid_to_elem[rid] for rid in plan_order if rid in rid_to_elem rid_to_elem[rid] for rid in plan_order if rid in rid_to_elem
) )
@@ -350,7 +342,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
if item == 'ppt/_rels/presentation.xml.rels': if item == 'ppt/_rels/presentation.xml.rels':
data = src.read(item).decode('utf-8') data = src.read(item).decode('utf-8')
# Remove unwanted slide rels
def keep_rel(m): def keep_rel(m):
rid = re.search(r'Id="([^"]+)"', m.group(0)) rid = re.search(r'Id="([^"]+)"', m.group(0))
rtype = re.search(r'Type="([^"]+)"', m.group(0)) rtype = re.search(r'Type="([^"]+)"', m.group(0))
@@ -359,7 +350,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
return m.group(0) return m.group(0)
data = re.sub(r'<Relationship[^>]*/>', keep_rel, data) data = re.sub(r'<Relationship[^>]*/>', keep_rel, data)
# Add relationships for copied slides
copy_rels = ''.join( copy_rels = ''.join(
f'<Relationship Id="{copy_rid}" ' f'<Relationship Id="{copy_rid}" '
f'Type="{SLIDE_REL}" ' f'Type="{SLIDE_REL}" '
@@ -374,7 +364,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
if item == '[Content_Types].xml': if item == '[Content_Types].xml':
data = src.read(item).decode('utf-8') data = src.read(item).decode('utf-8')
# Remove unwanted slide overrides
def keep_ct(m): def keep_ct(m):
ct_type = re.search(r'ContentType="([^"]+)"', m.group(0)) ct_type = re.search(r'ContentType="([^"]+)"', m.group(0))
part = re.search(r'PartName="([^"]+)"', m.group(0)) part = re.search(r'PartName="([^"]+)"', m.group(0))
@@ -385,7 +374,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
return m.group(0) return m.group(0)
data = re.sub(r'<Override[^>]*/>', keep_ct, data) data = re.sub(r'<Override[^>]*/>', keep_ct, data)
# Add content types for copied slides
copy_cts = ''.join( copy_cts = ''.join(
f'<Override PartName="/ppt/slides/slide{copy_num}.xml"' f'<Override PartName="/ppt/slides/slide{copy_num}.xml"'
f' ContentType="{SLIDE_CT}"/>' f' ContentType="{SLIDE_CT}"/>'
@@ -400,8 +388,8 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
# ── Write copied slide files ────────────────────────────────────── # ── Write copied slide files ──────────────────────────────────────
for copy_rid, (src_info, replacements, template, copy_num) in copies.items(): for copy_rid, (src_info, replacements, template, copy_num) in copies.items():
src_path = src_info[0] # e.g. ppt/slides/slide12.xml src_path = src_info[0]
src_rels = src_info[1] # e.g. ppt/slides/_rels/slide12.xml.rels src_rels = src_info[1]
dest_path = f'ppt/slides/slide{copy_num}.xml' dest_path = f'ppt/slides/slide{copy_num}.xml'
dest_rels = f'ppt/slides/_rels/slide{copy_num}.xml.rels' dest_rels = f'ppt/slides/_rels/slide{copy_num}.xml.rels'
@@ -412,7 +400,6 @@ def generate(slide_plan, output_path, media=None, page_numbers=True):
dst.writestr(dest_path, data) dst.writestr(dest_path, data)
print(f" ✓ {template:30s} (copy → slide{copy_num}.xml)") print(f" ✓ {template:30s} (copy → slide{copy_num}.xml)")
# Copy the rels file too (same media references)
if src_rels in src.namelist(): if src_rels in src.namelist():
dst.writestr(dest_rels, src.read(src_rels)) dst.writestr(dest_rels, src.read(src_rels))
@@ -462,80 +449,73 @@ if __name__ == '__main__':
]), ]),
("process-timeline", [ ("process-timeline", [
("WORKFLOW", "MAART \u2192 APRIL 2026"), ("WORKFLOW", "MAART → APRIL 2026"),
("Van ontwerp tot levering", "Een maand vol events en verbinding"), ("Van ontwerp tot levering", "Een maand vol events en verbinding"),
("Upload", "Ronald McDonald"), ("Upload", "Ronald McDonald"),
("Upload je ontwerp of gebruik onze online editor.", ("Upload je ontwerp of gebruik onze online editor.",
"Team kookte op 25 maart voor gezinnen bij het Ronald McDonald Huis."), "Team kookte op 25 maart voor gezinnen bij het Ronald McDonald Huis."),
("Designcheck", "HelloBoxing"), ("Designcheck", "HelloBoxing"),
("Automatische controle op resolutie, afloop en kleuren.", ("Automatische controle op resolutie, afloop en kleuren.",
"Bokslessen hernomen \u2014 bi-weekly op donderdag, start 16 april."), "Bokslessen hernomen — bi-weekly op donderdag, start 16 april."),
("Productie", "CEO Bezoek"), ("Productie", "CEO Bezoek"),
("We matchen je order aan de beste supplier.", ("We matchen je order aan de beste supplier.",
"17 CEO\u2019s aanwezig op 2 april voor Michael\u2019s AI-talk in the living."), "17 CEO's aanwezig op 2 april voor Michael's AI-talk in the living."),
("Levering", "Strategy Days"), ("Levering", "Strategy Days"),
("Track & trace tot aan je deur. Gemiddeld 5 werkdagen.", ("Track & trace tot aan je deur. Gemiddeld 5 werkdagen.",
"Twee dagen strategische afstemming op 14 & 15 april."), "Twee dagen strategische afstemming op 14 & 15 april."),
("Gemiddelde levertijd: ", "Keynote: "), ("Gemiddelde levertijd: ", "Keynote: "),
("5 werkdagen in NL/BE", "vrijdag 17 april om 16:30"), ("5 werkdagen in NL/BE", "vrijdag 17 april om 16:30"),
("Express: 2 werkdagen \u2192", "Met de laatste strategy updates \u2192"), ("Express: 2 werkdagen →", "Met de laatste strategy updates →"),
]), ]),
("big-number", [ ("big-number", [
("Q1 HIGHLIGHT", "CEO BEZOEK"), ("Q1 HIGHLIGHT", "CEO BEZOEK"),
("+42%", "17"), ("+42%", "17"),
("Year-over-year growth in new customer acquisition", ("Year-over-year growth in new customer acquisition",
"CEO\u2019s aanwezig bij Michael\u2019s AI-talk in the Rotterdam living"), "CEO's aanwezig bij Michael's AI-talk in de Rotterdam living"),
("Driven by marketplace expansion into 3 new markets and a 28% improvement in organic search visibility across all locales.", ("Driven by marketplace expansion into 3 new markets and a 28% improvement in organic search visibility across all locales.",
"Op 2 april verwelkomde het Rotterdam kantoor 17 CEO\u2019s voor een sessie over AI en de toekomst van ons vak."), "Op 2 april verwelkomde het Rotterdam kantoor 17 CEO's voor een sessie over AI en de toekomst van ons vak."),
]), ]),
# USP cards — each card now has individual content (nth-occurrence targeting)
("usp-4-cards", [ ("usp-4-cards", [
("WAAROM HELLOPRINT", "MENSEN & CULTUUR"), ("WAAROM HELLOPRINT", "MENSEN & CULTUUR"),
("Vier redenen om voor ons te kiezen", ("Vier redenen om voor ons te kiezen",
"Community moments deze maand"), "Community moments deze maand"),
# Card 1 — Armeense lunch
("Laagste prijs", "Armeense lunch", 1), ("Laagste prijs", "Armeense lunch", 1),
("garantie", "Tolma & Ghapama", 1), ("garantie", "Tolma & Ghapama", 1),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", ("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.",
"Sona nodigde iedereen uit voor een Armeense lunch op 10 april.", 1), "Sona nodigde iedereen uit voor een Armeense lunch op 10 april.", 1),
("30%", "10 apr", 1), ("30%", "10 apr", 1),
("goedkoper dan traditioneel", "Armeens lunchen", 1), ("goedkoper dan traditioneel", "Armeens lunchen", 1),
# Card 2 — Easter cake
("Laagste prijs", "Easter Cake", 2), ("Laagste prijs", "Easter Cake", 2),
("garantie", "Carrot Cake", 2), ("garantie", "Carrot Cake", 2),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", ("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.",
"Saskia bakte een worteltaart voor Pasen \u2014 op beiden verdiepingen!",2), "Saskia bakte een worteltaart voor Pasen — op beiden verdiepingen!", 2),
("30%", "2 apr", 2), ("30%", "2 apr", 2),
("goedkoper dan traditioneel", "Paastraktatie", 2), ("goedkoper dan traditioneel", "Paastraktatie", 2),
# Card 3 — Chinese snacks
("Laagste prijs", "Chinese Snacks", 3), ("Laagste prijs", "Chinese Snacks", 3),
("garantie", "Sachima & Mahua", 3), ("garantie", "Sachima & Mahua", 3),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", ("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.",
"Yaqi bracht traditionele Chinese snacks mee van haar vakantie.", 3), "Yaqi bracht traditionele Chinese snacks mee van haar vakantie.", 3),
("30%", "16 apr", 3), ("30%", "16 apr", 3),
("goedkoper dan traditioneel", "Internationaal genieten", 3), ("goedkoper dan traditioneel", "Internationaal genieten", 3),
# Card 4 — Dennis' book
("Laagste prijs", "Kinderboek Launch", 4), ("Laagste prijs", "Kinderboek Launch", 4),
("garantie", "My New Parents", 4), ("garantie", "My New Parents", 4),
("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.", ("All-inclusive prijzen zonder verborgen kosten. Vind je het goedkoper? Wij matchen het.",
"Dennis\u2019 vrouw lanceerde haar eerste kinderboek \u2014 met de hand geschreven en getekend!", "Dennis' vrouw lanceerde haar eerste kinderboek — met de hand geschreven en getekend!", 4),
4),
("30%", "9 apr", 4), ("30%", "9 apr", 4),
("goedkoper dan traditioneel", "NL & EN editie", 4), ("goedkoper dan traditioneel", "NL & EN editie", 4),
]), ]),
("testimonial", [ ("testimonial", [
("Helloprint heeft ons de flexibiliteit gegeven om snel te schakelen. Van 500 visitekaartjes tot 50.000 brochures \u2014 alles wordt op tijd en perfect geleverd.", ("Helloprint heeft ons de flexibiliteit gegeven om snel te schakelen. Van 500 visitekaartjes tot 50.000 brochures — alles wordt op tijd en perfect geleverd.",
"Rolling dolma boosts productivity by 89%. No proof\u2026 but worth testing! Kom naar boven om mee te helpen met Tolma en Ghapama voor de Armeense lunch."), "Rolling dolma boosts productivity by 89%. No proof... but worth testing! Kom naar boven om mee te helpen met Tolma en Ghapama voor de Armeense lunch."),
("Martine Verhoeven", "Sona Hovhannisyan"), ("Martine Verhoeven", "Sona Hovhannisyan"),
("Marketing Director \u2014 TechCorp BV", ("Marketing Director — TechCorp BV",
"People & Culture \u2014 Helloprint Rotterdam"), "People & Culture — Helloprint Rotterdam"),
("MV", "SH"), ("MV", "SH"),
]), ]),
# Second section-divider — duplicate template, now supported
("section-divider", [ ("section-divider", [
("02", "02"), ("02", "02"),
("Financial Performance", "Upcoming"), ("Financial Performance", "Upcoming"),
@@ -547,4 +527,4 @@ if __name__ == '__main__':
] ]
print("Generating: Office Rotterdam — Maandoverzicht\n") print("Generating: Office Rotterdam — Maandoverzicht\n")
generate(plan, "/sessions/brave-bold-cerf/mnt/outputs/OfficeRotterdam_Maandoverzicht_Apr2026.pptx") generate(plan, "/Users/derkdissselhof/Library/Application Support/Claude/local-agent-mode-sessions/a2d0567c-5a2d-4037-a100-87a5e74b9a55/3f471e31-d031-4ffe-bb3a-282799a05800/local_0cbc5cf3-74a0-4400-80d7-33c648a960dc/outputs/OfficeRotterdam_Maandoverzicht_Apr2026.pptx")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB