Presentation Production for WFM

From WFM Labs

Workforce management functions communicate primarily through documents and slide decks: maturity assessments, capacity business cases, quarterly reviews, vendor readouts and staffing requests. Most organisations supply a corporate presentation template, and most practitioners produce decks that either ignore it or fight it.

Presentation production is the mechanical half of that work — how a branded slide file is actually assembled — as distinct from what the slides should argue (Executive Communication for WFM) or which chart encodes a given dataset (Data Visualization for WFM). This article covers the production layer, including how to use a large language model against a corporate template without incurring prohibitive cost.

The template problem

Corporate brand kits are typically distributed as several presentation files running to hundreds of slides in total: an icon library, an infographic or chart catalogue, and a set of example layouts demonstrating approved visual patterns.

When a language model is used to draft a deck, the instinct is to supply these files as context so that the model can reproduce the style. This fails for three reasons.

  • Cost. A few hundred slides represents roughly 50,000–150,000 tokens when converted to text, and substantially more when supplied as images, which are billed at a higher rate.
  • Non-reusability. The cost is incurred again in every session.
  • Ineffectiveness. A design system cannot be reconstructed from a transcript of its output. Slide contents describe what was produced, not the structural rules that produced it.

The structural information a model requires is comparatively small: the names of the available layouts, the placeholders each layout exposes, the theme colour and font definitions, and an index of reusable assets. Extracted deterministically, this is a few kilobytes. In one documented case a 71-slide deck was reduced to an 11.4 KB index retaining every layout name, its placeholder map, the theme palette and a slide-level asset inventory.

Template files and asset libraries

A distinction that is frequently missed, and which accounts for most production failures.

Type Contains How it is used
Template file Slide masters with named layouts and placeholders Opened as the base file; slides are added to its layouts by name
Asset library Example slides, icons, chart styles; no usable layouts Individual shapes or slides are copied out

In the Office Open XML format, a template's branding lives in slideMaster and slideLayout parts. Content placed on those parts is inherited by every slide using them. An asset library has no such structure — its visual material sits on ordinary slides, and the only way to use it is to copy shapes across.

Most corporate brand kits are asset libraries and are mistaken for templates. A file of well-designed example slides carries no structural branding to inherit, so applying its "theme" achieves nothing.

Brand kit extraction

The extraction step reads one or more .pptx or .potx files and emits a compact markdown index. It is deterministic, requires no language model, and is performed once per organisation.

The index records theme colours and fonts (read from theme1.xml), every slide master with its layouts and their placeholder maps, and a slide inventory including alt-text asset names — which is where meaningful icon names are stored, as opposed to auto-generated shape names such as Rectangle 47.

pip install python-pptx
python3 extract-brandkit.py template.pptx icons.pptx creative.pptx > BRAND-KIT.md
#!/usr/bin/env python3
"""
extract-brandkit.py — build a compact, reusable brand kit from any corporate
PowerPoint template or asset library.

Reads one or more .pptx/.potx files and emits a markdown BRAND-KIT that a model
can use to generate on-brand decks, without ever ingesting the source slides.

Usage:
    python3 extract-brandkit.py template.pptx icons.pptx > BRAND-KIT.md
    python3 extract-brandkit.py corp.pptx --masters corporate > BRAND-KIT.md
    python3 extract-brandkit.py assets.pptx --inventory > BRAND-KIT.md

Flags:
    --masters a,b     keep only these masters (index or name substring, comma separated)
    --inventory       include the per-slide inventory even for template files
    --max-slides N    cap the inventory (default 400)

By default the slide inventory is emitted only for files with NO usable layouts —
asset libraries, which you copy from. Template files are built into, so their slide
lists are omitted. Multi-brand corporate kits carry one master per sub-brand; use
--masters to keep the one you need.

Requires: python-pptx    (pip install python-pptx)
Output is typically 2-6 KB per source file. That is the whole point.
"""
import sys, os, re, zipfile
from xml.etree import ElementTree as ET

try:
    from pptx import Presentation
    from pptx.util import Emu
