Google Slides Presentation Generator
Build polished, branded Google Slides presentations by generating a Python
script that calls the Slides API batchUpdate via the gws CLI.
When to Use
- "Create a presentation about X"
- "Make me a slide deck for Y"
- "Build Google Slides for Z"
- "Generate a deck on topic T"
- Any request where the deliverable is a Google Slides presentation.
Architecture — Two Approaches
Use Approach A (REST API via gws) by default. Everything runs inside Cursor — no browser steps, link delivered in chat. Use Approach B (Apps Script) only when the user explicitly requests it or needs advanced features like inserting images from URLs.
Approach A — REST API via gws (Default)
This approach generates a Python script that calls the Slides REST API
batchUpdate via the gws CLI. Everything stays inside Cursor.
A1 — Gather Content
Before writing any code, understand:
- Topic and audience — who will see the deck and what is the goal
- Source material — documents, notes, data the user has shared
- Branding — default to Red Hat branding unless user specifies otherwise
- Slide count — aim for 10-15 slides for a strategic deck
- Color mode — always ask the user: "Light mode or dark mode?" before generating any slides. Do not assume a default — wait for their answer.
Synthesize all source material into a clear narrative structure before touching any code. Outline the slide titles and key content for each.
A2 — Create the Presentation (Blank Slides + Manual Branding)
Create a fresh blank Google Slides presentation — do NOT copy the Red Hat
template. The template master (simple-light-2) does not support
predefinedLayout: "BLANK", which causes 400 errors. Red Hat Display and
Red Hat Text are Google Fonts and load correctly in any fresh Slides file.
Save the presentation ID. The fresh presentation starts with one default slide:
r = subprocess.run(['gws', 'slides', 'presentations', 'get',
'--params', json.dumps({
'presentationId': PRES_ID,
'fields': 'slides(objectId)'
})], capture_output=True, text=True)
lines = r.stdout.strip().split('\n')
start = next((i for i,l in enumerate(lines) if l.strip().startswith('{')), 0)
data = json.loads('\n'.join(lines[start:]))
existing_slides = [s['objectId'] for s in data.get('slides', [])]
Create slides using predefinedLayout: "BLANK" only — and ALWAYS set background:
def create_blank_slide(slide_id):
"""ALWAYS use predefinedLayout BLANK. NEVER use layoutId from template."""
return {"createSlide": {
"objectId": slide_id,
"slideLayoutReference": {"predefinedLayout": "BLANK"}
}}
def slide_bg(slide_id, color):
"""Set explicit slide background. MUST be called immediately after create_blank_slide."""
return {"updatePageProperties": {
"objectId": slide_id,
"pageProperties": {
"pageBackgroundFill": {
"solidFill": {"color": {"rgbColor": color}, "alpha": 1.0}
}
},
"fields": "pageBackgroundFill.solidFill.color,pageBackgroundFill.solidFill.alpha"
}}
MANDATORY SLIDE CREATION PATTERN — follow this for EVERY slide:
# Step 1: Set BG_PRIMARY at the top of the script based on COLOR_MODE
COLOR_MODE = "light" # or "dark" — set once, use everywhere
if COLOR_MODE == "light":
BG_PRIMARY = {"red": 1.0, "green": 1.0, "blue": 1.0} # WHITE
TEXT_PRIMARY = {"red": 0.082, "green": 0.082, "blue": 0.082} # GRAY_95
TEXT_SECONDARY= {"red": 0.302, "green": 0.302, "blue": 0.302} # GRAY_60
elif COLOR_MODE == "dark":
BG_PRIMARY = {"red": 0.082, "green": 0.082, "blue": 0.082} # GRAY_95
TEXT_PRIMARY = {"red": 1.0, "green": 1.0, "blue": 1.0} # WHITE
TEXT_SECONDARY= {"red": 0.639, "green": 0.639, "blue": 0.639} # GRAY_40
ACCENT = {"red": 0.933, "green": 0.0, "blue": 0.0} # RH_RED
# Step 2: For every content slide, create and immediately set background:
slide_id = uid()
reqs.append(create_blank_slide(slide_id)) # 1. BLANK layout — never layoutId
reqs.append(slide_bg(slide_id, BG_PRIMARY)) # 2. ALWAYS set bg — prevents master bleedthrough
# 3. Add content shapes...
For special slides that override the color (title, closing):
title_id = uid()
reqs.append(create_blank_slide(title_id))
reqs.append(slide_bg(title_id, ACCENT)) # Red override for title/closing only
The template master's background MUST NEVER control any slide's color. The script owns every slide's background, always.
NEVER use layoutId pointing to a named template layout (Interior blank,
Interior agenda, Interior callout, etc.) — they carry inherited background
and positional constraints that break both color mode and margins. Use ONLY
predefinedLayout: "BLANK".
This gives you a completely empty canvas with zero placeholder elements and zero inherited constraints. Red Hat fonts are inherited from the master.
Add branding manually to each slide:
def add_red_accent_bar(reqs, slide_id):
"""Red accent bar at top of content slides (full width, 0.06" tall)."""
sid = uid()
reqs.append({"createShape": {
"objectId": sid,
"shapeType": "RECTANGLE",
"elementProperties": {
"pageObjectId": slide_id,
"size": {"width": {"magnitude": 9144000, "unit": "EMU"},
"height": {"magnitude": 54864, "unit": "EMU"}},
"transform": {"scaleX": 1, "scaleY": 1,
"translateX": 0, "translateY": 0, "unit": "EMU"}
}
}})
reqs.append({"updateShapeProperties": {
"objectId": sid,
"fields": "shapeBackgroundFill.solidFill.color",
"shapeProperties": {"shapeBackgroundFill": {"solidFill": {
"color": {"rgbColor": {"red": 0.933, "green": 0.0, "blue": 0.0}}
}}}
}})
reqs.append({"updateShapeProperties": {
"objectId": sid,
"fields": "outline.outlineFill.solidFill.color,outline.weight",
"shapeProperties": {"outline": {"outlineFill": {"solidFill": {
"color": {"rgbColor": {"red": 0.933, "green": 0.0, "blue": 0.0}}
}}, "weight": {"magnitude": 0, "unit": "EMU"}}}
}})
Do NOT use template content layouts (Interior agenda, Interior callout, Interior two column, etc.) — they contain placeholder elements that show through as "Click to add subtitle" and cannot be reliably removed via the API. Build all layouts from scratch with shapes and text boxes.
IMPORTANT — Template does NOT support predefinedLayout: "BLANK":
The Red Hat template's master (simple-light-2) does NOT include predefinedLayout: "BLANK" as a valid layout. Using it will cause a 400 error. Therefore:
- Do NOT try to use
predefinedLayout: "BLANK"on a presentation copied from the Red Hat template - Instead, create a fresh blank Google Slides presentation using
gws slides presentations create - Red Hat Display and Red Hat Text are Google Fonts — they render correctly in any Slides file, so a fresh presentation is fully equivalent for branding purposes
Always create a fresh blank presentation:
gws slides presentations create --json '{"title": "Your Title Here"}' 2>&1 | \
python3 -c "
import sys, json
lines = sys.stdin.readlines()
start = next(i for i, l in enumerate(lines) if l.strip().startswith('{'))
data = json.loads(''.join(lines[start:]))
print(data.get('presentationId'))
"
The default first slide has objectId: "p" — delete it after creating your first real slide.
A3 — Write the Builder Script
Create a Python script at <workspace>/slides-script/build_slides.py.
Copy helpers.py into the same directory and import everything from it.
⚠️ MANDATORY: Every build script MUST start with this exact template:
#!/usr/bin/env python3
"""Google Slides builder script — generated from presentation skill."""
import sys
import os
# Copy helpers.py to the same directory as this script, then import:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from helpers import *
# ---- Configuration ----
COLOR_MODE = "light" # "light", "dark", or "expressive_dark"
setup_color_mode(COLOR_MODE)
# ---- Create presentation ----
PRES_ID = create_presentation("Your Title Here")
# ---- Build slides ----
reqs = []
# Slide 1: Title slide — MANDATORY build_title_slide() pattern, see below
s1 = uid()
reqs.append(create_slide(s1))
build_title_slide(reqs, s1, "Your Deck Title", "Optional deck subtitle",
presenter_name="Presenter Name", presenter_role="Title, Red Hat")
# Slide 2+: Content slides — ALWAYS use this pattern:
s2 = uid()
reqs.append(create_slide(s2))
reqs.append(slide_bg(s2, BG_PRIMARY))
add_red_accent_bar(reqs, s2)
add_slide_title(reqs, s2, "Your Slide Title")
add_slide_subtitle(reqs, s2, "Optional subtitle")
# ... add content using add_content_panel(), add_content_text(), etc. ...
add_rh_logo(reqs, s2, COLOR_MODE)
add_slide_number(reqs, s2, 2)
# Last slide: Thank You — MANDATORY build_thank_you_slide() pattern, see below
s_last = uid()
reqs.append(create_slide(s_last))
build_thank_you_slide(reqs, s_last, slide_num=3)
# Delete the default blank slide
reqs.append(delete_default_slide(PRES_ID))
# ---- Send ----
send_batch(PRES_ID, reqs)
print(f"https://docs.google.com/presentation/d/{PRES_ID}")
DO NOT redefine ANY constants or functions that exist in helpers.py. The helpers module provides: all layout constants (SLIDE_W, CONTENT_TOP_Y, COL2_W, etc.), all color constants (RH_RED, GRAY_95, etc.), all mandatory functions (add_slide_title, add_content_panel, add_rh_logo, etc.), all slide build patterns (build_two_column_slide, etc.), and the batch sender.
DO NOT create shapes with raw coordinates. Use the provided functions:
add_slide_title()— title at correct positionadd_slide_subtitle()— subtitle at correct positionadd_content_panel()— background panel with boundary enforcementadd_content_text()— text box with auto-height and boundary enforcementadd_red_accent_bar()— accent bar at topadd_rh_logo()— logo at bottom-rightadd_slide_number()— slide number at bottom-leftbuild_two_column_slide()— complete two-column slidebuild_three_column_slide()— complete three-column slidebuild_four_column_slide()— complete four-column slidebuild_card_grid_slide()— complete 2×3 card grid slidebuild_title_slide()— complete first slide (MANDATORY, see below)build_thank_you_slide()— complete last slide (MANDATORY, see below)build_agenda_slide()— complete agenda slide with icon-or-badge items (MANDATORY icon lookup first, see A4b below)build_split_image_slide()— text bullets + image side-by-side (v2.0)build_diagram_image_slide()— embed Rich Draw.io v4 exported PNGs as full-width diagram slides (v2.0)create_connector()— connected arrow between two shapes (v2.0)add_icon_overlay()— place small Red Hat product icons on diagram slides (v3.0)
Key rules for the builder script:
Delete template sample slides — remove the template's sample slides before adding your own (see A2). If using fallback blank mode, delete the default slide with
{"deleteObject": {"objectId": "p"}}Use BLANK slides only — always create slides with
predefinedLayout: "BLANK". NEVER uselayoutIdpointing to any named template layout (even "Interior blank") — those carry inherited backgrounds and positional constraints that break color mode and margins. OnlypredefinedLayout: "BLANK"gives a true empty canvas.Always set BG_PRIMARY immediately after create_blank_slide — call
slide_bg(slide_id, BG_PRIMARY)as the very next request after creating each slide. Without this, the template master's dark background bleeds through regardless of COLOR_MODE. Only override with a different color (e.g., ACCENT/red) on intentionally colored slides (title, closing).Object IDs — generate with
"e" + uuid.uuid4().hex[:10](must start with a letter)Units — all positions/sizes in EMU (1 inch = 914400 EMU, 1 pt = 12700 EMU)
Slide dimensions — widescreen: 10" × 5.625" (9144000 × 5143500 EMU). This is the DEFAULT Google Slides size. Do NOT change the page size — all coordinates in this skill are calibrated for 10" × 5.625". NEVER use 13.33" × 7.5" coordinates
Empty text — never call
insertTextwith empty string, never callupdateTextStyleon a shape with no text. Guard withif text:before text operationsChunk size — send batchUpdate in chunks of 200 requests max
Shape types — use
TEXT_BOXfor text,RECTANGLEfor boxes,ROUND_RECTANGLEfor badges/buttons,ELLIPSEfor dotsText overflow prevention — MANDATORY for every TEXT_BOX and RECTANGLE with text:
shrinkTextOnOverflow: true— set on EVERY shape that contains text, no exceptions- Calculate height using
calc_box_height()— see MANDATORY Content Sizing Rules section below - Never hardcode text box heights — always derive from content length and font size
- Add autofit properties after creating each text shape:
def set_text_autofit(shape_id):
"""MUST be called for every shape that contains text."""
return {"updateShapeProperties": {
"objectId": shape_id,
"fields": "autofit",
"shapeProperties": {
"autofit": {"autofitType": "TEXT_AUTOFIT"}
}
}}
- Content budget per slide — before building any slide, compute total content height:
- Title: 0.50" at TITLE_Y (separate from content zone — handled by
add_slide_title()) - Subtitle: 0.25" at SUBTITLE_Y (separate — handled by
add_slide_subtitle()) - Content zone: 3.80" (CONTENT_TOP_Y=1.00" to CONTENT_BOT_Y=4.80")
- Each bullet line: font_size_pt × 1.5 / 72 inches
- Spacing between sections: 0.2"
- Content MUST NOT exceed 3.80" total height
- If total exceeds 3.80", split the slide into two slides BEFORE building
- Title: 0.50" at TITLE_Y (separate from content zone — handled by
- Red Hat logo on every content slide — every content slide (not title, not Thank You) must have the Red Hat logo in the bottom-right corner, via
add_rh_logo(reqs, slide_id, color_mode). The logo is now a single, real wordmark IMAGE (hat icon + "Red Hat" text baked into one PNG, extracted from the official Red Hat Slides template) — two color variants (RH_LOGO_URL_WHITE/RH_LOGO_URL_BLACK) picked automatically bycolor_mode. Both are hosted on a plain public GitHub repo (raw.githubusercontent.com), which serves anonymous HTTPS GETs with no auth/token/expiry — this is what fixed the recurring "logo renders as plain text" defect, whose root cause was a tokenized/expiringgoogleusercontent.comURL (see Gotcha #3 below).add_rh_logo(),add_rh_logo_image(), and the constants (RH_LOGO_URL_WHITE/BLACK,RH_LOGO_W/H/X/Y) all live inhelpers.py— do not redefine them in a build script.
Logo placement rules:
Position: bottom-right corner of every content slide (
RH_LOGO_X/Y, unchanged across redesigns so footer-zone math elsewhere never needs to move)Always call
add_rh_logo(reqs, slide_id, COLOR_MODE)— it auto-selects white-text vs. black-text based oncolor_modeIf
createImageever fails despite the stable URL, fall back toadd_rh_logo_text()(native text, no image) rather than skipping the logo entirelyBefore EVER changing
RH_LOGO_URL_WHITE/RH_LOGO_URL_BLACK, verify the new URL empirically first:curl -s -o /dev/null -w "%{http_code}\n" <url>must print200with NO auth headers attached (this is what Google's fetcher does) — do not trust a URL just because it opens in a logged-in browserThe title slide and Thank You slide do NOT call
add_rh_logo()—build_title_slide()/build_thank_you_slide()place the correctly-colored logo image internallyTitle slide (slide 1 of every deck) — MANDATORY: use
build_title_slide(reqs, slide_id, deck_title, deck_subtitle=None, presenter_name=None, presenter_role=None)fromhelpers.py. NEVER hand-build this slide withadd_slide_title()/add_red_accent_bar()/raw shapes — those are content-slide-only conventions and will not match the brand template. This function reproduces the official Red Hat title-slide template pixel-for-pixel (verified by rendering a real test presentation and comparing thumbnails):- Full-bleed two-tone illustration background (
TITLE_SLIDE_BG_URL, viaslide_bg_image()) - White decorative accent bar, bottom-left
- Optional deck title (32pt bold) + subtitle (16pt) in the right-hand dark-red panel
- Optional presenter name (16pt bold) + role (13pt regular) below the title
- White wordmark logo, bottom-right (
RH_LOGO_URL_WHITE) - Do NOT add a slide number to the title slide
- Full-bleed two-tone illustration background (
Thank You slide (last slide of every deck, NOT "closing slide") — MANDATORY: use
build_thank_you_slide(reqs, slide_id, slide_num, body_text=None, social_links=None)fromhelpers.py. NEVER hand-build this slide. This function reproduces the official Red Hat closing-slide template pixel-for-pixel:- Full-bleed red (left ~62%) / white-footer (bottom strip) background (
CLOSING_SLIDE_BG_URL, viaslide_bg_image()) - Maroon decorative accent bars, top-left (tall) and bottom-left (short)
- "Thank you" headline (40pt, white, Red Hat Display)
- Body paragraph (12pt white) — defaults to standard Red Hat boilerplate if
body_textomitted - Up to 4 social-link rows (icon + text), right side — defaults to Red Hat's official LinkedIn/YouTube/Facebook/Twitter if
social_linksomitted; pass your own[(network, label), ...]list (network must be one of the keys inSOCIAL_ICON_URLS) to customize - Slide number + black-text wordmark logo on the white footer band
- Full-bleed red (left ~62%) / white-footer (bottom strip) background (
Example:
build_title_slide(reqs, s1, "SBI Shared File-System Analysis",
"GFS2 vs. IBM Storage Scale",
presenter_name="Nirjhar Jajodia",
presenter_role="Adoption Architect, Red Hat")
...
build_thank_you_slide(reqs, s_last, slide_num=len(all_slides))
A4 — Execute
cd <workspace>/slides-script && python3 build_slides.py
The script calls gws as a subprocess. Verify all chunks succeed.
On error, read the error message (usually wrong objectId or text styling
on empty shape), fix the script, create a new presentation, and re-run.
If a run fails partway, do NOT retry on the same presentation — create a fresh one and delete the broken one:
gws drive files delete --params '{"fileId": "<broken-id>"}'
A4b — Add Icons
For any Agenda slide, icon lookup is MANDATORY-FIRST, not optional: before writing the agenda slide's batchUpdate requests, you MUST search the Red Hat Icon Repository for a matching icon per agenda item. Only fall back to numbered circular badges (see the Agenda slide pattern below) if the Icon Repository genuinely has no reasonable match after searching — never default to badges/dots just because it's less work. This is the fix for the recurring "agenda slides use red dots instead of icons" defect.
For every other icon use (product/tech accents, diagram labels, etc.), sourcing from the Icon Repository remains optional/as-needed.
Icon Repository ID: 1SRhy8-bYBgaA3Jsi1t_Fxz-Yo9ORgdRy5Kec9hg_wSM
See references/icon-inventory.md for the full inventory and extraction pattern.
To insert an icon into your presentation:
- Get the icon's image contentUrl from the Icon Repository
- Use
createImagein your batchUpdate to place it on the target slide
def add_icon(reqs, slide_id, image_url, left, top, width, height):
"""Insert an icon image from a URL."""
sid = uid()
reqs.append({"createImage": {
"objectId": sid,
"url": image_url,
"elementProperties": {
"pageObjectId": slide_id,
"size": {
"width": {"magnitude": width, "unit": "EMU"},
"height": {"magnitude": height, "unit": "EMU"}
},
"transform": {
"scaleX": 1, "scaleY": 1,
"translateX": left, "translateY": top,
"unit": "EMU"
}
}
}})
return sid
Use icons sparingly -- 1-2 per slide maximum. Best uses: stat slide accents, feature list markers, architecture diagram labels, comparison column headers.
A4b — Known API Gotchas (MUST READ)
These are confirmed issues with the Google Slides batchUpdate API that will cause build failures if not handled:
TEXT_AUTOFIT is not supported —
updateShapePropertieswithautofit.autofitType: "TEXT_AUTOFIT"returns error "Autofit types other than NONE are not supported." Theset_text_autofit()helper is a no-op. Overflow prevention relies entirely oncalc_box_height()sizing. Keep text boxes generously sized (5-6 bullets max per box).Outline weight 0 is invalid —
outline.weight: 0returns error "should not be less than or equal to zero." To hide borders, useshape_no_border()which setsoutline.propertyState: "NOT_RENDERED".Drive/googleusercontent URLs fail or expire in createImage — URLs like
https://lh3.googleusercontent.com/d/FILE_IDordrive.google.com/uc?export=downloadrequire public sharing and still often fail with "Access to the provided image was forbidden," or work once and then break later. ThecontentUrlvalues extracted from existing Google Slides presentations (Icon Repository, Red Hat template) are Google-hosted, but they are tokenized and expire (documented ~30 min window) — fine for a one-off same-session insert, NOT safe to hardcode intohelpers.pyfor reuse across builds. Preferred fix: host the static asset yourself on a plain public GitHub repo and reference it viaraw.githubusercontent.com/<user>/<repo>/<branch>/<path>. That host serves anonymous HTTPS GETs with no auth/token/expiry — the exact conditionscreateImageneeds. SeeRH_LOGO_URL_WHITE/_BLACK,TITLE_SLIDE_BG_URL,CLOSING_SLIDE_BG_URL, andSOCIAL_ICON_URLSinhelpers.pyfor working examples, all hosted on thenirjhar17/slide-assetspublic repo. Whichever host you use, ALWAYS verify empirically before wiring a URL in:curl -s -o /dev/null -w "%{http_code}\n" <url>must print200with no auth headers attached — do not trust "it opens in my browser." This exact discipline (realcreateImagecall + thumbnail render + pixel comparison against the reference template) was used to validatebuild_title_slide()/build_thank_you_slide()before they were adopted here.Icon search terms must match actual titles — The Icon Repository icons have titles like
Technology_icon-Red_Hat-Ansible_Automation_Platform-Standard-RGB.png,Icon-Red_Hat-IT_modernization-Red-RGB.Large-icon.png, etc. Search by these exact substrings, not generic terms like "clock" or "arrow."
A5 — Deliver
Return the presentation URL to the user:
https://docs.google.com/presentation/d/<PRES_ID>
Remind them to:
- Add speaker notes if presenting live
- Review any sensitive names/data before sharing externally
- Logos are included via the template master slides; add product-specific logos manually if needed beyond what the template provides
Approach B — Google Apps Script (Advanced / On Request)
Use only when the user explicitly requests Apps Script or needs advanced features like inserting images from URLs. Requires first-time browser authorization — not seamless from Cursor.
B1 — Create an Apps Script Project
gws script projects create --json '{"title": "slide-builder"}' 2>&1 | \
python3 -c "
import sys, json
lines = sys.stdin.readlines()
start = next(i for i, l in enumerate(lines) if l.strip().startswith('{'))
data = json.loads(''.join(lines[start:]))
print(data.get('scriptId'))
"
B2 — Write Code.gs
Apps Script uses SlidesApp — simpler code, positions in points (not EMU),
chainable styling. Can insert images via slide.insertImage(url).
B3 — Push and Execute
gws script projects updateContent --params '{"scriptId": "<SCRIPT_ID>"}' \
--json '{"files": [{"name":"Code","type":"SERVER_JS","source":"<code>"}]}'
First run requires browser authorization at:
https://script.google.com/d/<SCRIPT_ID>/edit
B4 — Deliver
Return the presentation URL from the script output.
Design System
Color Mode
Before generating any slides, always ask the user which color mode they prefer. Do not assume a default — wait for their answer:
- Light mode — clean white backgrounds, dark text, best for print and email sharing
- Dark mode — cinematic dark backgrounds, white text, best for presenting on screen
- Expressive Dark mode — purple/teal-accented dark backgrounds, more colorful and energetic, best for creative or forward-looking topics
Set a COLOR_MODE variable at the top of the builder script. All slide
background, text, and accent colors derive from this choice.
Color Palette (Red Hat Brand)
Core colors (used in both modes):
# Brand red
RH_RED = {"red": 0.933, "green": 0.0, "blue": 0.0} # #ee0000 red-50
RH_RED_DARK = {"red": 0.651, "green": 0.0, "blue": 0.0} # #a60000 red-60
RH_RED_LIGHT = {"red": 0.961, "green": 0.431, "blue": 0.431} # #f56e6e red-40
RH_RED_TINT = {"red": 0.988, "green": 0.890, "blue": 0.890} # #fce3e3 red-10
# Grays
RH_DARK = {"red": 0.102, "green": 0.102, "blue": 0.102} # #1a1a1a
GRAY_95 = {"red": 0.082, "green": 0.082, "blue": 0.082} # #151515
GRAY_90 = {"red": 0.122, "green": 0.122, "blue": 0.122} # #1f1f1f
GRAY_80 = {"red": 0.161, "green": 0.161, "blue": 0.161} # #292929
GRAY_60 = {"red": 0.302, "green": 0.302, "blue": 0.302} # #4d4d4d
RH_GRAY = {"red": 0.29, "green": 0.29, "blue": 0.29} # #4a4a4a
GRAY_40 = {"red": 0.639, "green": 0.639, "blue": 0.639} # #a3a3a3
GRAY_20 = {"red": 0.878, "green": 0.878, "blue": 0.878} # #e0e0e0
GRAY_10 = {"red": 0.949, "green": 0.949, "blue": 0.949} # #f2f2f2
RH_LIGHT_GRAY = {"red": 0.96, "green": 0.96, "blue": 0.96} # #f5f5f5
WHITE = {"red": 1.0, "green": 1.0, "blue": 1.0} # #ffffff
# Teal
TEAL_10 = {"red": 0.855, "green": 0.949, "blue": 0.949} # #daf2f2
TEAL_40 = {"red": 0.388, "green": 0.741, "blue": 0.741} # #63bdbd
TEAL_50 = {"red": 0.216, "green": 0.639, "blue": 0.639} # #37a3a3
TEAL_60 = {"red": 0.078, "green": 0.471, "blue": 0.471} # #147878
# Purple
PURPLE_10 = {"red": 0.925, "green": 0.902, "blue": 1.0} # #ece6ff
PURPLE_40 = {"red": 0.529, "green": 0.435, "blue": 0.831} # #876fd4
PURPLE_50 = {"red": 0.369, "green": 0.251, "blue": 0.745} # #5e40be
PURPLE_60 = {"red": 0.239, "green": 0.153, "blue": 0.522} # #3d2785
# Orange
ORANGE_10 = {"red": 1.0, "green": 0.910, "blue": 0.800} # #ffe8cc
ORANGE_40 = {"red": 0.961, "green": 0.573, "blue": 0.106} # #f5921b
ORANGE_50 = {"red": 0.792, "green": 0.424, "blue": 0.059} # #ca6c0f
ORANGE_60 = {"red": 0.620, "green": 0.290, "blue": 0.024} # #9e4a06
# Yellow
YELLOW_10 = {"red": 1.0, "green": 0.957, "blue": 0.800} # #fff4cc
YELLOW_40 = {"red": 0.863, "green": 0.651, "blue": 0.078} # #dca614
YELLOW_50 = {"red": 0.725, "green": 0.518, "blue": 0.071} # #b98412
YELLOW_60 = {"red": 0.588, "green": 0.392, "blue": 0.059} # #96640f
# Utility
RH_BLUE = {"red": 0.0, "green": 0.4, "blue": 0.8} # #0066cc
RH_GREEN = {"red": 0.243, "green": 0.525, "blue": 0.208} # #3e8635
Dark mode palette (when dark mode chosen):
BG_PRIMARY = GRAY_95 # slide backgrounds
BG_SECONDARY = GRAY_80 # alternate/card backgrounds
TEXT_PRIMARY = WHITE # main text
TEXT_SECONDARY = GRAY_40 # subtitles, descriptions
TEXT_MUTED = GRAY_60 # footers, references
ACCENT = RH_RED # highlights, accent bars
Light mode palette:
BG_PRIMARY = WHITE # slide backgrounds
BG_SECONDARY = GRAY_10 # alternate/card backgrounds
TEXT_PRIMARY = GRAY_95 # main text
TEXT_SECONDARY = GRAY_60 # subtitles, descriptions
TEXT_MUTED = RH_GRAY # footers, references
ACCENT = RH_RED # highlights, accent bars
Expressive Dark mode palette (purple/teal accents for creative decks):
PURPLE_80 = {"red": 0.106, "green": 0.051, "blue": 0.200} # #1b0d33
PURPLE_70 = {"red": 0.129, "green": 0.075, "blue": 0.302} # #21134d
BLACK = {"red": 0.0, "green": 0.0, "blue": 0.0} # #000000
PURPLE_20 = {"red": 0.816, "green": 0.773, "blue": 0.957} # #d0c5f4
PURPLE_30 = {"red": 0.714, "green": 0.651, "blue": 0.914} # #b6a6e9
BG_PRIMARY = PURPLE_80 # slide backgrounds
BG_SECONDARY = BLACK # alternate/card backgrounds
BG_SURFACE = PURPLE_70 # card/panel backgrounds
TEXT_PRIMARY = WHITE # main text
TEXT_SECONDARY = PURPLE_20 # subtitles, descriptions
TEXT_MUTED = PURPLE_30 # footers, references
ACCENT = RH_RED # highlights, accent bars
HIGHLIGHT_TEAL = TEAL_50 # secondary accent for data, positive indicators
HIGHLIGHT_PURPLE = PURPLE_40 # tertiary accent for tags, categories
Use Expressive Dark for topics like AI, innovation, future strategy, or creative workshops where a more energetic visual tone is appropriate. Standard Dark mode remains the default for most corporate presentations.
If the user specifies a different brand, derive a palette from their brand colors using the same light-tint / dark-shade pattern above.
Slide Patterns
Use these proven patterns for professional layouts. Every pattern is built entirely from shapes and text boxes on a BLANK slide — never rely on template layout placeholders.
- Title slide — MANDATORY: use
build_title_slide()fromhelpers.py(see A3 above). Do NOT hand-build this slide. - Agenda slide — MANDATORY: use
build_agenda_slide()fromhelpers.py. Icon lookup is MANDATORY-FIRST (see A4b): search the Icon Repository for EVERY agenda item BEFORE calling this function, and pass each resolvedcontentUrlas that item'sicon_url. NEVER mix icons and numbered badges on the same slide —build_agenda_slide()enforces this: if even one item is missingicon_url, it silently forces ALL items to numbered badges instead of rendering a mixed slide (verified — see below). This means the real work is upstream: keep searching until every item has a reasonable icon, including approximate/metaphorical matches (e.g. a connectivity/link icon for a cross-site replication topic, a generic info icon for a scope/requirements item) — don't settle forNonejust because there's no exact-name match. Only fall back to a fully-badged slide when the topic set genuinely has no reasonable icon coverage at all. Verified by rendering two real test agendas: a mixed one (confirmed broken/inconsistent-looking) and the corrected all-icons version where every one of 4 items — including two initially-"no match" topics — got a real, on-topic icon:
items = [
{"title": "Requirements & Constraints", "description": "...", "icon_url": INFO_ICON_URL},
{"title": "Red Hat GFS2 Architecture", "description": "...", "icon_url": RHEL_ICON_URL},
{"title": "IBM Storage Scale (GPFS)", "description": "...", "icon_url": CONNECTIVITY_ICON_URL}, # metaphorical match: cross-site link
{"title": "Backup, DR & Support Boundaries", "description": "...", "icon_url": BACKUP_ICON_URL},
]
build_agenda_slide(reqs, slide_id, "Agenda", "What we will cover today", items, slide_num=2)
Row slots subdivide the content zone evenly so any item count (2-8) fits without overflow.
- Comparison / two-column — colored header rectangles (one per column, e.g., green vs orange), bullet points below each header as text boxes, background panel rectangles behind each column in pastel tint
- Card grid — 2×2 layout with colored header bars and white body with border
- Timeline (horizontal) — horizontal gray line as axis, colored circles (dots) at each milestone, pastel-filled rectangles above the line for version info, text labels for dates below. Use distinct colors per status (green=active, orange=warning, red=EOL)
- Split columns — left problem / right solution, colored headers
- Metric callouts — rounded-rect with large number + label beside it
- FROM → TO table — alternating rows, red tint for "from", green tint for "to"
- Thank You slide (last slide) — MANDATORY: use
build_thank_you_slide()fromhelpers.py(see A3 above). Do NOT hand-build this slide. - Quote slide — large pull quote (18-22pt Red Hat Display, light weight), attribution below in smaller Red Hat Text, red accent bar on left edge, BG_SECONDARY background
- Big Number / Stat slide — one giant number (36-48pt Red Hat Display Bold, ACCENT color), context label below (10-12pt), optional delta indicator (arrow or +/- in TEAL_50 or RH_RED)
- Flowchart / Decision slide — dark rectangle for decision question at top, colored rectangles for options below, small arrow shapes (triangle/rectangle) connecting elements vertically, 3 destination boxes at bottom
- Upgrade/Process Path diagram — colored background panels per path (pastel), white boxes for each state/step, small filled rectangles as directional arrows between boxes, label text below or beside each path, "RECOMMENDED" badge (green rounded-rect) on preferred path
Additional Visual Patterns
Pattern: Split-Layout with Image (text + illustration)
Use for concept slides where one half explains in bullets and the other half shows an AI-generated illustration, photo, or screenshot.
s = uid()
reqs.append(create_slide(s))
build_split_image_slide(reqs, s,
title="Prefill Is Compute, Decode Is Memory",
subtitle=None,
bullets=[
"PREFILL: processes entire input in parallel on GPU",
"Produces the first token (TTFT metric)",
"DECODE: generates tokens one at a time",
"Memory-bound — limited by KV cache access speed",
],
image_url="https://example.com/illustration.png",
slide_num=4,
color_mode=COLOR_MODE,
image_side="right") # or "left"
Image must be a publicly-fetchable URL (raw.githubusercontent.com is best). The image auto-centers vertically in the content zone and respects the footer boundary.
Pattern: Diagram Image Slide (Rich Draw.io v4 exported PNGs)
Use for embedding Rich Draw.io v4 exported PNG diagrams that should fill most of the content zone. See "Diagram Approach: Rich Draw.io v4" below for how to create the source .drawio files.
s = uid()
reqs.append(create_slide(s))
build_diagram_image_slide(reqs, s,
title="KServe Separates Runtime from Model",
subtitle="Platform teams own ServingRuntime, data scientists own InferenceService",
image_url="https://example.com/kserve-sketch.png",
slide_num=5,
color_mode=COLOR_MODE,
image_scale=0.85) # 0.0-1.0, fraction of content zone
For draw.io sketch diagrams, export at 2x scale for crisp display. Use
sketch=1, curveFitting=1, jiggle=2 in draw.io XML for hand-drawn style.
Set fontColor=#FFFFFF on edge labels when using dark backgrounds.
Pattern: Connected Connectors (node-to-node arrows)
Use create_connector() to draw real connected lines between shapes.
Connection site indices: 0=top, 1=right, 2=bottom, 3=left.
box_a = add_rounded_rect(reqs, slide_id, "Step A", ...)
box_b = add_rounded_rect(reqs, slide_id, "Step B", ...)
create_connector(reqs, slide_id, box_a, box_b,
start_site=2, end_site=0, # bottom of A → top of B
end_arrow="OPEN_ARROW",
line_color=TEXT_MUTED,
weight_pt=1.5)
Arrow styles: "NONE", "OPEN_ARROW", "FILL_ARROW",
"FILL_CIRCLE", "FILL_SQUARE", "FILL_DIAMOND".
Diagram Approach: Rich Draw.io v4
All architecture/flow/pipeline diagrams use hand-crafted draw.io XML exported as high-res PNG images. This is the ONLY approved diagram approach — do NOT use native Slides API shapes for diagrams.
When to Use a Diagram
- Architecture showing components and their relationships
- Pipeline/flow showing data transformation steps
- Any slide that would benefit from visual boxes + arrows
When NOT to Use a Diagram (use other slide types instead)
- Concept explanations → use build_split_image_slide() with AI illustration
- Feature comparisons → use build_two_column_slide() or build_card_slide()
- Simple lists → use build_bullet_slide()
Diagram Style Rules
- Dark purple background: Set
background="#1b0d33"in the mxGraphModel - Use proper industry shapes based on component type:
- Databases/storage:
shape=cylinder3(cylinder) - Cloud services:
shape=cloud - Documents/files:
shape=document - Processing/transforms:
shape=process(box with side bars) - Users/clients:
ellipse(circle/oval) - K8s resources:
rounded=1rectangle with badge - Containers/boundaries: dashed border container (
dashed=1;dashPattern=5 5;verticalAlign=top;)
- Databases/storage:
- Color palette:
- Teal (#37a3a3, #009DA5) for primary/data components
- Red (#EE0000) for critical/accent (model servers, first token)
- Purple (#7B2D8E, #21134d) for secondary/infrastructure
- Orange (#EC7A08) for optional/alternate paths
- Teal-dark (#147878) for badges
- ALL text:
fontColor=#FFFFFF - ALL edge labels:
fontColor=#FFFFFF;labelBackgroundColor=#1b0d33 - ALL arrows:
strokeColor=#63bdbd;strokeWidth=2orstrokeColor=#FFFFFF;strokeWidth=2 - Font sizes: 14-18px for node labels, 10-12px for edge labels, 18-24px for section titles
- Use nested containers for grouping (e.g., "Kubernetes Cluster", "Prefill Phase", "Query Pipeline")
- Use badges for annotations (e.g., "Compute-Bound", "Batch Ingestion", "Always-On")
- Page size: 1600x900 for proper aspect ratio
Export and Embed
# Export at 3x scale for crisp display
/Applications/draw.io.app/Contents/MacOS/draw.io --export --format png --scale 3 --output diagram.png diagram.drawio
Then embed using build_diagram_image_slide() in helpers.py.
Red Hat Product Icon Overlay
After embedding the diagram PNG, overlay official Red Hat product icons from the Icon Repository (presentation ID: 1SRhy8-bYBgaA3Jsi1t_Fxz-Yo9ORgdRy5Kec9hg_wSM) on relevant components:
- OpenShift icon on K8s boxes
- AI Model icon on ML model boxes
- Private Cloud icon on storage boxes
- AI Inference icon on inference/LLM boxes
Use the add_icon_overlay() helper from helpers.py to place icons as 32pt × 32pt separate createImage elements on the slide.
Reference Diagrams
See these .drawio files in diagram-templates/ as templates for the approved quality level:
inference-pipeline.drawio— two-phase inference with nested containers, badges, KV cache visualizationtest-rag-industry-colored.drawio— RAG pipeline with industry shapes and two-lane layouttest-mixed-kserve.drawio— KServe architecture with nested K8s boundary
Typography
Always use the official Red Hat font families (available in Google Slides via Google Fonts). Do not use Arial — Red Hat fonts are always available in Google Slides.
- Red Hat Display — slide titles, headlines, large impact text (bold, expressive)
- Red Hat Text — body copy, descriptions, bullet points (readable at small sizes)
- Red Hat Mono — code snippets, technical labels, tags
| Element | Font Family | Font Size | Weight | Color |
|---|---|---|---|---|
| Slide title |
…(truncated)