""" 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()