except ImportError:
    sys.exit("pip install python-pptx")

A = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
MAX_SLIDES = 400
WANT_INVENTORY = '--inventory' in sys.argv
if WANT_INVENTORY:
    sys.argv.remove('--inventory')
MASTERS = None
if '--masters' in sys.argv:
    i = sys.argv.index('--masters')
    MASTERS = [m.strip().lower() for m in sys.argv[i+1].split(',')]
    del sys.argv[i:i+2]
if '--max-slides' in sys.argv:
    i = sys.argv.index('--max-slides')
    MAX_SLIDES = int(sys.argv[i+1]); del sys.argv[i:i+2]


def want_master(idx, name):
    """--masters accepts indices (0,2) or name substrings (corporate,gbt)."""
    if MASTERS is None:
        return True
    nm = (name or '').lower()
    return any(m == str(idx) or (m and m in nm) for m in MASTERS)

FILES = [f for f in sys.argv[1:] if f.lower().endswith(('.pptx', '.potx'))]
if not FILES:
    sys.exit("give me one or more .pptx / .potx files")


def theme_of(path):
    """Pull the colour scheme and font scheme straight out of theme1.xml."""
    out = {'colors': {}, 'fonts': {}}
    try:
        with zipfile.ZipFile(path) as z:
            names = [n for n in z.namelist() if re.match(r'ppt/theme/theme\d+\.xml$', n)]
            if not names:
                return out
            root = ET.fromstring(z.read(sorted(names)[0]))
        cs = root.find(f'.//{A}clrScheme')
        if cs is not None:
            for child in cs:
                tag = child.tag.split('}')[1]
                srgb = child.find(f'{A}srgbClr')
                sysc = child.find(f'{A}sysClr')
                if srgb is not None:
                    out['colors'][tag] = '#' + srgb.get('val', '').upper()
                elif sysc is not None:
                    out['colors'][tag] = '#' + (sysc.get('lastClr') or '').upper()
        fs = root.find(f'.//{A}fontScheme')
        if fs is not None:
            for which in ('majorFont', 'minorFont'):
                el = fs.find(f'{A}{which}')
                if el is not None:
                    lt = el.find(f'{A}latin')
                    if lt is not None:
                        out['fonts'][which] = lt.get('typeface', '')
    except Exception as e:
        out['error'] = str(e)
    return out


def kind(shape):
    try:
        return str(shape.shape_type).split('(')[0].strip().replace('MSO_SHAPE_TYPE.', '')
    except Exception:
        return '?'


def first_text(shape_iter, limit=70):
    for sh in shape_iter:
        try:
            if sh.has_text_frame:
                t = sh.text_frame.text.strip()
                if t:
                    return ' '.join(t.split())[:limit]
        except Exception:
            continue
    return ''


# Auto-generated shape names are noise. Real asset names survive this filter.
_G = (r'rectangle|oval|ellipse|text\s*box|textbox|text\s*placeholder|content\s*placeholder'
      r'|picture\s*placeholder|placeholder|picture|image|group|content|title|subtitle'
      r'|freeform|straight\s*connector|connector|line|arrow[:\s]*\w*|shape|graphic|chart|table'
      r'|slide\s*number|footer|date|object|diagram|smartart|autoshape|right\s*brace'
      r'|isosceles\s*triangle|rounded\s*rectangle|flowchart|block\s*arc|frame|star')
GENERIC = re.compile(rf'^(?:{_G})(?:[\s:_-]*\d+)?$', re.I)


def alt_and_names(slide, cap=14):
    """Shape names and alt text — this is where icon/asset names actually live."""
    found = []
    for sh in slide.shapes:
        nm = (sh.name or '').strip()
        alt = ''
        try:
            alt = (sh._element._nvXxPr.cNvPr.get('descr') or '').strip()
        except Exception:
            pass
        label = alt or nm
        if label and not GENERIC.match(label):
            found.append(label[:34])
        if len(found) >= cap:
            break
    return found


print("# Brand kit\n")
print("Generated by `extract-brandkit.py`. Compact by design — this replaces the source")
print("decks as context. Do not paste the source decks themselves.\n")

