modcomms/documentation/generate_presentation.py
michael 2ffe3783d2 Add Mod Comms feature presentation and documentation
Create a comprehensive 25-slide PowerPoint presentation showcasing all
Mod Comms features, including multi-agent AI system, campaign management,
real-time analysis, feedback reports, knowledge base, analytics, auditing,
user roles, and technical architecture. Includes a Python generator script
for reproducible builds and a companion features markdown document.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 10:32:34 -06:00

1470 lines
57 KiB
Python

"""
Mod Comms — PowerPoint Presentation Generator
Generates a professional 16:9 widescreen presentation showcasing all features.
"""
import os
from pathlib import Path
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
SCRIPT_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = SCRIPT_DIR.parent
LOGO_PATH = PROJECT_ROOT / "UI_guidance" / "Barclays-Modcomms.png"
OUTPUT_PPTX = SCRIPT_DIR / "ModComms_Presentation.pptx"
# ---------------------------------------------------------------------------
# Design tokens
# ---------------------------------------------------------------------------
DARK_NAVY = RGBColor(0x1A, 0x21, 0x42)
ACTIVE_BLUE = RGBColor(0x00, 0x6D, 0xE3)
ELECTRIC_VIOLET = RGBColor(0x7A, 0x0F, 0xF9)
LIME = RGBColor(0xC3, 0xFB, 0x5A)
TEAL = RGBColor(0x01, 0xA1, 0xA2)
CYAN_BRAND = RGBColor(0x00, 0xAE, 0xEF)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLACK_TITLE = RGBColor(0x27, 0x27, 0x27)
GREY_700 = RGBColor(0x8E, 0x8E, 0x8E)
GREY_300 = RGBColor(0xE2, 0xE2, 0xE2)
GREY_100 = RGBColor(0xF6, 0xF6, 0xF6)
RAG_GREEN = RGBColor(0x09, 0x82, 0x1F)
RAG_AMBER = RGBColor(0xFF, 0xBA, 0x00)
RAG_RED = RGBColor(0xE3, 0x00, 0x0F)
FONT_NAME = "Arial"
# Slide dimensions (16:9)
SLIDE_WIDTH = Inches(13.333)
SLIDE_HEIGHT = Inches(7.5)
# Margins
MARGIN_LEFT = Inches(0.8)
MARGIN_RIGHT = Inches(0.8)
MARGIN_TOP = Inches(0.6)
CONTENT_WIDTH = SLIDE_WIDTH - MARGIN_LEFT - MARGIN_RIGHT
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def set_slide_bg(slide, color):
"""Set a solid background colour for a slide."""
bg = slide.background
fill = bg.fill
fill.solid()
fill.fore_color.rgb = color
def add_textbox(slide, left, top, width, height, text, font_size=18,
color=BLACK_TITLE, bold=False, alignment=PP_ALIGN.LEFT,
font_name=FONT_NAME, line_spacing=1.2):
"""Add a simple single-run text box and return the shape."""
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.color.rgb = color
p.font.bold = bold
p.font.name = font_name
p.alignment = alignment
p.space_after = Pt(0)
p.space_before = Pt(0)
if line_spacing != 1.0:
p.line_spacing = Pt(font_size * line_spacing)
return txBox
def add_bullet_list(slide, left, top, width, height, items, font_size=14,
color=BLACK_TITLE, bullet_color=ACTIVE_BLUE, line_spacing=1.5):
"""Add a bulleted list text box. Each item is a separate paragraph."""
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
for i, item in enumerate(items):
if i == 0:
p = tf.paragraphs[0]
else:
p = tf.add_paragraph()
p.text = item
p.font.size = Pt(font_size)
p.font.color.rgb = color
p.font.name = FONT_NAME
p.space_after = Pt(4)
p.space_before = Pt(2)
p.line_spacing = Pt(font_size * line_spacing)
# Bullet
pPr = p._pPr
if pPr is None:
from pptx.oxml.ns import qn
pPr = p._p.get_or_add_pPr()
from pptx.oxml.ns import qn
buNone = pPr.find(qn("a:buNone"))
if buNone is not None:
pPr.remove(buNone)
buChar = pPr.makeelement(qn("a:buChar"), {"char": "\u2022"})
pPr.append(buChar)
buClr = pPr.makeelement(qn("a:buClr"), {})
srgbClr = buClr.makeelement(qn("a:srgbClr"), {"val": f"{bullet_color}"})
buClr.append(srgbClr)
pPr.append(buClr)
buSzPct = pPr.makeelement(qn("a:buSzPct"), {"val": "120000"})
pPr.append(buSzPct)
# Indent
pPr.set("marL", str(Emu(Inches(0.3))))
pPr.set("indent", str(Emu(Inches(-0.25))))
return txBox
def add_rounded_rect(slide, left, top, width, height, fill_color, text="",
font_size=12, font_color=WHITE, bold=False, line_color=None):
"""Add a rounded rectangle shape with optional text."""
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height)
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color
if line_color:
shape.line.color.rgb = line_color
shape.line.width = Pt(1.5)
else:
shape.line.fill.background()
# Smaller corner radius
shape.adjustments[0] = 0.1
if text:
tf = shape.text_frame
tf.word_wrap = True
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.color.rgb = font_color
p.font.bold = bold
p.font.name = FONT_NAME
tf.margin_left = Pt(6)
tf.margin_right = Pt(6)
tf.margin_top = Pt(4)
tf.margin_bottom = Pt(4)
return shape
def add_circle(slide, left, top, diameter, fill_color, text="", font_size=11,
font_color=WHITE, bold=True):
"""Add a circle shape with centered text."""
shape = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, diameter, diameter)
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color
shape.line.fill.background()
if text:
tf = shape.text_frame
tf.word_wrap = True
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.color.rgb = font_color
p.font.bold = bold
p.font.name = FONT_NAME
tf.margin_left = Pt(2)
tf.margin_right = Pt(2)
tf.margin_top = Pt(2)
tf.margin_bottom = Pt(2)
return shape
def add_connector_line(slide, start_x, start_y, end_x, end_y, color=GREY_300, width=1.5):
"""Add a straight connector line."""
connector = slide.shapes.add_connector(
1, # MSO_CONNECTOR.STRAIGHT
start_x, start_y, end_x, end_y
)
connector.line.color.rgb = color
connector.line.width = Pt(width)
return connector
def add_rich_textbox(slide, left, top, width, height):
"""Add a text box and return the text frame for manual paragraph building."""
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
return tf
# ---------------------------------------------------------------------------
# Slide builders
# ---------------------------------------------------------------------------
def make_title_slide(prs):
"""Slide 1: Title slide with logo."""
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
set_slide_bg(slide, DARK_NAVY)
# Logo
if LOGO_PATH.exists():
slide.shapes.add_picture(
str(LOGO_PATH),
Inches(0.8), Inches(1.0),
height=Inches(1.8)
)
# Title
add_textbox(slide, Inches(0.8), Inches(3.2), Inches(10), Inches(1.0),
"AI-Powered Proof Review", font_size=40, color=WHITE, bold=True)
# Subtitle
tf = add_rich_textbox(slide, Inches(0.8), Inches(4.2), Inches(10), Inches(0.8))
p = tf.paragraphs[0]
run1 = p.add_run()
run1.text = "Automated compliance, brand, and channel analysis for "
run1.font.size = Pt(20)
run1.font.color.rgb = GREY_300
run1.font.name = FONT_NAME
run2 = p.add_run()
run2.text = "Barclays"
run2.font.size = Pt(20)
run2.font.color.rgb = LIME
run2.font.bold = True
run2.font.name = FONT_NAME
run3 = p.add_run()
run3.text = " marketing materials"
run3.font.size = Pt(20)
run3.font.color.rgb = GREY_300
run3.font.name = FONT_NAME
# Built by line
add_textbox(slide, Inches(0.8), Inches(5.4), Inches(6), Inches(0.5),
"Built by OLIVER Agency", font_size=14, color=GREY_700)
# Decorative accent bar
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.05),
Inches(2.0), Pt(4))
bar.fill.solid()
bar.fill.fore_color.rgb = LIME
bar.line.fill.background()
def make_section_divider(prs, section_number, title, subtitle=""):
"""Section divider slide — dark navy with large number."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, DARK_NAVY)
# Large section number
add_textbox(slide, Inches(0.8), Inches(1.0), Inches(3), Inches(2.5),
f"{section_number:02d}", font_size=96, color=LIME, bold=True)
# Accent bar
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.6),
Inches(2.0), Pt(4))
bar.fill.solid()
bar.fill.fore_color.rgb = LIME
bar.line.fill.background()
# Title
add_textbox(slide, Inches(0.8), Inches(3.9), Inches(10), Inches(1.2),
title, font_size=36, color=WHITE, bold=True)
if subtitle:
add_textbox(slide, Inches(0.8), Inches(5.1), Inches(10), Inches(0.8),
subtitle, font_size=18, color=GREY_300)
def make_content_slide(prs, title, bullets, subtitle=""):
"""Standard content slide with heading and bullet list."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
# Title
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
title, font_size=28, color=DARK_NAVY, bold=True)
# Accent underline
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
y_start = Inches(1.3)
if subtitle:
add_textbox(slide, MARGIN_LEFT, y_start, CONTENT_WIDTH, Inches(0.5),
subtitle, font_size=15, color=GREY_700)
y_start = Inches(1.8)
add_bullet_list(slide, MARGIN_LEFT, y_start, CONTENT_WIDTH, Inches(5.5),
bullets, font_size=15, color=BLACK_TITLE)
return slide
def make_two_column_slide(prs, title, left_title, left_items, right_title, right_items):
"""Two-column content slide."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
# Title
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
title, font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
col_width = Inches(5.5)
gap = Inches(0.8)
# Left column
add_textbox(slide, MARGIN_LEFT, Inches(1.4), col_width, Inches(0.4),
left_title, font_size=18, color=TEAL, bold=True)
add_bullet_list(slide, MARGIN_LEFT, Inches(1.85), col_width, Inches(5.0),
left_items, font_size=14)
# Right column
right_left = MARGIN_LEFT + col_width + gap
add_textbox(slide, right_left, Inches(1.4), col_width, Inches(0.4),
right_title, font_size=18, color=TEAL, bold=True)
add_bullet_list(slide, right_left, Inches(1.85), col_width, Inches(5.0),
right_items, font_size=14)
return slide
# ---------------------------------------------------------------------------
# Specific slides
# ---------------------------------------------------------------------------
def make_agenda_slide(prs):
"""Slide 2: Agenda."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"Agenda", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
agenda_items = [
("01", "The Challenge", "Why manual proof review doesn't scale"),
("02", "Introducing Mod Comms", "AI-powered proof review at a glance"),
("03", "Multi-Agent AI System", "Four specialist agents + Lead Agent"),
("04", "Campaign Management", "Organising and tracking marketing proofs"),
("05", "Real-Time Analysis", "Live WebSocket-powered review"),
("06", "Feedback & Reporting", "Structured results and PDF export"),
("07", "Knowledge Base & Admin", "Managing guidelines, analytics, and access"),
("08", "Technical Architecture", "How it all fits together"),
]
y = Inches(1.4)
for num, title, desc in agenda_items:
# Number
add_textbox(slide, MARGIN_LEFT, y, Inches(0.6), Inches(0.45),
num, font_size=20, color=ACTIVE_BLUE, bold=True)
# Title
add_textbox(slide, Inches(1.5), y, Inches(4), Inches(0.3),
title, font_size=16, color=DARK_NAVY, bold=True)
# Description
add_textbox(slide, Inches(5.6), y + Inches(0.02), Inches(6), Inches(0.3),
desc, font_size=14, color=GREY_700)
# Divider line
if num != "08":
line = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
MARGIN_LEFT, y + Inches(0.48),
CONTENT_WIDTH, Pt(0.75))
line.fill.solid()
line.fill.fore_color.rgb = GREY_300
line.line.fill.background()
y += Inches(0.58)
def make_problem_slide(prs):
"""Slide 4: Problem statement."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"The Manual Review Bottleneck", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Problem cards — 2x2 grid
cards = [
("Slow Turnaround", "Manual review of each proof takes hours.\nCampaign launches are delayed\nwaiting for feedback."),
("Inconsistent Quality", "Different reviewers apply guidelines\ndifferently. Critical compliance\nissues are sometimes missed."),
("Scaling Challenges", "Hundreds of proofs across Social,\nDisplay, Email, and Print channels\noverwhelm review teams."),
("Knowledge Silos", "Brand guidelines, legal requirements,\nand channel specs live in separate\ndocuments — hard to cross-reference."),
]
card_w = Inches(5.5)
card_h = Inches(2.3)
x_positions = [MARGIN_LEFT, MARGIN_LEFT + card_w + Inches(0.6)]
y_positions = [Inches(1.5), Inches(4.1)]
for idx, (card_title, card_desc) in enumerate(cards):
x = x_positions[idx % 2]
y = y_positions[idx // 2]
# Card background
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, card_w, card_h)
card.fill.solid()
card.fill.fore_color.rgb = GREY_100
card.line.color.rgb = GREY_300
card.line.width = Pt(1)
card.adjustments[0] = 0.05
# Red accent bar at top of card
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x + Inches(0.2),
y + Inches(0.2), Inches(0.3), Pt(4))
accent.fill.solid()
accent.fill.fore_color.rgb = RAG_RED
accent.line.fill.background()
# Title
add_textbox(slide, x + Inches(0.7), y + Inches(0.1), card_w - Inches(1), Inches(0.4),
card_title, font_size=16, color=DARK_NAVY, bold=True)
# Description
add_textbox(slide, x + Inches(0.3), y + Inches(0.6), card_w - Inches(0.6), Inches(1.5),
card_desc, font_size=13, color=GREY_700, line_spacing=1.4)
def make_solution_slide(prs):
"""Slide 6: Solution overview with value proposition cards."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"How Mod Comms Solves This", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
cards = [
(ACTIVE_BLUE, "Instant Analysis",
"Four AI agents review every\nproof in parallel — results in\nseconds, not days."),
(ELECTRIC_VIOLET, "Consistent Standards",
"Every proof is checked against\nthe same guidelines. No more\nhuman inconsistency."),
(TEAL, "Full Coverage",
"Legal, Brand, Channel Best\nPractices, and Tech Specs —\nall checked simultaneously."),
(RAG_GREEN, "Actionable Feedback",
"Clear RAG status with specific,\nconstructive recommendations\nfor every issue found."),
]
card_w = Inches(2.7)
card_h = Inches(4.0)
total_w = card_w * 4 + Inches(0.4) * 3
start_x = (SLIDE_WIDTH - total_w) / 2
for idx, (color, title, desc) in enumerate(cards):
x = start_x + idx * (card_w + Inches(0.4))
y = Inches(1.8)
# Card
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, y, card_w, card_h)
card.fill.solid()
card.fill.fore_color.rgb = DARK_NAVY
card.line.fill.background()
card.adjustments[0] = 0.06
# Top accent bar
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
x + Inches(0.3), y + Inches(0.3),
Inches(0.8), Pt(4))
accent.fill.solid()
accent.fill.fore_color.rgb = color
accent.line.fill.background()
# Title
add_textbox(slide, x + Inches(0.3), y + Inches(0.6), card_w - Inches(0.6), Inches(0.5),
title, font_size=16, color=LIME, bold=True)
# Description
add_textbox(slide, x + Inches(0.3), y + Inches(1.2), card_w - Inches(0.6), Inches(2.5),
desc, font_size=13, color=GREY_300, line_spacing=1.5)
def make_agent_architecture_slide(prs):
"""Slide 8: Agent architecture diagram."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.3), CONTENT_WIDTH, Inches(0.6),
"Multi-Agent Architecture", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(0.88),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# --- Proof Upload box ---
upload_x = Inches(0.5)
upload_y = Inches(2.8)
upload_w = Inches(1.8)
upload_h = Inches(1.2)
add_rounded_rect(slide, upload_x, upload_y, upload_w, upload_h,
GREY_100, "Proof\nUpload", font_size=14, font_color=DARK_NAVY,
bold=True, line_color=GREY_300)
# --- Arrow from upload to agents area ---
add_connector_line(slide, upload_x + upload_w, upload_y + upload_h / 2,
Inches(2.8), upload_y + upload_h / 2, ACTIVE_BLUE, 2)
# --- Four specialist agents ---
agent_x = Inches(2.8)
agents = [
(ACTIVE_BLUE, "Legal\nAgent"),
(ELECTRIC_VIOLET, "Brand\nAgent"),
(TEAL, "Channel Best\nPractices Agent"),
(CYAN_BRAND, "Channel Tech\nSpecs Agent"),
]
agent_w = Inches(2.0)
agent_h = Inches(0.9)
agent_gap = Inches(0.2)
agents_total_h = len(agents) * agent_h + (len(agents) - 1) * agent_gap
agent_start_y = Inches(1.2)
for idx, (color, name) in enumerate(agents):
y = agent_start_y + idx * (agent_h + agent_gap)
add_rounded_rect(slide, agent_x, y, agent_w, agent_h,
color, name, font_size=12, font_color=WHITE, bold=True)
# Arrow to lead agent
add_connector_line(slide, agent_x + agent_w, y + agent_h / 2,
Inches(5.6), y + agent_h / 2, GREY_300, 1.5)
# --- Gemini API box (below agents) ---
gemini_x = Inches(2.8)
gemini_y = agent_start_y + agents_total_h + Inches(0.4)
add_rounded_rect(slide, gemini_x, gemini_y, agent_w, Inches(0.7),
DARK_NAVY, "Google Gemini 2.5 Flash",
font_size=11, font_color=LIME, bold=True)
# Connector from Gemini to agents (vertical line on the left side)
gem_center_x = gemini_x + agent_w / 2
add_connector_line(slide, gem_center_x, agent_start_y + agents_total_h,
gem_center_x, gemini_y, GREY_300, 1.5)
# --- Lead Agent (central) ---
lead_x = Inches(5.6)
lead_y = Inches(2.4)
lead_w = Inches(2.2)
lead_h = Inches(1.8)
lead = add_rounded_rect(slide, lead_x, lead_y, lead_w, lead_h,
DARK_NAVY, "", line_color=LIME)
# Lead agent text
add_textbox(slide, lead_x + Inches(0.15), lead_y + Inches(0.2),
lead_w - Inches(0.3), Inches(0.4),
"Lead Agent", font_size=16, color=LIME, bold=True,
alignment=PP_ALIGN.CENTER)
add_textbox(slide, lead_x + Inches(0.15), lead_y + Inches(0.65),
lead_w - Inches(0.3), Inches(0.9),
"Synthesises all\nreviews into final\nstatus & summary",
font_size=11, color=WHITE, alignment=PP_ALIGN.CENTER)
# --- Arrow from lead to output ---
add_connector_line(slide, lead_x + lead_w, lead_y + lead_h / 2,
Inches(8.5), lead_y + lead_h / 2, ACTIVE_BLUE, 2)
# --- Knowledge Base box (below lead) ---
kb_x = Inches(5.6)
kb_y = gemini_y
add_rounded_rect(slide, kb_x, kb_y, lead_w, Inches(0.7),
DARK_NAVY, "Knowledge Base",
font_size=11, font_color=LIME, bold=True)
add_connector_line(slide, kb_x + lead_w / 2, lead_y + lead_h,
kb_x + lead_w / 2, kb_y, GREY_300, 1.5)
# --- Output: RAG Status ---
output_x = Inches(8.5)
output_y = Inches(1.5)
output_w = Inches(4.2)
output_h = Inches(4.8)
# Output container
out_card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
output_x, output_y, output_w, output_h)
out_card.fill.solid()
out_card.fill.fore_color.rgb = GREY_100
out_card.line.color.rgb = GREY_300
out_card.line.width = Pt(1)
out_card.adjustments[0] = 0.04
add_textbox(slide, output_x + Inches(0.2), output_y + Inches(0.15),
output_w - Inches(0.4), Inches(0.35),
"Analysis Output", font_size=14, color=DARK_NAVY, bold=True,
alignment=PP_ALIGN.CENTER)
# RAG circles
rag_items = [
(RAG_GREEN, "Green", "Passed — compliant"),
(RAG_AMBER, "Amber", "Minor issues to address"),
(RAG_RED, "Red", "Failed — must resolve"),
]
circle_d = Inches(0.4)
rag_y = output_y + Inches(0.7)
for idx, (color, label, desc) in enumerate(rag_items):
cy = rag_y + idx * Inches(0.6)
add_circle(slide, output_x + Inches(0.3), cy, circle_d, color,
"", font_size=9, bold=True)
add_textbox(slide, output_x + Inches(0.85), cy + Inches(0.02),
Inches(1), Inches(0.35),
label, font_size=12, color=DARK_NAVY, bold=True)
add_textbox(slide, output_x + Inches(1.8), cy + Inches(0.02),
Inches(2.2), Inches(0.35),
desc, font_size=11, color=GREY_700)
# Overall status labels
status_y = rag_y + Inches(2.0)
statuses = [
(RAG_GREEN, "Passed"),
(RAG_AMBER, "Requires Manual Legal Review"),
(RAG_RED, "Failed"),
(GREY_700, "Analysis Error"),
]
add_textbox(slide, output_x + Inches(0.2), status_y - Inches(0.3),
output_w - Inches(0.4), Inches(0.3),
"Overall Status:", font_size=12, color=DARK_NAVY, bold=True)
for idx, (color, label) in enumerate(statuses):
sy = status_y + idx * Inches(0.35)
dot = slide.shapes.add_shape(MSO_SHAPE.OVAL,
output_x + Inches(0.3), sy + Inches(0.05),
Inches(0.15), Inches(0.15))
dot.fill.solid()
dot.fill.fore_color.rgb = color
dot.line.fill.background()
add_textbox(slide, output_x + Inches(0.6), sy,
Inches(3.2), Inches(0.3),
label, font_size=11, color=DARK_NAVY)
def make_legal_agent_slide(prs):
"""Slide 9: Legal Agent deep dive."""
make_content_slide(prs, "Legal Agent",
[
"Detects financial promotions — interest rates, APR, credit products, savings rates",
"Checks advertising standards compliance against ASA/CAP code",
"Verifies required disclaimers are present, legible, and properly placed",
"Assesses FCA regulatory compliance for financial services marketing",
"Reviews terms and conditions — referenced where necessary, qualifying text clear",
"Checks third-party content — permissions, attributions, influencer disclosures",
"Financial promotion detected → overall status becomes 'Requires Manual Legal Review'",
"Uses British English and constructive language throughout all feedback",
],
subtitle="Compliance specialist ensuring all marketing materials meet legal and regulatory requirements")
def make_brand_agent_slide(prs):
"""Slide 10: Brand Agent deep dive."""
make_two_column_slide(prs,
"Brand Agent",
"Barclays Brand Checks",
[
"Logo usage — correct version, minimum size, clear space, placement",
"Colour palette — approved masterbrand colours, WCAG-compliant pairings",
"Typography — Barclays Effra (Arial fallback), correct weights and scale",
"Design principles — overall design reflects brand expression",
"Sacred assets — present and unaltered",
"Accessibility — legible font sizes, proper contrast ratios",
],
"Barclaycard Specifics",
[
"Card Portal — stroke weight, corner radius, border colour, rotation limits",
"Barclaycard-specific core principles and guidelines",
"Experiential and email-specific guidelines applied",
"Social media guidelines for Barclaycard-branded content",
"Brand selection is per-campaign — agents load the correct spec dynamically",
"15+ brand guideline documents in the Knowledge Base",
])
def make_channel_agents_slide(prs):
"""Slide 11: Channel agents deep dive."""
make_two_column_slide(prs,
"Channel Agents",
"Best Practices Agent",
[
"Content strategy — messaging clarity, CTA effectiveness",
"Creative best practices — visual hierarchy, engagement patterns",
"Platform optimisation — algorithm tips, safe zones, text-to-image ratios",
"Engagement — hashtags, mentions, tone suitability",
"Mobile-first design — legibility, touch targets, thumb-zone navigation",
],
"Tech Specs Agent",
[
"Dimensions & resolution — platform-specific sizes, DPI/PPI, aspect ratios",
"File format — type, size limits, compression quality",
"Typography specs — minimum font sizes, character counts",
"Digital grid system — 12-col desktop, 6-col mobile, 8px baseline",
"WCAG accessibility — colour contrast, documented pairings",
"Platform-specific specs — safe zones, video formats, frame rates",
])
def make_rag_status_slide(prs):
"""Slide 12: RAG Status & Decision Logic."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"RAG Status & Decision Logic", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Agent RAG section
add_textbox(slide, MARGIN_LEFT, Inches(1.3), Inches(5), Inches(0.4),
"Per-Agent RAG Status", font_size=18, color=TEAL, bold=True)
rag_items = [
(RAG_GREEN, "Green", "Fully compliant — no issues found"),
(RAG_AMBER, "Amber", "Minor issues that should be addressed"),
(RAG_RED, "Red", "Significant issues that must be resolved"),
(GREY_700, "Error", "Agent could not analyse with confidence"),
]
for idx, (color, label, desc) in enumerate(rag_items):
y = Inches(1.85) + idx * Inches(0.55)
add_circle(slide, MARGIN_LEFT + Inches(0.2), y, Inches(0.35), color)
add_textbox(slide, MARGIN_LEFT + Inches(0.75), y + Inches(0.02),
Inches(1), Inches(0.3), label, font_size=14, color=DARK_NAVY, bold=True)
add_textbox(slide, MARGIN_LEFT + Inches(1.8), y + Inches(0.02),
Inches(4), Inches(0.3), desc, font_size=13, color=GREY_700)
# Decision logic section
add_textbox(slide, Inches(7.0), Inches(1.3), Inches(5.5), Inches(0.4),
"Lead Agent Decision Logic", font_size=18, color=TEAL, bold=True)
# Decision flow as cards
decisions = [
("1", RAG_AMBER, "Financial promotion detected?",
"→ Requires Manual Legal Review"),
("2", RAG_RED, "Any agent returned Error?",
"→ Analysis Error"),
("3", RAG_RED, "Any agent returned Red?",
"→ Failed"),
("4", RAG_GREEN, "Otherwise",
"→ Passed"),
]
for idx, (num, color, question, result) in enumerate(decisions):
y = Inches(1.85) + idx * Inches(1.15)
x = Inches(7.0)
# Decision card
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
x, y, Inches(5.5), Inches(0.95))
card.fill.solid()
card.fill.fore_color.rgb = GREY_100
card.line.color.rgb = GREY_300
card.line.width = Pt(1)
card.adjustments[0] = 0.08
# Step number
add_circle(slide, x + Inches(0.15), y + Inches(0.2), Inches(0.45),
color, num, font_size=14, font_color=WHITE)
add_textbox(slide, x + Inches(0.75), y + Inches(0.1),
Inches(4.5), Inches(0.35),
question, font_size=13, color=DARK_NAVY, bold=True)
add_textbox(slide, x + Inches(0.75), y + Inches(0.5),
Inches(4.5), Inches(0.35),
result, font_size=13, color=ACTIVE_BLUE, bold=True)
def make_campaign_lifecycle_slide(prs):
"""Slide 14: Campaign & proof lifecycle."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"Campaign & Proof Lifecycle", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Flow steps
steps = [
("Create\nCampaign", "Set brand, agency,\nclient lead"),
("Upload\nProof", "Name, channel,\nsub-channel, type"),
("AI\nAnalysis", "4 agents analyse\nin parallel"),
("Review\nFeedback", "RAG status per\nagent + summary"),
("Resolve or\nRevise", "Mark issues resolved\nor upload new version"),
("Export\nReport", "PDF with full\nfeedback details"),
]
step_w = Inches(1.7)
step_h = Inches(1.3)
desc_h = Inches(0.9)
gap = Inches(0.25)
total_w = len(steps) * step_w + (len(steps) - 1) * gap
start_x = (SLIDE_WIDTH - total_w) / 2
step_y = Inches(1.8)
colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL, ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL]
for idx, (title, desc) in enumerate(steps):
x = start_x + idx * (step_w + gap)
# Step box
add_rounded_rect(slide, x, step_y, step_w, step_h,
colors[idx], title, font_size=13, font_color=WHITE, bold=True)
# Description below
add_textbox(slide, x, step_y + step_h + Inches(0.15), step_w, desc_h,
desc, font_size=11, color=GREY_700, alignment=PP_ALIGN.CENTER,
line_spacing=1.4)
# Arrow
if idx < len(steps) - 1:
arrow_x = x + step_w
arrow_y = step_y + step_h / 2
add_connector_line(slide, arrow_x, arrow_y,
arrow_x + gap, arrow_y, GREY_300, 2)
# Bottom section: Campaign table details
add_textbox(slide, MARGIN_LEFT, Inches(4.4), CONTENT_WIDTH, Inches(0.4),
"Campaign Table Features", font_size=16, color=TEAL, bold=True)
add_bullet_list(slide, MARGIN_LEFT, Inches(4.9), Inches(5.5), Inches(2.5),
[
"Sortable, filterable columns — name, status, proof count, agency",
"\"My Campaigns Only\" toggle for personal workspace",
"Show/Hide Completed toggle",
"Quick-create modal with brand guideline selection",
], font_size=13)
add_textbox(slide, Inches(7.0), Inches(4.4), Inches(5.5), Inches(0.4),
"Proof Management", font_size=16, color=TEAL, bold=True)
add_bullet_list(slide, Inches(7.0), Inches(4.9), Inches(5.5), Inches(2.5),
[
"Dependent dropdowns: Channel → Sub-Channel → Proof Type",
"Version history with download and comparison",
"Duplicate file detection via MD5 hash",
"Supported: Social, Display, Copy channels (22+ formats)",
], font_size=13)
def make_websocket_slide(prs):
"""Slide 16: WebSocket live analysis."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"Real-Time WebSocket Analysis", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Message flow diagram
messages = [
("Client", ACTIVE_BLUE, "analyze", "File (base64) + metadata sent via WebSocket"),
("Server", TEAL, "agent_started", "\"Legal Agent is analysing...\""),
("Server", TEAL, "agent_completed", "Legal Agent review returned (RAG + feedback)"),
("Server", TEAL, "agent_started", "\"Brand Agent is analysing...\""),
("Server", TEAL, "agent_completed", "Brand Agent review returned"),
("Server", ELECTRIC_VIOLET, "summary", "Lead Agent summary generated"),
("Server", RAG_GREEN, "complete", "Full AgentReview + proof ID + version ID"),
]
y = Inches(1.5)
for sender, color, msg_type, desc in messages:
# Direction indicator
if sender == "Client":
arrow_text = ""
label_color = ACTIVE_BLUE
else:
arrow_text = ""
label_color = TEAL
# Message type badge
add_rounded_rect(slide, MARGIN_LEFT, y, Inches(0.8), Inches(0.4),
GREY_100, sender, font_size=10, font_color=GREY_700,
line_color=GREY_300)
add_textbox(slide, Inches(1.75), y + Inches(0.03), Inches(0.3), Inches(0.35),
arrow_text, font_size=16, color=label_color, bold=True)
add_rounded_rect(slide, Inches(2.2), y, Inches(2.0), Inches(0.4),
color, msg_type, font_size=11, font_color=WHITE, bold=True)
add_textbox(slide, Inches(4.4), y + Inches(0.05), Inches(5), Inches(0.35),
desc, font_size=12, color=GREY_700)
y += Inches(0.55)
# Right side: Key features
add_textbox(slide, Inches(9.8), Inches(1.3), Inches(3), Inches(0.4),
"Key Capabilities", font_size=16, color=TEAL, bold=True)
add_bullet_list(slide, Inches(9.8), Inches(1.8), Inches(3.2), Inches(4.5),
[
"Parallel agent execution via asyncio.gather()",
"Real-time progress — agents report as they finish",
"PDF rasterisation (up to 10 pages)",
"Revision-aware analysis with previous review context",
"Authenticated via MSAL bearer token",
"Automatic proof persistence to database",
], font_size=12)
def make_feedback_reports_slide(prs):
"""Slide 18: Feedback reports & PDF export."""
make_two_column_slide(prs,
"Feedback Reports & PDF Export",
"Asset Detail View",
[
"Two-column layout: proof preview (left) + agent feedback (right)",
"RAG status badge per agent with colour-coded indicators",
"Detailed text feedback with constructive recommendations",
"Actionable issues listed with 'Mark as Resolved' capability",
"Resolution notes recorded for audit trail",
"Flag incorrect feedback — sent to Auditing dashboard",
"Version history with one-click navigation between versions",
],
"PDF Export",
[
"Single Proof Report — detailed feedback for one proof",
"Campaign Report — consolidated report for all proofs",
"Cover page with Barclays branding, campaign name, date",
"Proof preview with metadata (name, version, channel)",
"Lead Agent summary with overall status",
"Per-agent sections: RAG status, full feedback, issues list",
"Professional formatting ready for stakeholder review",
])
def make_knowledge_base_slide(prs):
"""Slide 20: Knowledge Base management."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"Knowledge Base Management", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Pipeline steps
pipe_steps = [
("Upload\nDocuments", "PDF or Markdown\nbrand/legal/channel\nguidelines"),
("Parse &\nConvert", "AI converts uploads\nto structured\nmarkdown"),
("Generate\nSpec", "AI synthesises all\ndocuments into\nunified specification"),
("Version\nControl", "New spec version\ncreated with diff\ncomparison"),
("Activate", "Admin selects\nwhich version\nagents use"),
]
step_w = Inches(2.0)
step_h = Inches(1.2)
gap = Inches(0.35)
total_w = len(pipe_steps) * step_w + (len(pipe_steps) - 1) * gap
start_x = (SLIDE_WIDTH - total_w) / 2
step_y = Inches(1.5)
pipe_colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, TEAL, ACTIVE_BLUE, RAG_GREEN]
for idx, (title, desc) in enumerate(pipe_steps):
x = start_x + idx * (step_w + gap)
add_rounded_rect(slide, x, step_y, step_w, step_h,
pipe_colors[idx], title, font_size=13, font_color=WHITE, bold=True)
add_textbox(slide, x, step_y + step_h + Inches(0.1), step_w, Inches(0.9),
desc, font_size=11, color=GREY_700, alignment=PP_ALIGN.CENTER,
line_spacing=1.4)
if idx < len(pipe_steps) - 1:
ax = x + step_w
ay = step_y + step_h / 2
add_connector_line(slide, ax, ay, ax + gap, ay, GREY_300, 2)
# Knowledge bases list
add_textbox(slide, MARGIN_LEFT, Inches(4.3), CONTENT_WIDTH, Inches(0.4),
"Five Knowledge Bases", font_size=16, color=TEAL, bold=True)
kbs = [
("Legal", "FCA regulations, ASA/CAP code, financial promotion rules"),
("Brand — Barclays", "15+ guideline documents covering logo, colour, typography, design"),
("Brand — Barclaycard", "Core principles, digital guidelines, email and social specs"),
("Channel Best Practices", "LinkedIn, Reddit, platform optimisation, creative inspiration"),
("Channel Tech Specs", "Social templates, dimension specs, platform-specific requirements"),
]
kb_w = Inches(2.2)
kb_h = Inches(1.8)
kb_gap = Inches(0.25)
total_kb_w = len(kbs) * kb_w + (len(kbs) - 1) * kb_gap
kb_start_x = (SLIDE_WIDTH - total_kb_w) / 2
kb_y = Inches(4.8)
kb_colors = [ACTIVE_BLUE, ELECTRIC_VIOLET, ELECTRIC_VIOLET, TEAL, CYAN_BRAND]
for idx, (name, desc) in enumerate(kbs):
x = kb_start_x + idx * (kb_w + kb_gap)
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
x, kb_y, kb_w, kb_h)
card.fill.solid()
card.fill.fore_color.rgb = DARK_NAVY
card.line.fill.background()
card.adjustments[0] = 0.06
# Accent
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
x + Inches(0.2), kb_y + Inches(0.15),
Inches(0.6), Pt(3))
accent.fill.solid()
accent.fill.fore_color.rgb = kb_colors[idx]
accent.line.fill.background()
add_textbox(slide, x + Inches(0.15), kb_y + Inches(0.35),
kb_w - Inches(0.3), Inches(0.35),
name, font_size=12, color=LIME, bold=True,
alignment=PP_ALIGN.CENTER)
add_textbox(slide, x + Inches(0.15), kb_y + Inches(0.75),
kb_w - Inches(0.3), Inches(0.9),
desc, font_size=10, color=GREY_300,
alignment=PP_ALIGN.CENTER, line_spacing=1.4)
def make_admin_three_col_slide(prs):
"""Slide 21: Analytics, Auditing & Settings (3-column)."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"Analytics, Auditing & Settings", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
columns = [
("Analytics", ACTIVE_BLUE, [
"Proofs Uploaded — total count",
"Pass Rate — % of proofs that passed",
"Issues Found — total across agents",
"Time Saved — hours estimated",
"AI Performance Summary — weekly trends",
"Agent Performance Table — per-agent stats",
]),
("Auditing", ELECTRIC_VIOLET, [
"Flags Tab — user-reported incorrect feedback",
"Resolutions Tab — user-resolved issues",
"Errors Tab — analysis failures",
"Links to specific proof and version",
"Full audit trail with timestamps",
"Agency-filterable for oversight admins",
]),
("Settings", TEAL, [
"Manage Channels — add/remove options",
"Sub-Channels — dependent on parent",
"Proof Types — dependent on sub-channel",
"Changes propagate immediately",
"Admin-only access for editing",
"Oversight admins get read-only view",
]),
]
col_w = Inches(3.7)
col_gap = Inches(0.3)
total_col_w = len(columns) * col_w + (len(columns) - 1) * col_gap
start_x = (SLIDE_WIDTH - total_col_w) / 2
for idx, (title, color, items) in enumerate(columns):
x = start_x + idx * (col_w + col_gap)
y = Inches(1.4)
# Column header card
add_rounded_rect(slide, x, y, col_w, Inches(0.5),
color, title, font_size=15, font_color=WHITE, bold=True)
# Bullet list
add_bullet_list(slide, x + Inches(0.1), y + Inches(0.7),
col_w - Inches(0.2), Inches(5.0),
items, font_size=13, line_spacing=1.4)
def make_user_roles_slide(prs):
"""Slide 22: User Roles & Access Control (table-style)."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.4), CONTENT_WIDTH, Inches(0.7),
"User Roles & Access Control", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(1.05),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Table data
headers = ["Role", "Write", "Analytics", "Auditing", "Knowledge\nBase", "Settings", "User\nMgmt", "Agency\nFilter"]
rows_data = [
["Super Admin", "\u2713", "\u2713", "\u2713", "\u2713", "Full", "\u2713", "\u2713"],
["Oversight Admin", "\u2717", "\u2713", "\u2713", "\u2717", "Read", "\u2717", "\u2713"],
["Agency Admin", "\u2713", "\u2713", "\u2717", "\u2717", "Full", "\u2717", "\u2717"],
["Basic User", "\u2713", "\u2717", "\u2717", "\u2717", "\u2717", "\u2717", "\u2717"],
]
# Dimensions
col_widths = [Inches(1.8), Inches(0.9), Inches(1.2), Inches(1.2), Inches(1.2),
Inches(1.0), Inches(1.0), Inches(1.1)]
row_h = Inches(0.6)
header_h = Inches(0.7)
table_start_x = (SLIDE_WIDTH - sum(w for w in col_widths)) / 2
table_start_y = Inches(1.5)
# Draw header row
x = table_start_x
for i, header in enumerate(headers):
cell = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, table_start_y,
col_widths[i], header_h)
cell.fill.solid()
cell.fill.fore_color.rgb = DARK_NAVY
cell.line.color.rgb = DARK_NAVY
cell.line.width = Pt(0.5)
tf = cell.text_frame
tf.word_wrap = True
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
p = tf.paragraphs[0]
p.text = header
p.font.size = Pt(11)
p.font.color.rgb = LIME
p.font.bold = True
p.font.name = FONT_NAME
tf.margin_top = Pt(6)
tf.margin_bottom = Pt(6)
x += col_widths[i]
# Draw data rows
for row_idx, row in enumerate(rows_data):
x = table_start_x
y = table_start_y + header_h + row_idx * row_h
bg = WHITE if row_idx % 2 == 0 else GREY_100
for col_idx, val in enumerate(row):
cell = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y,
col_widths[col_idx], row_h)
cell.fill.solid()
cell.fill.fore_color.rgb = bg
cell.line.color.rgb = GREY_300
cell.line.width = Pt(0.5)
tf = cell.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = val
p.font.size = Pt(12)
p.font.name = FONT_NAME
tf.margin_top = Pt(4)
tf.margin_bottom = Pt(4)
if col_idx == 0:
p.font.color.rgb = DARK_NAVY
p.font.bold = True
p.alignment = PP_ALIGN.LEFT
tf.margin_left = Pt(8)
else:
p.alignment = PP_ALIGN.CENTER
if val == "\u2713":
p.font.color.rgb = RAG_GREEN
p.font.bold = True
elif val == "\u2717":
p.font.color.rgb = RAG_RED
elif val in ("Full", "Read"):
p.font.color.rgb = ACTIVE_BLUE
p.font.bold = True
else:
p.font.color.rgb = DARK_NAVY
x += col_widths[col_idx]
# Auth info below table
auth_y = table_start_y + header_h + len(rows_data) * row_h + Inches(0.6)
add_textbox(slide, MARGIN_LEFT, auth_y, CONTENT_WIDTH, Inches(0.4),
"Authentication & Security", font_size=16, color=TEAL, bold=True)
add_bullet_list(slide, MARGIN_LEFT, auth_y + Inches(0.4), CONTENT_WIDTH, Inches(2.5),
[
"Azure AD / O365 SSO integration via MSAL — shared with CopyGenAI application",
"Bearer token authentication on all API requests and WebSocket connections",
"Agency-scoped data access — basic users only see their own agency's campaigns",
"User Management screen (Super Admin only) — assign roles, agencies, view change history",
], font_size=13)
def make_tech_architecture_slide(prs):
"""Slide 23: Technical Architecture (layered diagram)."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, WHITE)
add_textbox(slide, MARGIN_LEFT, Inches(0.3), CONTENT_WIDTH, Inches(0.6),
"Technical Architecture", font_size=28, color=DARK_NAVY, bold=True)
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, MARGIN_LEFT, Inches(0.85),
Inches(1.5), Pt(3))
bar.fill.solid()
bar.fill.fore_color.rgb = ACTIVE_BLUE
bar.line.fill.background()
# Layer definitions
layers = [
("Frontend", ACTIVE_BLUE, [
("React + TypeScript", Inches(2.5)),
("Tailwind CSS", Inches(1.8)),
("MSAL Auth", Inches(1.5)),
("WebSocket Client", Inches(2.0)),
("REST Client", Inches(1.6)),
]),
("Backend API", ELECTRIC_VIOLET, [
("FastAPI (Python)", Inches(2.5)),
("WebSocket /ws/analyze", Inches(2.3)),
("REST /api/*", Inches(1.8)),
("Analysis Service", Inches(2.0)),
]),
("AI Layer", TEAL, [
("Legal Agent", Inches(1.8)),
("Brand Agent", Inches(1.8)),
("Best Practices Agent", Inches(2.3)),
("Tech Specs Agent", Inches(2.0)),
("Lead Agent", Inches(1.6)),
]),
("Data Layer", DARK_NAVY, [
("PostgreSQL", Inches(2.0)),
("SQLAlchemy Async", Inches(2.2)),
("Alembic Migrations", Inches(2.2)),
("File Storage", Inches(1.8)),
]),
]
layer_h = Inches(1.1)
layer_gap = Inches(0.25)
start_y = Inches(1.15)
label_w = Inches(1.8)
content_start_x = MARGIN_LEFT + label_w + Inches(0.2)
# Reserve 2.6" on right for external service boxes
ext_col_w = Inches(2.3)
ext_gap = Inches(0.3)
content_end_x = SLIDE_WIDTH - MARGIN_RIGHT - ext_col_w - ext_gap
for layer_idx, (layer_name, color, components) in enumerate(layers):
y = start_y + layer_idx * (layer_h + layer_gap)
# Layer label
add_rounded_rect(slide, MARGIN_LEFT, y, label_w, layer_h,
color, layer_name, font_size=13, font_color=WHITE, bold=True)
# Components
comp_gap = Inches(0.15)
total_comp_w = sum(w for _, w in components) + (len(components) - 1) * comp_gap
available_w = content_end_x - content_start_x
scale = min(1.0, available_w / total_comp_w)
cx = content_start_x
for comp_name, comp_w in components:
scaled_w = comp_w * scale
add_rounded_rect(slide, cx, y + Inches(0.15), scaled_w, layer_h - Inches(0.3),
GREY_100, comp_name, font_size=11, font_color=DARK_NAVY,
bold=False, line_color=color)
cx += scaled_w + comp_gap * scale
# External services on the right (in reserved column)
ext_x = SLIDE_WIDTH - MARGIN_RIGHT - ext_col_w
# Position Azure AD aligned with Backend API row
ext_y_azure = start_y + 1 * (layer_h + layer_gap) + Inches(0.15)
add_rounded_rect(slide, ext_x, ext_y_azure, ext_col_w, layer_h - Inches(0.3),
DARK_NAVY, "Azure AD", font_size=12, font_color=LIME, bold=True)
# Position Gemini aligned with AI Layer row
ext_y_gemini = start_y + 2 * (layer_h + layer_gap) + Inches(0.15)
add_rounded_rect(slide, ext_x, ext_y_gemini, ext_col_w, layer_h - Inches(0.3),
DARK_NAVY, "Google Gemini\n2.5 Flash", font_size=12,
font_color=LIME, bold=True)
# Bottom note
note_y = start_y + len(layers) * (layer_h + layer_gap) + Inches(0.1)
add_textbox(slide, MARGIN_LEFT, note_y, CONTENT_WIDTH, Inches(0.8),
"All communication secured via MSAL bearer tokens. "
"Agents run in parallel via asyncio. "
"Database uses async SQLAlchemy + asyncpg for non-blocking I/O.",
font_size=12, color=GREY_700)
def make_closing_slide(prs):
"""Slide 24: Closing slide with logo."""
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide, DARK_NAVY)
# Logo
if LOGO_PATH.exists():
slide.shapes.add_picture(
str(LOGO_PATH),
Inches(0.8), Inches(1.2),
height=Inches(1.8)
)
# Accent bar
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(0.8), Inches(3.4),
Inches(2.0), Pt(4))
bar.fill.solid()
bar.fill.fore_color.rgb = LIME
bar.line.fill.background()
tf = add_rich_textbox(slide, Inches(0.8), Inches(3.7), Inches(10), Inches(1.0))
p = tf.paragraphs[0]
run1 = p.add_run()
run1.text = "Intelligent Review. "
run1.font.size = Pt(32)
run1.font.color.rgb = ELECTRIC_VIOLET
run1.font.bold = True
run1.font.name = FONT_NAME
run2 = p.add_run()
run2.text = "Confident Delivery."
run2.font.size = Pt(32)
run2.font.color.rgb = WHITE
run2.font.bold = True
run2.font.name = FONT_NAME
add_textbox(slide, Inches(0.8), Inches(4.8), Inches(8), Inches(0.5),
"AI-powered proof review for Barclays marketing materials",
font_size=16, color=GREY_300)
add_textbox(slide, Inches(0.8), Inches(5.6), Inches(6), Inches(0.5),
"Built by OLIVER Agency", font_size=14, color=GREY_700)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
prs = Presentation()
prs.slide_width = SLIDE_WIDTH
prs.slide_height = SLIDE_HEIGHT
# Slide 1: Title
make_title_slide(prs)
# Slide 2: Agenda
make_agenda_slide(prs)
# Slide 3: Section — The Challenge
make_section_divider(prs, 1, "The Challenge",
"Why manual proof review doesn't scale")
# Slide 4: Problem statement
make_problem_slide(prs)
# Slide 5: Section — Introducing Mod Comms
make_section_divider(prs, 2, "Introducing Mod Comms",
"AI-powered proof review at a glance")
# Slide 6: Solution overview
make_solution_slide(prs)
# Slide 7: Section — Multi-Agent AI System
make_section_divider(prs, 3, "Multi-Agent AI System",
"Four specialist agents working in parallel")
# Slide 8: Agent architecture diagram
make_agent_architecture_slide(prs)
# Slide 9: Legal Agent
make_legal_agent_slide(prs)
# Slide 10: Brand Agent
make_brand_agent_slide(prs)
# Slide 11: Channel Agents
make_channel_agents_slide(prs)
# Slide 12: RAG Status
make_rag_status_slide(prs)
# Slide 13: Section — Campaign Management
make_section_divider(prs, 4, "Campaign Management",
"Organising and tracking marketing proofs")
# Slide 14: Campaign lifecycle
make_campaign_lifecycle_slide(prs)
# Slide 15: Section — Real-Time Analysis
make_section_divider(prs, 5, "Real-Time Analysis",
"Live WebSocket-powered proof review")
# Slide 16: WebSocket analysis
make_websocket_slide(prs)
# Slide 17: Section — Feedback & Reporting
make_section_divider(prs, 6, "Feedback & Reporting",
"Structured results and PDF export")
# Slide 18: Feedback reports
make_feedback_reports_slide(prs)
# Slide 19: Section — Knowledge Base & Admin
make_section_divider(prs, 7, "Knowledge Base & Admin",
"Managing guidelines, analytics, and access control")
# Slide 20: Knowledge Base
make_knowledge_base_slide(prs)
# Slide 21: Analytics, Auditing, Settings
make_admin_three_col_slide(prs)
# Slide 22: User Roles
make_user_roles_slide(prs)
# Slide 23: Section — Technical Architecture
make_section_divider(prs, 8, "Technical Architecture",
"How it all fits together")
# Slide 24: Tech Architecture
make_tech_architecture_slide(prs)
# Slide 25: Closing
make_closing_slide(prs)
# Save
prs.save(str(OUTPUT_PPTX))
print(f"Presentation saved to: {OUTPUT_PPTX}")
print(f"Total slides: {len(prs.slides)}")
if __name__ == "__main__":
main()