1139 lines
64 KiB
Python
1139 lines
64 KiB
Python
#!/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<FString, FString>",
|
||
"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<FString, FString>)"
|
||
]),
|
||
("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}")
|