for path in FILES:
    base = os.path.basename(path)
    try:
        prs = Presentation(path)
    except Exception as e:
        print(f"\n---\n\n## {base}\n\n**Could not open:** {e}\n")
        continue

    W = prs.slide_width or 0
    H = prs.slide_height or 0
    ratio = f"{round(W/H, 3)}" if H else "?"
    aspect = "16:9" if abs(W/H - 16/9) < .02 else ("4:3" if abs(W/H - 4/3) < .02 else ratio)

    print(f"\n---\n\n## {base}")
    print(f"\n`{len(prs.slides)} slides · {len(prs.slide_masters)} master(s) · "
          f"{Emu(W).inches:.2f}×{Emu(H).inches:.2f}in · {aspect}`\n")

    th = theme_of(path)
    if th['colors']:
        print("**Theme colours**\n")
        order = ['dk1','lt1','dk2','lt2','accent1','accent2','accent3',
                 'accent4','accent5','accent6','hlink','folHlink']
        cells = [f"`{k}` {th['colors'][k]}" for k in order if k in th['colors']]
        print(' · '.join(cells) + "\n")
    if th['fonts']:
        print(f"**Fonts** — headings `{th['fonts'].get('majorFont','?')}` · "
              f"body `{th['fonts'].get('minorFont','?')}`\n")

    # ---- masters and layouts: the structural payload ----
    has_layouts = False
    n_masters = len(prs.slide_masters)
    shown = 0
    for mi, m in enumerate(prs.slide_masters):
        if not len(m.slide_layouts):
            continue
        has_layouts = True
        if not want_master(mi, m.name):
            continue
        shown += 1
        print(f"\n### Master {mi}{len(m.slide_layouts)} layouts\n")
        print("| # | Layout name | Placeholders (idx:type) | Static art |")
        print("|---|---|---|---|")
        for li, l in enumerate(m.slide_layouts):
            phs = []
            for ph in l.placeholders:
                try:
                    t = str(ph.placeholder_format.type).split('(')[0].strip()
                    t = t.replace('PP_PLACEHOLDER.', '')
                    phs.append(f"{ph.placeholder_format.idx}:{t}")
                except Exception:
                    pass
            art = sum(1 for s in l.shapes if not s.is_placeholder)
            print(f"| {li} | `{l.name}` | {', '.join(phs) if phs else '—'} | {art} |")

    if has_layouts and MASTERS is not None and shown < n_masters:
        names = ', '.join(f'`{i}:{m.name}`' for i, m in enumerate(prs.slide_masters))
        print(f"\n*{n_masters - shown} of {n_masters} masters omitted by `--masters`. "
              f"All masters: {names}*\n")

    if not has_layouts:
        print("\n**No usable layouts** — this is an asset library, not a template.\n")

    # ---- slide inventory: the asset payload ----
    # A template is built INTO; its slide inventory is bulk with little value.
    # An asset library is copied FROM; there the inventory is the whole point.
    if has_layouts and not WANT_INVENTORY:
        print(f"\n*Slide inventory omitted — this file has usable layouts, so build into them "
              f"rather than copying from its {len(prs.slides)} slides. Pass `--inventory` to "
              f"include it.*\n")
        continue

    n = min(len(prs.slides), MAX_SLIDES)
    if n:
        print(f"\n### Slide inventory — {n} slides\n")
        print("| # | Layout | Heading | Contents | Named assets |")
        print("|---|---|---|---|---|")
        for si, s in enumerate(list(prs.slides)[:n], 1):
            counts = {}
            for sh in s.shapes:
                k = kind(sh)
                counts[k] = counts.get(k, 0) + 1
            for attr, tag in (('has_chart', 'CHART'), ('has_table', 'TABLE')):
                if any(getattr(sh, attr, False) for sh in s.shapes):
                    counts[tag] = counts.get(tag, 0) + 1
            summary = ' '.join(f"{k}×{v}" for k, v in sorted(counts.items(),
                               key=lambda x: -x[1])[:5])
            names = alt_and_names(s)
            lay = ''
            try:
                lay = s.slide_layout.name
            except Exception:
                pass
            print(f"| {si} | `{lay[:22]}` | {first_text(s.shapes)} | {summary} "
                  f"| {', '.join(names[:8])} |")
        if len(prs.slides) > n:
            print(f"\n*(truncated at {n} of {len(prs.slides)} — raise `--max-slides`)*")

