diff --git a/PS_AI_Agent_ElevenLabs_Documentation.pptx b/PS_AI_Agent_ElevenLabs_Documentation.pptx deleted file mode 100644 index 6dc72d5..0000000 Binary files a/PS_AI_Agent_ElevenLabs_Documentation.pptx and /dev/null differ diff --git a/PS_AI_ConvAgent_Documentation.pptx b/PS_AI_ConvAgent_Documentation.pptx new file mode 100644 index 0000000..89a3a64 Binary files /dev/null and b/PS_AI_ConvAgent_Documentation.pptx differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Animations/BodyBP.uasset b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Animations/BodyBP.uasset index fc47dc2..1776c18 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Animations/BodyBP.uasset and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Animations/BodyBP.uasset differ diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Demo_Metahuman.umap b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Demo_Metahuman.umap index 9832746..2b3ca7b 100644 Binary files a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Demo_Metahuman.umap and b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Content/Demo_Metahuman.umap differ diff --git a/generate_doc_pptx.py b/generate_doc_pptx.py new file mode 100644 index 0000000..60ba5d6 --- /dev/null +++ b/generate_doc_pptx.py @@ -0,0 +1,1138 @@ +#!/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}")