#!/usr/bin/env python3 """Generate PS_AI_ConvAgent Plugin Documentation PowerPoint. Style inspired by Asterion VR PPT Model (dark theme, orange accents). """ 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 # ── Colors (from Asterion VR template) ────────────────────────────────────── BG_DARK = RGBColor(0x16, 0x1C, 0x26) BG_LIGHTER = RGBColor(0x1E, 0x25, 0x33) WHITE = RGBColor(0xFF, 0xFF, 0xFF) GRAY_LIGHT = RGBColor(0xAD, 0xAD, 0xAD) GRAY_MED = RGBColor(0x78, 0x78, 0x78) ORANGE = RGBColor(0xF2, 0x63, 0x00) TEAL = RGBColor(0x00, 0x96, 0x88) CYAN = RGBColor(0x4D, 0xD0, 0xE1) YELLOW_GRN = RGBColor(0xEE, 0xFF, 0x41) DARK_TEXT = RGBColor(0x16, 0x1C, 0x26) # Slide dimensions (16:9) SLIDE_W = Inches(13.333) SLIDE_H = Inches(7.5) prs = Presentation() prs.slide_width = SLIDE_W prs.slide_height = SLIDE_H # ── Helpers ───────────────────────────────────────────────────────────────── def set_slide_bg(slide, color): bg = slide.background fill = bg.fill fill.solid() fill.fore_color.rgb = color def add_rect(slide, left, top, width, height, fill_color=None, line_color=None): shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height) shape.line.fill.background() if fill_color: shape.fill.solid() shape.fill.fore_color.rgb = fill_color if line_color: shape.line.color.rgb = line_color shape.line.width = Pt(1) return shape def add_text_box(slide, left, top, width, height): return slide.shapes.add_textbox(left, top, width, height) def set_text(tf, text, size=14, color=WHITE, bold=False, alignment=PP_ALIGN.LEFT, font_name="Arial"): tf.clear() tf.word_wrap = True p = tf.paragraphs[0] p.text = text p.font.size = Pt(size) p.font.color.rgb = color p.font.bold = bold p.font.name = font_name p.alignment = alignment return p def add_para(tf, text, size=14, color=WHITE, bold=False, alignment=PP_ALIGN.LEFT, space_before=Pt(4), space_after=Pt(2), font_name="Arial", level=0): p = tf.add_paragraph() p.text = text p.font.size = Pt(size) p.font.color.rgb = color p.font.bold = bold p.font.name = font_name p.alignment = alignment p.level = level if space_before: p.space_before = space_before if space_after: p.space_after = space_after return p def add_bullet(tf, text, size=12, color=WHITE, bold=False, level=0, space_before=Pt(2)): p = tf.add_paragraph() p.text = text p.font.size = Pt(size) p.font.color.rgb = color p.font.bold = bold p.font.name = "Arial" p.level = level p.space_before = space_before p.space_after = Pt(1) return p def add_section_header(slide, title, subtitle=""): """Add a section divider slide.""" set_slide_bg(slide, BG_DARK) # Accent bar add_rect(slide, Inches(0), Inches(3.2), Inches(13.333), Pt(4), fill_color=ORANGE) # Title tb = add_text_box(slide, Inches(0.8), Inches(2.0), Inches(11.7), Inches(1.2)) set_text(tb.text_frame, title, size=40, color=WHITE, bold=True, alignment=PP_ALIGN.LEFT) if subtitle: add_para(tb.text_frame, subtitle, size=18, color=GRAY_LIGHT, alignment=PP_ALIGN.LEFT, space_before=Pt(12)) def add_page_number(slide, num, total): tb = add_text_box(slide, Inches(12.3), Inches(7.0), Inches(0.8), Inches(0.4)) set_text(tb.text_frame, f"{num}/{total}", size=9, color=GRAY_MED, alignment=PP_ALIGN.RIGHT) def add_footer_bar(slide): add_rect(slide, Inches(0), Inches(7.15), SLIDE_W, Pt(2), fill_color=ORANGE) def add_top_bar(slide, title): add_rect(slide, Inches(0), Inches(0), SLIDE_W, Inches(0.7), fill_color=BG_LIGHTER) tb = add_text_box(slide, Inches(0.5), Inches(0.1), Inches(10), Inches(0.5)) set_text(tb.text_frame, title, size=14, color=ORANGE, bold=True) add_rect(slide, Inches(0), Inches(0.7), SLIDE_W, Pt(2), fill_color=ORANGE) def make_content_slide(title, section_label=""): slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank set_slide_bg(slide, BG_DARK) if section_label: add_top_bar(slide, section_label) # Title tb = add_text_box(slide, Inches(0.6), Inches(0.9), Inches(12), Inches(0.6)) set_text(tb.text_frame, title, size=26, color=WHITE, bold=True) add_footer_bar(slide) return slide def add_placeholder_box(slide, left, top, width, height, text): """Add a dashed placeholder box for screenshots/images the user will add.""" shape = add_rect(slide, left, top, width, height, fill_color=RGBColor(0x22, 0x2A, 0x38)) shape.line.color.rgb = ORANGE shape.line.width = Pt(1.5) shape.line.dash_style = 4 # dash tb = add_text_box(slide, left + Inches(0.2), top + Inches(0.1), width - Inches(0.4), height - Inches(0.2)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "📷 " + text, size=11, color=ORANGE, bold=False, alignment=PP_ALIGN.CENTER) tf.paragraphs[0].alignment = PP_ALIGN.CENTER tb.text_frame.paragraphs[0].space_before = Pt(0) return shape # Track total slides for numbering at the end slides_data = [] # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 1: Title # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) # Orange accent line add_rect(slide, Inches(0), Inches(4.8), SLIDE_W, Pt(4), fill_color=ORANGE) # Title tb = add_text_box(slide, Inches(0.8), Inches(2.2), Inches(11.7), Inches(1.0)) set_text(tb.text_frame, "PS_AI_ConvAgent", size=52, color=WHITE, bold=True, alignment=PP_ALIGN.LEFT) # Subtitle tb = add_text_box(slide, Inches(0.8), Inches(3.4), Inches(11.7), Inches(1.2)) set_text(tb.text_frame, "Conversational AI Plugin for Unreal Engine 5", size=24, color=GRAY_LIGHT, alignment=PP_ALIGN.LEFT) add_para(tb.text_frame, "ElevenLabs Integration · Real-Time Voice · Full-Body Animation · Multiplayer", size=14, color=TEAL, alignment=PP_ALIGN.LEFT, space_before=Pt(12)) # Bottom info tb = add_text_box(slide, Inches(0.8), Inches(5.4), Inches(11.7), Inches(1.0)) set_text(tb.text_frame, "ASTERION", size=18, color=ORANGE, bold=True, alignment=PP_ALIGN.LEFT) add_para(tb.text_frame, "Plugin Documentation · v1.0 · March 2026", size=12, color=GRAY_MED, alignment=PP_ALIGN.LEFT, space_before=Pt(6)) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 2: Table of Contents # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Table of Contents", "OVERVIEW") tb = add_text_box(slide, Inches(0.8), Inches(1.7), Inches(5.5), Inches(5.0)) tf = tb.text_frame tf.word_wrap = True sections = [ ("01", "Plugin Overview", "Architecture, components, and key features"), ("02", "Quick Start", "Get up and running in 5 minutes"), ("03", "ElevenLabs Component", "Conversation lifecycle and configuration"), ("04", "Posture System", "Head, eye, and body tracking"), ("05", "Facial Expressions", "Emotion-driven animations"), ("06", "Lip Sync", "Real-time audio-driven visemes"), ("07", "Interaction System", "Multi-agent selection and routing"), ("08", "Agent Configuration", "Data asset and editor tools"), ("09", "Network & Multiplayer", "Replication, LOD, and Opus compression"), ("10", "Animation Nodes", "AnimBP integration (Body + Face)"), ("11", "Blueprint Library", "Utility functions"), ] for i, (num, title, desc) in enumerate(sections): if i == 0: p = tf.paragraphs[0] else: p = tf.add_paragraph() p.space_before = Pt(8) p.space_after = Pt(2) run_num = p.add_run() run_num.text = f"{num} " run_num.font.size = Pt(14) run_num.font.color.rgb = ORANGE run_num.font.bold = True run_num.font.name = "Arial" run_title = p.add_run() run_title.text = title run_title.font.size = Pt(14) run_title.font.color.rgb = WHITE run_title.font.bold = True run_title.font.name = "Arial" p2 = tf.add_paragraph() p2.text = f" {desc}" p2.font.size = Pt(10) p2.font.color.rgb = GRAY_LIGHT p2.font.name = "Arial" p2.space_before = Pt(0) p2.space_after = Pt(4) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 3: Plugin Overview - Architecture # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "01 Plugin Overview", "Architecture and Key Features") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 4: What is PS_AI_ConvAgent? # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("What is PS_AI_ConvAgent?", "OVERVIEW") tb = add_text_box(slide, Inches(0.6), Inches(1.7), Inches(7.0), Inches(5.0)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "A full-stack Unreal Engine 5 plugin for real-time conversational AI NPCs.", size=14, color=WHITE, bold=False) add_para(tf, "", size=6, color=WHITE) features = [ ("Voice Conversation", "Two-way real-time voice via ElevenLabs Conversational AI API (WebSocket)"), ("Full-Body Animation", "Procedural head/eye/body tracking, emotion-driven facial expressions, audio-driven lip sync"), ("Multi-Agent Support", "Distance/view-cone selection, centralized mic routing, automatic agent switching"), ("Multiplayer Ready", "Full network replication, Opus audio compression (16x), audio/lip-sync LOD culling"), ("MetaHuman Compatible", "ARKit blendshapes, CTRL_expressions curves, OVR visemes, AnimBP nodes"), ("Editor Tools", "Agent configuration data asset with voice/model/LLM pickers, REST API sync"), ("Persistent Memory", "WebSocket stays open across interactions — agent remembers the full conversation"), ] for title, desc in features: add_bullet(tf, "", size=4, color=WHITE, level=0, space_before=Pt(6)) p = tf.add_paragraph() p.space_before = Pt(0) p.space_after = Pt(2) run_t = p.add_run() run_t.text = f"▸ {title}: " run_t.font.size = Pt(12) run_t.font.color.rgb = ORANGE run_t.font.bold = True run_t.font.name = "Arial" run_d = p.add_run() run_d.text = desc run_d.font.size = Pt(11) run_d.font.color.rgb = GRAY_LIGHT run_d.font.name = "Arial" add_placeholder_box(slide, Inches(8.2), Inches(1.7), Inches(4.5), Inches(5.0), "SCREENSHOT: Vue d'ensemble de l'éditeur UE5 avec un NPC agent en scène.\n" "Montrer le viewport avec le personnage + le Details panel affichant les composants " "(ElevenLabsComponent, PostureComponent, FacialExpressionComponent, LipSyncComponent).") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 5: Architecture Diagram # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Component Architecture", "OVERVIEW") # Left column - NPC Actor tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(5.8), Inches(0.4)) set_text(tb.text_frame, "NPC Actor (Server-side)", size=16, color=ORANGE, bold=True) components_npc = [ ("ElevenLabsComponent", "WebSocket, audio pipeline, conversation lifecycle", TEAL), ("PostureComponent", "Head/eye/body procedural tracking", CYAN), ("FacialExpressionComponent", "Emotion-to-animation blending", CYAN), ("LipSyncComponent", "FFT spectral analysis → ARKit blendshapes", CYAN), ("MicrophoneCaptureComponent", "WASAPI capture, resample to 16kHz", TEAL), ] y_pos = 2.1 for name, desc, accent in components_npc: box = add_rect(slide, Inches(0.6), Inches(y_pos), Inches(5.8), Inches(0.7), fill_color=RGBColor(0x1E, 0x28, 0x36)) box.line.color.rgb = accent box.line.width = Pt(1) tb = add_text_box(slide, Inches(0.8), Inches(y_pos + 0.05), Inches(5.4), Inches(0.6)) tf = tb.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = name run.font.size = Pt(11) run.font.color.rgb = WHITE run.font.bold = True run.font.name = "Arial" p2 = tf.add_paragraph() p2.text = desc p2.font.size = Pt(9) p2.font.color.rgb = GRAY_LIGHT p2.font.name = "Arial" p2.space_before = Pt(1) y_pos += 0.8 # Right column - Player Pawn tb = add_text_box(slide, Inches(7.0), Inches(1.6), Inches(5.8), Inches(0.4)) set_text(tb.text_frame, "Player Pawn (Client-side)", size=16, color=ORANGE, bold=True) components_player = [ ("InteractionComponent", "Agent selection, mic routing, RPC relay", TEAL), ] y_pos = 2.1 for name, desc, accent in components_player: box = add_rect(slide, Inches(7.0), Inches(y_pos), Inches(5.8), Inches(0.7), fill_color=RGBColor(0x1E, 0x28, 0x36)) box.line.color.rgb = accent box.line.width = Pt(1) tb = add_text_box(slide, Inches(7.2), Inches(y_pos + 0.05), Inches(5.4), Inches(0.6)) tf = tb.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = name run.font.size = Pt(11) run.font.color.rgb = WHITE run.font.bold = True run.font.name = "Arial" p2 = tf.add_paragraph() p2.text = desc p2.font.size = Pt(9) p2.font.color.rgb = GRAY_LIGHT p2.font.name = "Arial" p2.space_before = Pt(1) y_pos += 0.8 # Subsystem box = add_rect(slide, Inches(7.0), Inches(3.1), Inches(5.8), Inches(0.7), fill_color=RGBColor(0x1E, 0x28, 0x36)) box.line.color.rgb = YELLOW_GRN box.line.width = Pt(1) tb = add_text_box(slide, Inches(7.2), Inches(3.15), Inches(5.4), Inches(0.6)) tf = tb.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = "InteractionSubsystem" run.font.size = Pt(11) run.font.color.rgb = WHITE run.font.bold = True run.font.name = "Arial" p2 = tf.add_paragraph() p2.text = "World subsystem — agent registry & discovery" p2.font.size = Pt(9) p2.font.color.rgb = GRAY_LIGHT p2.font.name = "Arial" p2.space_before = Pt(1) # Data Assets tb = add_text_box(slide, Inches(7.0), Inches(4.2), Inches(5.8), Inches(0.4)) set_text(tb.text_frame, "Data Assets", size=16, color=ORANGE, bold=True) assets = [ ("AgentConfig", "Agent ID, voice, LLM, prompt, emotions, dynamic variables"), ("EmotionPoseMap", "7 emotions × 3 intensities → AnimSequence references"), ("LipSyncPoseMap", "15 OVR visemes → AnimSequence pose references"), ] y_pos = 4.7 for name, desc in assets: box = add_rect(slide, Inches(7.0), Inches(y_pos), Inches(5.8), Inches(0.55), fill_color=RGBColor(0x1E, 0x28, 0x36)) box.line.color.rgb = GRAY_LIGHT box.line.width = Pt(1) tb = add_text_box(slide, Inches(7.2), Inches(y_pos + 0.03), Inches(5.4), Inches(0.5)) tf = tb.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = name run.font.size = Pt(11) run.font.color.rgb = WHITE run.font.bold = True run.font.name = "Arial" run2 = p.add_run() run2.text = f" — {desc}" run2.font.size = Pt(9) run2.font.color.rgb = GRAY_LIGHT run2.font.name = "Arial" y_pos += 0.65 # Animation Nodes tb = add_text_box(slide, Inches(0.6), Inches(6.3), Inches(12.0), Inches(0.4)) tf = tb.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = "Animation Nodes (AnimBP): " run.font.size = Pt(11) run.font.color.rgb = ORANGE run.font.bold = True run.font.name = "Arial" run2 = p.add_run() run2.text = "PS AI ConvAgent Posture · PS AI ConvAgent Facial Expression · PS AI ConvAgent Lip Sync" run2.font.size = Pt(11) run2.font.color.rgb = GRAY_LIGHT run2.font.name = "Arial" add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 6: Quick Start - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "02 Quick Start", "Get up and running in 5 minutes") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 7: Quick Start - Step 1-3 # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Quick Start (1/2)", "QUICK START") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(7.0), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True # Step 1 set_text(tf, "Step 1: Enable the Plugin", size=16, color=ORANGE, bold=True) add_bullet(tf, "Edit > Plugins > search \"PS_AI_ConvAgent\" > Enable > Restart Editor", size=11, color=WHITE) # Step 2 add_para(tf, "", size=6, color=WHITE) add_para(tf, "Step 2: Set your API Key", size=16, color=ORANGE, bold=True, space_before=Pt(12)) add_bullet(tf, "Project Settings > Plugins > PS AI ConvAgent - ElevenLabs", size=11, color=WHITE) add_bullet(tf, "Paste your ElevenLabs API Key", size=11, color=WHITE) add_bullet(tf, "Set your default Agent ID (from elevenlabs.io dashboard)", size=11, color=WHITE) # Step 3 add_para(tf, "", size=6, color=WHITE) add_para(tf, "Step 3: Set up the NPC Actor", size=16, color=ORANGE, bold=True, space_before=Pt(12)) add_bullet(tf, "Add ElevenLabsComponent to your NPC actor (or Blueprint)", size=11, color=WHITE) add_bullet(tf, "Add PostureComponent for head/eye tracking (optional)", size=11, color=WHITE) add_bullet(tf, "Add FacialExpressionComponent for emotions (optional)", size=11, color=WHITE) add_bullet(tf, "Add LipSyncComponent for lip-sync (optional)", size=11, color=WHITE) add_bullet(tf, "Set AgentID on the component (or create an AgentConfig data asset)", size=11, color=WHITE) add_placeholder_box(slide, Inches(8.0), Inches(1.6), Inches(4.8), Inches(2.2), "SCREENSHOT: Project Settings > Plugins > PS AI ConvAgent - ElevenLabs.\n" "Montrer les champs API Key et Default Agent ID.") add_placeholder_box(slide, Inches(8.0), Inches(4.0), Inches(4.8), Inches(2.8), "SCREENSHOT: Details panel d'un NPC actor avec les 4 composants ajoutés:\n" "ElevenLabsComponent, PostureComponent, FacialExpressionComponent, LipSyncComponent.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 8: Quick Start - Step 4-5 # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Quick Start (2/2)", "QUICK START") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(7.0), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True # Step 4 set_text(tf, "Step 4: Set up the Player Pawn", size=16, color=ORANGE, bold=True) add_bullet(tf, "Add InteractionComponent to your Player Pawn Blueprint", size=11, color=WHITE) add_bullet(tf, "Configure MaxInteractionDistance (default 300 cm)", size=11, color=WHITE) add_bullet(tf, "bAutoStartConversation = true (automatic) or false (manual)", size=11, color=WHITE) add_bullet(tf, "bAutoManageListening = true for hands-free mic management", size=11, color=WHITE) # Step 5 add_para(tf, "", size=6, color=WHITE) add_para(tf, "Step 5: Wire Blueprint Events (optional)", size=16, color=ORANGE, bold=True, space_before=Pt(12)) add_bullet(tf, "OnAgentTranscript — display user speech-to-text", size=11, color=WHITE) add_bullet(tf, "OnAgentTextResponse — display agent's complete response", size=11, color=WHITE) add_bullet(tf, "OnAgentPartialResponse — real-time streaming subtitles", size=11, color=WHITE) add_bullet(tf, "OnAgentStartedSpeaking / OnAgentStoppedSpeaking — UI feedback", size=11, color=WHITE) add_bullet(tf, "OnAgentEmotionChanged — custom emotion reactions", size=11, color=WHITE) add_para(tf, "", size=6, color=WHITE) add_para(tf, "That's it! Walk near the NPC and start talking.", size=14, color=TEAL, bold=True, space_before=Pt(12)) add_placeholder_box(slide, Inches(8.0), Inches(1.6), Inches(4.8), Inches(2.5), "SCREENSHOT: Blueprint Event Graph montrant les principaux events:\n" "OnAgentConnected, OnAgentTranscript, OnAgentTextResponse, OnAgentStoppedSpeaking.\n" "Avec des Print String pour chaque event.") add_placeholder_box(slide, Inches(8.0), Inches(4.3), Inches(4.8), Inches(2.5), "SCREENSHOT: InteractionComponent sur le Player Pawn Blueprint.\n" "Details panel montrant MaxInteractionDistance, bAutoStartConversation, " "bAutoManageListening.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 9: ElevenLabs Component - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "03 ElevenLabs Component", "Conversation lifecycle, audio pipeline, and configuration") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 10: ElevenLabsComponent - Configuration # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("ElevenLabsComponent — Configuration", "REFERENCE") # Properties table props = [ ("AgentConfig", "Data Asset", "—", "Agent configuration override"), ("AgentID", "FString", "\"\"", "ElevenLabs Agent ID (fallback to project default)"), ("TurnMode", "Enum", "Server", "Server VAD (hands-free) or Client (push-to-talk)"), ("bPersistentSession", "bool", "true", "Keep WebSocket alive across Start/End cycles"), ("bAutoStartListening", "bool", "true", "Auto-open mic on connection (Server VAD only)"), ("MicChunkDurationMs", "int32", "100", "Mic chunk size in ms (20–500)"), ("bAllowInterruption", "bool", "true", "Allow user to interrupt agent"), ("AudioPreBufferMs", "int32", "2000", "Pre-buffer delay before playback (0–4000)"), ("ResponseTimeoutSeconds", "float", "10.0", "Timeout for server response"), ("MaxReconnectAttempts", "int32", "5", "Auto-reconnect attempts (exponential backoff)"), ("bExternalMicManagement", "bool", "false", "External mic via FeedExternalAudio()"), ("SoundAttenuation", "Asset", "null", "3D spatial audio settings"), ] # Header row add_rect(slide, Inches(0.5), Inches(1.55), Inches(12.3), Inches(0.35), fill_color=RGBColor(0x2A, 0x35, 0x45)) tb = add_text_box(slide, Inches(0.6), Inches(1.55), Inches(3.0), Inches(0.35)) set_text(tb.text_frame, "Property", size=10, color=ORANGE, bold=True) tb = add_text_box(slide, Inches(3.6), Inches(1.55), Inches(1.2), Inches(0.35)) set_text(tb.text_frame, "Type", size=10, color=ORANGE, bold=True) tb = add_text_box(slide, Inches(4.8), Inches(1.55), Inches(1.0), Inches(0.35)) set_text(tb.text_frame, "Default", size=10, color=ORANGE, bold=True) tb = add_text_box(slide, Inches(5.8), Inches(1.55), Inches(7.0), Inches(0.35)) set_text(tb.text_frame, "Description", size=10, color=ORANGE, bold=True) y = 1.95 for name, typ, default, desc in props: bg = RGBColor(0x1E, 0x28, 0x36) if props.index((name, typ, default, desc)) % 2 == 0 else BG_DARK add_rect(slide, Inches(0.5), Inches(y), Inches(12.3), Inches(0.33), fill_color=bg) tb = add_text_box(slide, Inches(0.6), Inches(y), Inches(3.0), Inches(0.33)) set_text(tb.text_frame, name, size=9, color=WHITE, bold=True) tb = add_text_box(slide, Inches(3.6), Inches(y), Inches(1.2), Inches(0.33)) set_text(tb.text_frame, typ, size=9, color=GRAY_LIGHT) tb = add_text_box(slide, Inches(4.8), Inches(y), Inches(1.0), Inches(0.33)) set_text(tb.text_frame, default, size=9, color=TEAL) tb = add_text_box(slide, Inches(5.8), Inches(y), Inches(7.0), Inches(0.33)) set_text(tb.text_frame, desc, size=9, color=GRAY_LIGHT) y += 0.33 add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 11: ElevenLabsComponent - Events # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("ElevenLabsComponent — Events", "REFERENCE") events = [ ("OnAgentConnected", "(ConversationInfo)", "WebSocket connected, session ready"), ("OnAgentDisconnected", "(StatusCode, Reason)", "WebSocket closed"), ("OnAgentError", "(ErrorMessage)", "Connection or protocol error"), ("OnAgentTranscript", "(Segment)", "Real-time user speech-to-text"), ("OnAgentTextResponse", "(ResponseText)", "Agent's complete text response"), ("OnAgentPartialResponse", "(PartialText)", "Streaming LLM tokens (subtitles)"), ("OnAgentStartedSpeaking", "()", "First audio chunk arrived"), ("OnAgentStoppedSpeaking", "()", "Audio playback finished"), ("OnAgentInterrupted", "()", "Agent speech interrupted"), ("OnAgentStartedGenerating", "()", "Server started LLM generation"), ("OnAgentResponseTimeout", "()", "Server response timeout"), ("OnAgentEmotionChanged", "(Emotion, Intensity)", "Agent set emotion via tool"), ("OnAgentClientToolCall", "(ToolCall)", "Custom client tool invocation"), ] add_rect(slide, Inches(0.5), Inches(1.55), Inches(12.3), Inches(0.32), fill_color=RGBColor(0x2A, 0x35, 0x45)) tb = add_text_box(slide, Inches(0.6), Inches(1.55), Inches(3.5), Inches(0.32)) set_text(tb.text_frame, "Event", size=10, color=ORANGE, bold=True) tb = add_text_box(slide, Inches(4.1), Inches(1.55), Inches(3.0), Inches(0.32)) set_text(tb.text_frame, "Parameters", size=10, color=ORANGE, bold=True) tb = add_text_box(slide, Inches(7.1), Inches(1.55), Inches(5.7), Inches(0.32)) set_text(tb.text_frame, "Description", size=10, color=ORANGE, bold=True) y = 1.9 for name, params, desc in events: bg = RGBColor(0x1E, 0x28, 0x36) if events.index((name, params, desc)) % 2 == 0 else BG_DARK add_rect(slide, Inches(0.5), Inches(y), Inches(12.3), Inches(0.32), fill_color=bg) tb = add_text_box(slide, Inches(0.6), Inches(y), Inches(3.5), Inches(0.32)) set_text(tb.text_frame, name, size=9, color=WHITE, bold=True) tb = add_text_box(slide, Inches(4.1), Inches(y), Inches(3.0), Inches(0.32)) set_text(tb.text_frame, params, size=9, color=TEAL) tb = add_text_box(slide, Inches(7.1), Inches(y), Inches(5.7), Inches(0.32)) set_text(tb.text_frame, desc, size=9, color=GRAY_LIGHT) y += 0.32 add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 12: ElevenLabsComponent - Functions # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("ElevenLabsComponent — Functions", "REFERENCE") # Callable tb = add_text_box(slide, Inches(0.6), Inches(1.5), Inches(5.5), Inches(0.3)) set_text(tb.text_frame, "Blueprint Callable", size=14, color=ORANGE, bold=True) funcs_call = [ ("StartConversation()", "Open WebSocket and begin conversation"), ("EndConversation()", "Stop mic, stop audio, close WebSocket (if non-persistent)"), ("StartListening()", "Open microphone, start streaming audio to server"), ("StopListening()", "Close microphone, flush remaining audio"), ("SendTextMessage(Text)", "Send text without microphone"), ("InterruptAgent()", "Stop the agent's current utterance"), ("FeedExternalAudio(FloatPCM)", "Feed mic data from an external source"), ] y = 1.85 for fname, desc in funcs_call: tb = add_text_box(slide, Inches(0.6), Inches(y), Inches(5.5), Inches(0.28)) tf = tb.text_frame p = tf.paragraphs[0] r1 = p.add_run() r1.text = fname + " " r1.font.size = Pt(10) r1.font.color.rgb = WHITE r1.font.bold = True r1.font.name = "Consolas" r2 = p.add_run() r2.text = desc r2.font.size = Pt(9) r2.font.color.rgb = GRAY_LIGHT r2.font.name = "Arial" y += 0.3 # Pure tb = add_text_box(slide, Inches(0.6), Inches(y + 0.15), Inches(5.5), Inches(0.3)) set_text(tb.text_frame, "Blueprint Pure (Getters)", size=14, color=ORANGE, bold=True) y += 0.5 funcs_pure = [ ("IsConnected() → bool", "WebSocket connection state"), ("IsListening() → bool", "Microphone capture active"), ("IsAgentSpeaking() → bool", "Agent audio playing"), ("IsPreBuffering() → bool", "Pre-buffer phase active"), ("GetConversationInfo()", "ConversationID, AgentID"), ("GetWebSocketProxy()", "Low-level WebSocket access"), ] for fname, desc in funcs_pure: tb = add_text_box(slide, Inches(0.6), Inches(y), Inches(5.5), Inches(0.28)) tf = tb.text_frame p = tf.paragraphs[0] r1 = p.add_run() r1.text = fname + " " r1.font.size = Pt(10) r1.font.color.rgb = WHITE r1.font.bold = True r1.font.name = "Consolas" r2 = p.add_run() r2.text = desc r2.font.size = Pt(9) r2.font.color.rgb = GRAY_LIGHT r2.font.name = "Arial" y += 0.3 add_placeholder_box(slide, Inches(7.0), Inches(1.6), Inches(5.8), Inches(5.0), "SCREENSHOT: Blueprint palette montrant les fonctions du ElevenLabsComponent.\n" "Ou un petit graph Blueprint montrant:\n" "StartConversation → delay → StartListening → OnAgentStoppedSpeaking → StartListening") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 13: Posture System - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "04 Posture System", "Procedural head, eye, and body tracking") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 14: PostureComponent # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("PostureComponent — Configuration", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(6.5), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "Three-layer cascade: Eyes → Head → Body", size=13, color=TEAL, bold=True) add_para(tf, "Procedural gaze tracking with automatic activation/deactivation blend.", size=11, color=GRAY_LIGHT, space_before=Pt(6)) # Tracking config add_para(tf, "", size=4, color=WHITE) add_para(tf, "Tracking", size=14, color=ORANGE, bold=True, space_before=Pt(10)) add_bullet(tf, "TargetActor — Actor to look at (auto-set by InteractionComponent)", size=10, color=WHITE) add_bullet(tf, "bActive — Enable/disable tracking", size=10, color=WHITE) add_bullet(tf, "bEnableBodyTracking — 360° continuous yaw rotation", size=10, color=WHITE) add_bullet(tf, "ActivationBlendDuration — Smooth on/off transitions (0.05–3.0s)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Angle Limits (degrees)", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "MaxHeadYaw: 40° · MaxHeadPitch: 30°", size=10, color=WHITE) add_bullet(tf, "MaxEyeHorizontal: 15° · MaxEyeVertical: 10°", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Smoothing Speeds", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "BodyInterpSpeed: 4.0 · HeadInterpSpeed: 4.0", size=10, color=WHITE) add_bullet(tf, "EyeInterpSpeed: 5.0 · ReturnToNeutralSpeed: 3.0", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Animation Compensation", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "HeadAnimationCompensation: 0.9 — override vs. additive blending", size=10, color=WHITE) add_bullet(tf, "EyeAnimationCompensation: 0.6 — same for eyes", size=10, color=WHITE) add_bullet(tf, "BodyDriftCompensation: 0.8 — counteracts spine bending", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Bone Configuration", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "HeadBoneName: \"head\"", size=10, color=WHITE) add_bullet(tf, "NeckBoneChain: multi-bone weighted distribution (e.g. neck_01=0.25, neck_02=0.35, head=0.40)", size=10, color=WHITE) add_placeholder_box(slide, Inches(7.5), Inches(1.6), Inches(5.3), Inches(5.0), "SCREENSHOT: NPC en jeu regardant le joueur avec le debug gaze activé.\n" "Montrer les lignes de debug des yeux (bDrawDebugGaze=true) " "et la rotation de la tête vers le joueur.\n" "Idéalement vue de profil pour voir la rotation du cou/tête.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 15: Facial Expression - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "05 Facial Expressions", "Emotion-driven procedural animation") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 16: FacialExpressionComponent # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("FacialExpressionComponent", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(6.0), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "Emotion-to-Animation Blending", size=13, color=TEAL, bold=True) add_para(tf, "Plays AnimSequence curves based on the agent's current emotion.\n" "Crossfades between emotions. Mouth curves excluded (lip-sync overrides).", size=11, color=GRAY_LIGHT, space_before=Pt(6)) add_para(tf, "", size=4, color=WHITE) add_para(tf, "7 Emotions × 3 Intensities", size=14, color=ORANGE, bold=True, space_before=Pt(8)) emotions = ["Neutral", "Joy", "Sadness", "Anger", "Surprise", "Fear", "Disgust"] for em in emotions: add_bullet(tf, f"{em} → Low (Normal) · Medium · High (Extreme)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Configuration", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "EmotionPoseMap — Data asset mapping emotions to AnimSequences", size=10, color=WHITE) add_bullet(tf, "EmotionBlendDuration — Crossfade duration (0.1–3.0s, default 0.5s)", size=10, color=WHITE) add_bullet(tf, "ActivationBlendDuration — On/off transition (0.05–3.0s, default 0.5s)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Driven by the \"set_emotion\" client tool", size=11, color=TEAL, bold=False, space_before=Pt(8)) add_para(tf, "Configure the agent's prompt to use emotions via the AgentConfig data asset.\n" "The emotion tool is auto-injected when bIncludeEmotionTool is enabled.", size=10, color=GRAY_LIGHT) add_placeholder_box(slide, Inches(7.0), Inches(1.6), Inches(5.8), Inches(2.3), "SCREENSHOT: EmotionPoseMap data asset dans le Content Browser.\n" "Montrer le mapping des émotions avec les AnimSequences\n" "(Joy/Normal, Joy/Medium, Joy/Extreme, etc.).") add_placeholder_box(slide, Inches(7.0), Inches(4.1), Inches(5.8), Inches(2.5), "SCREENSHOT: Comparaison côte à côte du MetaHuman avec différentes émotions.\n" "Ex: Neutral → Joy → Anger (3 captures du visage).") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 17: Lip Sync - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "06 Lip Sync", "Real-time audio-driven viseme estimation") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 18: LipSyncComponent # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("LipSyncComponent", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(6.5), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "FFT Spectral Analysis → 15 OVR Visemes → ARKit Blendshapes", size=12, color=TEAL, bold=True) add_para(tf, "Real-time frequency-based lip sync. No external model needed.", size=11, color=GRAY_LIGHT, space_before=Pt(4)) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Pipeline", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "Agent audio (PCM 16kHz) → FFT spectral analysis", size=10, color=WHITE) add_bullet(tf, "Frequency bands → 15 OVR viseme weights", size=10, color=WHITE) add_bullet(tf, "Visemes → ARKit blendshape mapping (MetaHuman compatible)", size=10, color=WHITE) add_bullet(tf, "Optional: text-driven viseme timeline (decoupled from audio chunks)", size=10, color=WHITE) add_bullet(tf, "Emotion expression blending during speech", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "15 OVR Visemes", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "sil, PP (P/B/M), FF (F/V), TH, DD (T/D), kk (K/G), CH (CH/SH/J)", size=10, color=WHITE) add_bullet(tf, "SS (S/Z), nn (N/L), RR (R), aa (A), E, ih (I), oh (O), ou (OO)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Configuration", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "LipSyncStrength: 0–3 (default 1.0) — overall amplitude", size=10, color=WHITE) add_bullet(tf, "SmoothingSpeed: 35–65 (default 50) — viseme interpolation", size=10, color=WHITE) add_bullet(tf, "EmotionExpressionBlend: 0–1 (default 0.5) — emotion bleed-through", size=10, color=WHITE) add_bullet(tf, "EnvelopeAttackMs / ReleaseMs — mouth open/close dynamics", size=10, color=WHITE) add_bullet(tf, "PoseMap — optional LipSyncPoseMap data asset for custom poses", size=10, color=WHITE) add_placeholder_box(slide, Inches(7.5), Inches(1.6), Inches(5.3), Inches(5.0), "SCREENSHOT: MetaHuman parlant avec lip sync actif.\n" "Idéalement un GIF ou une séquence de 3 captures montrant " "différentes positions de la bouche (aa, oh, FF).\n" "Ou le viewport avec le debug verbosity montrant les visemes.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 19: Interaction System - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "07 Interaction System", "Multi-agent selection, microphone routing, and discovery") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 20: InteractionComponent # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("InteractionComponent", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(6.2), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "Player-Side Agent Selection & Routing", size=13, color=TEAL, bold=True) add_para(tf, "Attach to the Player Pawn. Automatically selects the nearest visible agent\n" "and manages conversation, microphone, and posture.", size=11, color=GRAY_LIGHT, space_before=Pt(4)) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Selection", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "MaxInteractionDistance — max range in cm (default 300)", size=10, color=WHITE) add_bullet(tf, "ViewConeHalfAngle — selection cone (default 45°)", size=10, color=WHITE) add_bullet(tf, "SelectionStickyAngle — hysteresis to prevent flickering (default 60°)", size=10, color=WHITE) add_bullet(tf, "bRequireLookAt — must look at agent to select (default true)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Automation", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "bAutoStartConversation — auto-connect when agent selected (default true)", size=10, color=WHITE) add_bullet(tf, "bAutoManageListening — auto open/close mic (default true)", size=10, color=WHITE) add_bullet(tf, "bAutoManagePosture — auto-set posture target (default true)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Events", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "OnAgentSelected(Agent) — new agent in range", size=10, color=WHITE) add_bullet(tf, "OnAgentDeselected(Agent) — left range or switched agent", size=10, color=WHITE) add_bullet(tf, "OnNoAgentInRange() — no agents nearby", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Functions", size=14, color=ORANGE, bold=True, space_before=Pt(6)) add_bullet(tf, "GetSelectedAgent() — current agent", size=10, color=WHITE) add_bullet(tf, "ForceSelectAgent(Agent) — manual override", size=10, color=WHITE) add_bullet(tf, "ClearSelection() — deselect", size=10, color=WHITE) add_placeholder_box(slide, Inches(7.2), Inches(1.6), Inches(5.5), Inches(5.0), "SCREENSHOT: Vue du jeu avec le joueur s'approchant d'un NPC.\n" "Montrer le debug du InteractionComponent si possible " "(rayon de sélection, cone de vue).\n" "Ou le Details panel avec les paramètres du InteractionComponent.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 21: Agent Config - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "08 Agent Configuration", "Data asset and editor tools") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 22: AgentConfig Data Asset # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("AgentConfig Data Asset", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(6.0), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "Reusable Agent Configuration", size=13, color=TEAL, bold=True) add_para(tf, "Create via Content Browser → right-click → Miscellaneous → PS AI ConvAgent Agent Config.\n" "Assign to ElevenLabsComponent.AgentConfig to override per-component settings.", size=11, color=GRAY_LIGHT, space_before=Pt(4)) sections_cfg = [ ("Identity", [ "AgentID — ElevenLabs Agent ID", "AgentName — Display name", ]), ("Voice", [ "VoiceID — ElevenLabs voice (editor picker available)", "TTSModelID — TTS model (default: eleven_turbo_v2_5)", "Stability (0–1), SimilarityBoost (0–1), Speed (0.7–1.95)", ]), ("Language & LLM", [ "LLMModel — LLM backend (default: gemini-2.5-flash, editor dropdown)", "Language — Agent language (editor dropdown)", "bMultilingual — Dynamic language switching", ]), ("Behavior", [ "CharacterPrompt — Agent personality (multiline)", "FirstMessage — Greeting on connection", "TurnTimeout — Idle timeout (default 7s, -1 for infinite)", "bDisableIdleFollowUp — Prevent unprompted speech", ]), ("Emotion Tool", [ "bIncludeEmotionTool — Auto-inject emotion instructions in prompt", "EmotionToolPromptFragment — Customizable tool instructions", ]), ("Dynamic Variables", [ "DefaultDynamicVariables — TMap", "Use {{variable_name}} in prompts, substituted at runtime", ]), ] for section_title, items in sections_cfg: add_para(tf, "", size=3, color=WHITE) add_para(tf, section_title, size=12, color=ORANGE, bold=True, space_before=Pt(6)) for item in items: add_bullet(tf, item, size=9, color=WHITE, space_before=Pt(1)) add_placeholder_box(slide, Inches(7.0), Inches(1.6), Inches(5.8), Inches(2.6), "SCREENSHOT: AgentConfig data asset ouvert dans l'éditeur.\n" "Montrer les sections Identity, Voice (avec le picker), " "et CharacterPrompt.") add_placeholder_box(slide, Inches(7.0), Inches(4.4), Inches(5.8), Inches(2.4), "SCREENSHOT: Editor boutons Create/Update/Fetch Agent\n" "dans la custom detail view de l'AgentConfig.\n" "Montrer le dropdown Voice et LLM Model.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 23: Network - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "09 Network & Multiplayer", "Replication, LOD, and audio compression") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 24: Network Features # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Network Architecture", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(12.0), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "Client-Server Architecture", size=13, color=TEAL, bold=True) add_para(tf, "Server owns NPC actors and ElevenLabs WebSocket. Clients relay commands via InteractionComponent.", size=11, color=GRAY_LIGHT, space_before=Pt(4)) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Replicated State", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "bNetIsConversing — conversation active (all clients see it)", size=10, color=WHITE) add_bullet(tf, "NetConversatingPawn — pawn of conversating player (for posture target on remote clients)", size=10, color=WHITE) add_bullet(tf, "CurrentEmotion / CurrentEmotionIntensity — agent emotion (drives expressions on all clients)", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Audio Compression", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "Opus codec — 16x bandwidth reduction on agent audio broadcast", size=10, color=WHITE) add_bullet(tf, "Mic audio optionally Opus-compressed before relay to server", size=10, color=WHITE) add_bullet(tf, "Automatic fallback to raw PCM if Opus is unavailable", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "LOD (Level of Detail)", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "AudioLODCullDistance (default 3000 cm) — skip audio beyond this for non-speaking players", size=10, color=WHITE) add_bullet(tf, "LipSyncLODDistance (default 1500 cm) — skip lip-sync beyond this", size=10, color=WHITE) add_bullet(tf, "Speaking player always receives full quality regardless of distance", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Multicast RPCs", size=14, color=ORANGE, bold=True, space_before=Pt(8)) add_bullet(tf, "MulticastReceiveAgentAudio — broadcast agent voice to all clients", size=10, color=WHITE) add_bullet(tf, "MulticastAgentTextResponse / PartialResponse — broadcast subtitles", size=10, color=WHITE) add_bullet(tf, "MulticastAgentStartedSpeaking / StoppedSpeaking / Interrupted / StartedGenerating", size=10, color=WHITE) add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 25: Animation Nodes - Section Header # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "10 Animation Nodes", "AnimBP integration for Body and Face") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 26: Animation Nodes Detail # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Animation Blueprint Nodes", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(6.0), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "Three animation nodes for Body and Face AnimBPs", size=13, color=TEAL, bold=True) # Node 1 add_para(tf, "", size=4, color=WHITE) add_para(tf, "PS AI ConvAgent Posture", size=14, color=ORANGE, bold=True, space_before=Pt(10)) add_bullet(tf, "Place in: Body AnimBP (bApplyHeadRotation=true, bApplyEyeCurves=false)", size=10, color=WHITE) add_bullet(tf, "Also place in: Face AnimBP (bApplyHeadRotation=false, bApplyEyeCurves=true)", size=10, color=WHITE) add_bullet(tf, "Injects head/neck rotation and ARKit eye curves into the pose", size=10, color=WHITE) add_bullet(tf, "Multi-bone neck chain support with weighted distribution", size=10, color=WHITE) # Node 2 add_para(tf, "", size=4, color=WHITE) add_para(tf, "PS AI ConvAgent Facial Expression", size=14, color=ORANGE, bold=True, space_before=Pt(10)) add_bullet(tf, "Place in: Face AnimBP, BEFORE mh_arkit_mapping_pose", size=10, color=WHITE) add_bullet(tf, "Injects CTRL_expressions_* curves from emotion AnimSequences", size=10, color=WHITE) add_bullet(tf, "Mouth curves excluded (lip-sync takes priority)", size=10, color=WHITE) # Node 3 add_para(tf, "", size=4, color=WHITE) add_para(tf, "PS AI ConvAgent Lip Sync", size=14, color=ORANGE, bold=True, space_before=Pt(10)) add_bullet(tf, "Place in: Face AnimBP, AFTER Facial Expression, BEFORE mh_arkit_mapping_pose", size=10, color=WHITE) add_bullet(tf, "Injects ARKit blendshape curves (jawOpen, mouthFunnel, etc.)", size=10, color=WHITE) add_bullet(tf, "Works with MetaHuman CTRL_expressions pipeline", size=10, color=WHITE) add_para(tf, "", size=4, color=WHITE) add_para(tf, "Node Order in Face AnimBP:", size=12, color=TEAL, bold=True, space_before=Pt(10)) add_para(tf, "BasePose → Posture (eyes) → Facial Expression → Lip Sync → mh_arkit_mapping_pose", size=11, color=WHITE, space_before=Pt(4)) add_placeholder_box(slide, Inches(7.0), Inches(1.6), Inches(5.8), Inches(2.3), "SCREENSHOT: Body AnimBP AnimGraph montrant le node\n" "\"PS AI ConvAgent Posture\" connecté dans la chaîne.\n" "Montrer BasePose → Posture → Output Pose.") add_placeholder_box(slide, Inches(7.0), Inches(4.1), Inches(5.8), Inches(2.5), "SCREENSHOT: Face AnimBP AnimGraph montrant les 3 nodes:\n" "Posture (eyes only) → Facial Expression → Lip Sync → mh_arkit_mapping_pose.\n" "Montrer la chaîne complète.") add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 27: Blueprint Library # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_section_header(slide, "11 Blueprint Library", "Utility functions") # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 28: Blueprint Library Detail # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Blueprint Library", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(12.0), Inches(2.0)) tf = tb.text_frame tf.word_wrap = True set_text(tf, "UPS_AI_ConvAgent_BlueprintLibrary", size=13, color=TEAL, bold=True) add_para(tf, "", size=4, color=WHITE) add_para(tf, "SetPostProcessAnimBlueprint(SkelMeshComp, AnimBPClass)", size=12, color=ORANGE, bold=True, space_before=Pt(10)) add_para(tf, "Assign a post-process AnimBlueprint to a SkeletalMeshComponent at runtime.\n" "Per-instance override without modifying the shared asset. Pass nullptr to clear.\n" "Use this to dynamically add the Face AnimBP that contains the ConvAgent animation nodes.", size=11, color=GRAY_LIGHT, space_before=Pt(4)) add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 29: Enums Reference # ═══════════════════════════════════════════════════════════════════════════════ slide = make_content_slide("Enums Reference", "REFERENCE") tb = add_text_box(slide, Inches(0.6), Inches(1.6), Inches(5.5), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True enums_data = [ ("EPS_AI_ConvAgent_ConnectionState_ElevenLabs", [ "Disconnected", "Connecting", "Connected", "Error" ]), ("EPS_AI_ConvAgent_TurnMode_ElevenLabs", [ "Server — Server VAD (hands-free)", "Client — Push-to-talk" ]), ("EPS_AI_ConvAgent_Emotion", [ "Neutral", "Joy", "Sadness", "Anger", "Surprise", "Fear", "Disgust" ]), ("EPS_AI_ConvAgent_EmotionIntensity", [ "Low (Normal)", "Medium", "High (Extreme)" ]), ] for enum_name, values in enums_data: add_para(tf, enum_name, size=12, color=ORANGE, bold=True, space_before=Pt(10)) for v in values: add_bullet(tf, v, size=10, color=WHITE, space_before=Pt(1)) # Data Structures tb = add_text_box(slide, Inches(7.0), Inches(1.6), Inches(5.8), Inches(5.5)) tf = tb.text_frame tf.word_wrap = True structs = [ ("FPS_AI_ConvAgent_ConversationInfo_ElevenLabs", [ "ConversationID (FString)", "AgentID (FString)" ]), ("FPS_AI_ConvAgent_TranscriptSegment_ElevenLabs", [ "Text (FString)", "Speaker (FString) — \"user\" or \"agent\"", "bIsFinal (bool)" ]), ("FPS_AI_ConvAgent_ClientToolCall_ElevenLabs", [ "ToolName (FString)", "ToolCallId (FString)", "Parameters (TMap)" ]), ("FPS_AI_ConvAgent_NeckBoneEntry", [ "BoneName (FName)", "Weight (float, 0–1)" ]), ] set_text(tf, "Data Structures", size=14, color=TEAL, bold=True) for struct_name, fields in structs: add_para(tf, struct_name, size=11, color=ORANGE, bold=True, space_before=Pt(10)) for f in fields: add_bullet(tf, f, size=9, color=WHITE, space_before=Pt(1)) add_footer_bar(slide) # ═══════════════════════════════════════════════════════════════════════════════ # SLIDE 30: Closing # ═══════════════════════════════════════════════════════════════════════════════ slide = prs.slides.add_slide(prs.slide_layouts[6]) set_slide_bg(slide, BG_DARK) add_rect(slide, Inches(0), Inches(4.2), SLIDE_W, Pt(4), fill_color=ORANGE) tb = add_text_box(slide, Inches(0.8), Inches(2.5), Inches(11.7), Inches(1.0)) set_text(tb.text_frame, "PS_AI_ConvAgent", size=44, color=WHITE, bold=True, alignment=PP_ALIGN.CENTER) tb = add_text_box(slide, Inches(0.8), Inches(3.5), Inches(11.7), Inches(0.6)) set_text(tb.text_frame, "Plugin Documentation · v1.0", size=16, color=GRAY_LIGHT, alignment=PP_ALIGN.CENTER) tb = add_text_box(slide, Inches(0.8), Inches(4.8), Inches(11.7), Inches(1.0)) set_text(tb.text_frame, "ASTERION", size=20, color=ORANGE, bold=True, alignment=PP_ALIGN.CENTER) add_para(tb.text_frame, "asterion-vr.com", size=12, color=GRAY_MED, alignment=PP_ALIGN.CENTER, space_before=Pt(6)) # ── Add page numbers ────────────────────────────────────────────────────────── total = len(prs.slides) for i, slide in enumerate(prs.slides): add_page_number(slide, i + 1, total) # ── Save ────────────────────────────────────────────────────────────────────── output = r"E:\ASTERION\GIT\PS_AI_Agent\PS_AI_ConvAgent_Documentation.pptx" prs.save(output) print(f"Documentation saved to {output}") print(f"Total slides: {total}")