print("\n---\n")
print("## How to use this\n")
print("- **Template files** (those with layouts) supply the master. Open one as the base "
      "and add slides to its layouts **by name**. Never create a new master.")
print("- **Asset libraries** (those without layouts) supply things to copy. Reference them "
      "as `file · slide N` and copy the shape across.")
print("- Match content to form using `FORM.md`, then form to layout using the tables above.")

The resulting file replaces the source decks as context permanently.

Running the extraction

Most people do not have Python on the machine that holds the corporate template. Both paths work, and the choice changes nothing about the output.

With a local terminal

pip install python-pptx
python3 extract-brandkit.py template.pptx assets.pptx --masters corporate > BRAND-KIT.md
wc -c BRAND-KIT.md

Without one — the common case

Run the extraction in a disposable conversation, not in the project that will hold the kit.

  1. Open a new conversation. Not the project.
  2. Upload the template files.
  3. Paste the extractor and ask for it to be run against them, saving the output as a file for download. Ask for only the master and layout tables to be shown, not the whole document.
  4. Download the resulting BRAND-KIT.md.
  5. Upload that one file to the project as knowledge, alongside the reference blocks.
  6. Close the disposable conversation and do not reopen it.

Why disposable. Anything placed in project knowledge persists and is retrieved on every relevant message thereafter. Three large presentation files sitting there is a permanent cost for no benefit, because their useful content is now in the kit.

Why this is cheap. When a presentation file is uploaded to a code-execution environment it is read by the library in a sandbox — the model never reads the slides themselves. Only the printed index enters the conversation. A file of several hundred slides therefore costs roughly the size of its index, not the size of the file.

Then, per deck

  1. Open a conversation inside the project.
  2. Upload the deck to be built or converted, plus the template file identified in the kit as carrying the master.
  3. Ask for the build plan first — one row per slide, and nothing generated yet.
  4. Approve or adjust the plan.
  5. Ask for generation, then run the verification snippet.

Sizing, and what to do when it comes back large

Expect 2–6 KB per file with default settings. If the output is very much larger, the cause is almost always one of the following.

Symptom Cause Fix
Tens of KB per file, many masters listed A multi-brand kit — one master per sub-brand, practice area or product line --masters with the one you need. Index or name substring
Very large slide inventory The file is a template and its per-slide list is being emitted Omitted by default from version 1.1 onward. Do not pass --inventory for template files
Forty or more layouts per master Normal for a mature corporate template Nothing to fix. A given deck will use eight to twelve of them

A multi-brand kit needs one master for a given audience. Generate a second kit later if a different brand is required; it is cheap once the flags are set.


Slide composition

Chart selection is covered in Data Visualization for WFM; the framing of findings for an executive audience is covered in Executive Communication for WFM. Slide composition is a narrower question: given a finding, what object should occupy the slide.

The claim is… Form Common error
A single figure that matters The figure, set large, with one line of context Rendering it as a chart
A ranking Horizontal bars, sorted, labels left-aligned Vertical bars with rotated labels
Spread across units Dot plot or sorted bars with the range annotated Reporting the mean, which conceals the finding
A sequence or process Numbered steps, bounded at five Cycle diagrams for processes that do not cycle
A structure or hierarchy Nested boxes or a tree An indented bullet list
Two-axis positioning A 2×2, where both axes are measured A 2×2 constructed to justify a predetermined conclusion
Several qualitatively different items A table Bullets, which flatten distinctions a table preserves
A trade-off Two opposed columns with the choice named Prose
An argument Prose, well set A diagram of the argument

Two working tests. A chart requiring a paragraph of explanation is a table with decoration. A slide requiring two sentences to make sense is two slides.

Slide titles should state the finding rather than the topic. A deck whose titles, read in sequence and without their slides, form a coherent argument is structurally sound; one whose titles are nouns is a document with headings.

Producing the file

Generation is normally performed with python-pptx or an equivalent OOXML library.

Mechanical rules

  • Open the template as the base file. Instantiating a presentation object with no argument creates a new minimal slide master. The branded master, if subsequently imported, becomes an unused passenger and no slide inherits from it.
  • Select layouts by name, not by index. Index ordering is not stable across template revisions.
  • Never create a slide master or layout.
  • Place text in placeholders, addressed by placeholder index. Free-drawn text boxes inherit no branding, cannot be reflowed by a layout change, and defeat later correction.
  • Do not set fonts or colours literally. Inherit from the placeholder or reference theme colours; hard-coded values defeat the purpose of the template.

Verification

from pptx import Presentation
p = Presentation('output.pptx')
print(f'masters={len(p.slide_masters)}  slides={len(p.slides)}')
for i, s in enumerate(p.slides, 1):
    ph = sum(1 for sh in s.shapes if sh.is_placeholder)
    free = len(s.shapes) - ph
    flag = '  <-- CHECK' if ph == 0 else ''
    print(f'{i:3d} | {s.slide_layout.name:30s} | ph={ph} free={free}{flag}')

A correct file reports the template's original master count, every slide on a named branded layout, and at least one placeholder per slide. A file reporting zero placeholders throughout cannot be branded by any subsequent operation, because its content consists entirely of free shapes; it must be regenerated.

Recovery from unbranded output

Where a deck has already been produced without branding, the following sequence is performed entirely within PowerPoint and requires no regeneration.

  1. Design → Themes → Browse for Themes, selecting the template file. This imports the master and is the appropriate first step where the file contains no branded master at all.
  2. Select all slides, then Home → Layout to apply a branded layout, followed by Home → Reset.
  3. View → Slide Master and delete the empty generated master; slides using it are reassigned to the remaining master.
  4. As a last resort, open the template and paste the slides into it using Use Destination Theme.

Where the layout gallery offers only a single blank option, the slides belong to a generated master and the branded master is either absent or unused; begin at step 1.

Content drawn as free text boxes will sit over the imported branding rather than reflowing within it. Backgrounds and logos render correctly, and individual objects require manual adjustment.

When a slide deck is the wrong artifact

For internal working documents, pre-reads and material that will not be edited by others, a self-contained HTML file is generally preferable: it renders identically across machines, requires no template, cannot inherit incorrectly, and prints to PDF reliably.

Presentation files are appropriate where the material must be incorporated into another organisation's deck, edited by other contributors, or presented from managed corporate software.

Maturity Model Position

Presentation production sits within the communication capability of the WFM Labs Maturity Model™.

  • Initial — decks are assembled individually; branding is applied manually or not at all; each author works from a different starting file.
  • Developing — a corporate template is available and used inconsistently; formatting effort is duplicated across authors.
  • Defined — the template is applied consistently, and slide composition standards exist for recurring artifacts such as business cases and reviews.
  • Managed — production is partly automated; branded output is generated from analysis directly, and correctness is verified rather than inspected.
  • Optimising — templates and composition standards are maintained as assets, and the cost of producing a branded artifact is no longer a constraint on how often analysis is communicated.

Use this with Claude

A ready-to-deploy instruction set and reference files are at Wiki:Packs/Presentation Production (CP-CNT-001).

See Also

References

  • ECMA International. ECMA-376 Office Open XML File Formats. 5th ed., 2016.
  • Knaflic, Cole Nussbaumer. Storytelling with Data. Wiley, 2015.
  • Duarte, Nancy. DataStory: Explain Data and Inspire Action Through Story. Ideapress, 2019.
  • Tufte, Edward R. The Visual Display of Quantitative Information. 2nd ed., Graphics Press, 2001.
  • python-pptx documentation. https://python-pptx.readthedocs.io/