// PS_AI_ConvAgent User Documentation Generator // Palette: Midnight Executive (navy #1E2761, ice blue #CADCFC, white) const pptxgen = require("pptxgenjs"); const pres = new pptxgen(); pres.layout = "LAYOUT_WIDE"; // 13.333 x 7.5 inch // ─── Color Palette ───────────────────────────────── const NAVY = "1E2761"; const NAVY_DARK = "141B45"; const ICE = "CADCFC"; const ICE_DEEP = "7892D6"; const WHITE = "FFFFFF"; const GREY = "3A3F5C"; const LIGHT_GREY= "F5F7FC"; const ACCENT = "F9A825"; // warm gold accent // ─── Font ────────────────────────────────────────── const FONT_H = "Calibri"; const FONT_B = "Calibri"; // ─── Master slide backgrounds ────────────────────── pres.defineSlideMaster({ title: "CONTENT", background: { color: LIGHT_GREY }, objects: [ { rect: { x: 0, y: 0, w: 13.333, h: 0.35, fill: { color: NAVY } } }, { rect: { x: 0, y: 7.15, w: 13.333, h: 0.35, fill: { color: NAVY } } }, { text: { text: "PS_AI_ConvAgent — User Documentation", options: { x: 0.3, y: 7.18, w: 8, h: 0.3, fontSize: 10, color: ICE, fontFace: FONT_B } } }, { text: { text: "ASTERION", options: { x: 11.5, y: 7.18, w: 1.5, h: 0.3, fontSize: 10, color: ICE, fontFace: FONT_B, align: "right", bold: true } } }, ], }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 1 — TITLE // ═══════════════════════════════════════════════════════════════════ let s = pres.addSlide(); s.background = { color: NAVY }; s.addShape(pres.ShapeType.rect, { x: 0, y: 3.3, w: 13.333, h: 0.05, fill: { color: ACCENT } }); s.addText("PS_AI_ConvAgent", { x: 0.6, y: 2.2, w: 12, h: 1.2, fontSize: 60, bold: true, color: WHITE, fontFace: FONT_H }); s.addText("Conversational AI for MetaHumans in Unreal Engine", { x: 0.6, y: 3.5, w: 12, h: 0.6, fontSize: 22, color: ICE, fontFace: FONT_B, italic: true }); s.addText("User Documentation", { x: 0.6, y: 4.4, w: 12, h: 0.5, fontSize: 18, color: ICE_DEEP, fontFace: FONT_B }); s.addText("ElevenLabs • MetaHuman • Listen Server", { x: 0.6, y: 5.2, w: 12, h: 0.4, fontSize: 14, color: ICE_DEEP, fontFace: FONT_B }); s.addText("ASTERION • 2026", { x: 0.6, y: 6.6, w: 12, h: 0.4, fontSize: 12, color: ICE_DEEP, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 2 — OVERVIEW // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Plugin Overview", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("What it does", { x: 0.5, y: 1.7, w: 6, h: 0.4, fontSize: 18, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "Full conversational AI NPC integration for Unreal Engine", options: { bullet: true, fontSize: 14 } }, { text: "Real-time voice capture, turn-taking, TTS playback", options: { bullet: true, fontSize: 14 } }, { text: "Emotion-driven MetaHuman animations (face, body, gaze)", options: { bullet: true, fontSize: 14 } }, { text: "Multiplayer support with speaker arbitration", options: { bullet: true, fontSize: 14 } }, { text: "Tool system for LLM-triggered game actions", options: { bullet: true, fontSize: 14 } }, ], { x: 0.5, y: 2.1, w: 6, h: 2.6, color: GREY, fontFace: FONT_B, paraSpaceAfter: 8 }); s.addShape(pres.ShapeType.roundRect, { x: 7, y: 1.7, w: 5.8, h: 4.8, fill: { color: NAVY }, rectRadius: 0.1 }); s.addText("Primary Use Case", { x: 7.2, y: 1.9, w: 5.4, h: 0.4, fontSize: 16, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText("Interactive, emotionally expressive voice NPCs that listen to players, respond with contextual answers, express emotion through facial animation and body language, and trigger physical actions based on conversation flow.", { x: 7.2, y: 2.4, w: 5.4, h: 1.7, fontSize: 13, color: ICE, fontFace: FONT_B, italic: true }); s.addText("Key Differentiators", { x: 7.2, y: 4.2, w: 5.4, h: 0.4, fontSize: 16, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText([ { text: "Full-duplex Server VAD or push-to-talk", options: { bullet: true, fontSize: 12 } }, { text: "Adaptive pre-buffering & speculative TTS", options: { bullet: true, fontSize: 12 } }, { text: "Spectral lip sync + artist-driven poses", options: { bullet: true, fontSize: 12 } }, { text: "Opus codec + distance LOD for multiplayer", options: { bullet: true, fontSize: 12 } }, ], { x: 7.2, y: 4.7, w: 5.4, h: 1.7, color: ICE, fontFace: FONT_B, paraSpaceAfter: 4 }); s.addText("\"Drop components on a MetaHuman, configure a few data assets, and talk.\"", { x: 0.5, y: 5.3, w: 6, h: 0.6, fontSize: 13, color: NAVY, italic: true, fontFace: FONT_B, bold: true }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 3 — ARCHITECTURE DIAGRAM // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Architecture", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Component chain on a MetaHuman NPC", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); // Input row const boxY = 2.4; const boxH = 1.0; const boxes = [ { x: 0.5, label: "Microphone\nCapture", sub: "VAD • 16kHz" }, { x: 2.6, label: "Interaction\nComponent", sub: "Agent select\n(player side)" }, { x: 4.7, label: "ElevenLabs\nComponent", sub: "WebSocket\nTurn-taking" }, { x: 6.8, label: "Lip Sync\nComponent", sub: "Spectral\nFFT → visemes" }, { x: 8.9, label: "Facial Expr.\nComponent", sub: "Emotion\ncurves" }, { x: 11.0, label: "Body Expr.\nComponent", sub: "Gesture\nanim blend" }, ]; boxes.forEach((b, i) => { s.addShape(pres.ShapeType.roundRect, { x: b.x, y: boxY, w: 2.0, h: boxH, fill: { color: NAVY }, rectRadius: 0.1 }); s.addText(b.label, { x: b.x, y: boxY + 0.1, w: 2.0, h: 0.5, fontSize: 13, bold: true, color: WHITE, align: "center", fontFace: FONT_H }); s.addText(b.sub, { x: b.x, y: boxY + 0.58, w: 2.0, h: 0.4, fontSize: 9, color: ICE, align: "center", fontFace: FONT_B }); }); // Arrows between boxes for (let i = 0; i < boxes.length - 1; i++) { const ax = boxes[i].x + 2.0; s.addShape(pres.ShapeType.line, { x: ax, y: boxY + 0.5, w: 0.1, h: 0, line: { color: ACCENT, width: 2 } }); } // Gaze row s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 4.0, w: 2.0, h: boxH, fill: { color: ICE_DEEP }, rectRadius: 0.1 }); s.addText("Gaze\nComponent", { x: 0.5, y: 4.1, w: 2.0, h: 0.5, fontSize: 13, bold: true, color: WHITE, align: "center", fontFace: FONT_H }); s.addText("Eyes • Head\n• Body", { x: 0.5, y: 4.58, w: 2.0, h: 0.4, fontSize: 9, color: WHITE, align: "center", fontFace: FONT_B }); // AnimGraph pipeline s.addText("→ AnimGraph (Face + Body)", { x: 2.8, y: 4.2, w: 4.5, h: 0.5, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText("FacialExpression → LipSync | Gaze → BodyExpression", { x: 2.8, y: 4.65, w: 8, h: 0.4, fontSize: 12, color: GREY, italic: true, fontFace: FONT_B }); // Project settings layer s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.6, w: 12.3, h: 0.9, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 }); s.addText("PROJECT SETTINGS — API Key • Server Region • Global Context Prompt", { x: 0.7, y: 5.75, w: 12, h: 0.3, fontSize: 13, bold: true, color: NAVY, fontFace: FONT_H }); s.addText("Data Assets: AgentConfig • EmotionPoseMap • LipSyncPoseMap • BodyPoseMap • ActionSet • Tool", { x: 0.7, y: 6.1, w: 12, h: 0.3, fontSize: 11, color: GREY, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 4 — ELEVENLABS COMPONENT // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("ElevenLabs Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("The core conversation engine — WebSocket, turn-taking, audio pipeline.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Key Properties", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "AgentConfig — identity, voice, tools", options: { bullet: true, fontSize: 12 } }, { text: "TurnMode — ServerVAD / Client (push-to-talk)", options: { bullet: true, fontSize: 12 } }, { text: "MicChunkDurationMs (50–250 ms)", options: { bullet: true, fontSize: 12 } }, { text: "AudioPreBufferMs / bAdaptivePreBuffer", options: { bullet: true, fontSize: 12 } }, { text: "bAllowInterruption, bPersistentSession", options: { bullet: true, fontSize: 12 } }, { text: "SoundAttenuation (optional 3D audio)", options: { bullet: true, fontSize: 12 } }, { text: "AudioLODCullDistance / LipSyncLODDistance", options: { bullet: true, fontSize: 12 } }, ], { x: 0.5, y: 2.55, w: 6, h: 3.6, color: GREY, fontFace: FONT_B, paraSpaceAfter: 4 }); s.addText("Key Methods", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "StartConversation() / EndConversation()", options: { bullet: true, fontSize: 12 } }, { text: "StartListening() / StopListening()", options: { bullet: true, fontSize: 12 } }, { text: "SendTextMessage() — text without voice", options: { bullet: true, fontSize: 12 } }, { text: "InterruptAgent() — force stop mid-speech", options: { bullet: true, fontSize: 12 } }, { text: "ForceDisableConversation(Action) — blend to neutral", options: { bullet: true, fontSize: 12 } }, { text: "ForceEnableConversation() — re-enable", options: { bullet: true, fontSize: 12 } }, { text: "FeedExternalAudio() — external mic source", options: { bullet: true, fontSize: 12 } }, ], { x: 7, y: 2.55, w: 6, h: 3.6, color: GREY, fontFace: FONT_B, paraSpaceAfter: 4 }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.2, w: 12.3, h: 0.75, fill: { color: NAVY }, rectRadius: 0.08 }); s.addText("Major events: OnAgentConnected • OnAgentTranscript • OnAgentTextResponse • OnAgentStartedSpeaking • OnAgentEmotionChanged • OnAgentActionRequested • OnActiveSpeakerChanged • OnReadyForAction", { x: 0.7, y: 6.3, w: 12, h: 0.55, fontSize: 11, color: ICE, fontFace: FONT_B, italic: true }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 5 — MICROPHONE CAPTURE // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Microphone Capture Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Captures, resamples, and performs local voice activity detection.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Audio Pipeline", { x: 0.5, y: 2.2, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "WASAPI 48kHz stereo float32 input", options: { bullet: true, fontSize: 13 } }, { text: "Resample to 16kHz mono float", options: { bullet: true, fontSize: 13 } }, { text: "Convert to int16 LE byte stream", options: { bullet: true, fontSize: 13 } }, { text: "Delivered via multicast delegate", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 2.6, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 6 }); s.addText("Voice Activity Detection", { x: 7, y: 2.2, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "VolumeMultiplier — mic gain", options: { bullet: true, fontSize: 13 } }, { text: "VoiceActivityThreshold — RMS threshold", options: { bullet: true, fontSize: 13 } }, { text: "VoiceOnsetTime — debounce on speech start", options: { bullet: true, fontSize: 13 } }, { text: "VoiceSilenceTime — debounce on end", options: { bullet: true, fontSize: 13 } }, { text: "OnUserVoiceActivityChanged event", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 2.6, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 6 }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.4, w: 12.3, h: 1.4, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 }); s.addText("Public API", { x: 0.7, y: 5.5, w: 12, h: 0.3, fontSize: 13, bold: true, color: NAVY, fontFace: FONT_H }); s.addText("StartCapture() • StopCapture() • IsCapturing()", { x: 0.7, y: 5.85, w: 12, h: 0.3, fontSize: 13, color: GREY, fontFace: "Consolas" }); s.addText("The component auto-opens the default input device. Output is consumed by ElevenLabsComponent (or InteractionComponent, which relays via Server RPC in networked games).", { x: 0.7, y: 6.2, w: 12, h: 0.55, fontSize: 11, color: GREY, italic: true, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 6 — LIP SYNC // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Lip Sync Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Spectral analysis of agent audio → 15 OVR visemes → ARKit blendshapes.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("How it works", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "FFT on agent audio stream", options: { bullet: true, fontSize: 13 } }, { text: "Estimates 15 OVR viseme weights", options: { bullet: true, fontSize: 13 } }, { text: "Maps to ARKit blendshapes (jawOpen, mouthFunnel…)", options: { bullet: true, fontSize: 13 } }, { text: "Optional: custom PoseMap with artist-crafted poses", options: { bullet: true, fontSize: 13 } }, { text: "Thread-safe curve output for AnimGraph", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 2.55, w: 6, h: 3.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Key Properties", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "LipSyncStrength — overall intensity", options: { bullet: true, fontSize: 13 } }, { text: "SmoothingSpeed (35–65) — viseme interp", options: { bullet: true, fontSize: 13 } }, { text: "SpeechBlendDuration — fade in/out", options: { bullet: true, fontSize: 13 } }, { text: "EmotionExpressionBlend (0–1) — emotion bleed through mouth", options: { bullet: true, fontSize: 13 } }, { text: "EnvelopeAttackMs / EnvelopeReleaseMs", options: { bullet: true, fontSize: 13 } }, { text: "PoseMap — optional artist override", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 2.55, w: 6, h: 3.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.2, w: 12.3, h: 0.75, fill: { color: NAVY }, rectRadius: 0.08 }); s.addText("AnimGraph placement (Face AnimBP): Live Link → FacialExpression → LipSync → mh_arkit_mapping_pose", { x: 0.7, y: 6.3, w: 12, h: 0.55, fontSize: 12, color: ICE, fontFace: FONT_B, italic: true, bold: true }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 7 — FACIAL EXPRESSION // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Facial Expression Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Emotion-driven face animation via real-time AnimSequence sampling.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Design", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "Plays AnimSequences in real-time (looping)", options: { bullet: true, fontSize: 13 } }, { text: "Extracts emotion curves (eyes, brows, cheeks, mouth mood)", options: { bullet: true, fontSize: 13 } }, { text: "Injects into AnimGraph via FacialExpression AnimNode", options: { bullet: true, fontSize: 13 } }, { text: "Lip sync overrides mouth area during speech", options: { bullet: true, fontSize: 13 } }, { text: "3 intensity levels: Normal / Medium / Extreme", options: { bullet: true, fontSize: 13 } }, { text: "Crossfades between emotions", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 2.55, w: 6, h: 3.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Properties", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "EmotionPoseMap — data asset of AnimSequences", options: { bullet: true, fontSize: 13 } }, { text: "ActivationBlendDuration (default 0.5s)", options: { bullet: true, fontSize: 13 } }, { text: "EmotionBlendDuration — crossfade time", options: { bullet: true, fontSize: 13 } }, { text: "bActive — auto-managed by conversation", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 2.55, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addShape(pres.ShapeType.roundRect, { x: 7, y: 5.0, w: 5.8, h: 1.5, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 }); s.addText("Emotions covered", { x: 7.15, y: 5.1, w: 5.6, h: 0.3, fontSize: 13, bold: true, color: NAVY, fontFace: FONT_H }); s.addText("Joy • Sadness • Anger • Fear • Surprise • Disgust • Neutral", { x: 7.15, y: 5.45, w: 5.6, h: 0.9, fontSize: 12, color: GREY, italic: true, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 8 — BODY EXPRESSION // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Body Expression Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Upper-body gestures and posture driven by emotion + speaking state.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Animation lists per emotion", { x: 0.5, y: 2.15, w: 6.3, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); const animLists = [ { name: "Idle", desc: "Played while listening (not speaking)" }, { name: "Normal", desc: "Low-intensity speaking anims" }, { name: "Medium", desc: "Medium-intensity speaking" }, { name: "Extreme", desc: "High-intensity speaking" }, ]; animLists.forEach((a, i) => { const y = 2.7 + i * 0.75; s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 1.5, h: 0.55, fill: { color: NAVY }, rectRadius: 0.05 }); s.addText(a.name, { x: 0.5, y: y, w: 1.5, h: 0.55, fontSize: 13, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H }); s.addText(a.desc, { x: 2.15, y: y + 0.05, w: 4.6, h: 0.45, fontSize: 12, color: GREY, valign: "middle", fontFace: FONT_B }); }); s.addText("Key Properties", { x: 7.2, y: 2.15, w: 5.8, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "BodyPoseMap — emotion → anim lists", options: { bullet: true, fontSize: 13 } }, { text: "ActivationBlendDuration (0.5s)", options: { bullet: true, fontSize: 13 } }, { text: "EmotionBlendDuration (1.0s)", options: { bullet: true, fontSize: 13 } }, { text: "BlendWeight (0–1)", options: { bullet: true, fontSize: 13 } }, ], { x: 7.2, y: 2.55, w: 5.8, h: 2, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("AnimNode behaviour", { x: 7.2, y: 4.7, w: 5.8, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "Override mode, per-bone mask", options: { bullet: true, fontSize: 12 } }, { text: "Default BlendRootBone = spine_02", options: { bullet: true, fontSize: 12 } }, { text: "Excludes neck_01 (Gaze handles it)", options: { bullet: true, fontSize: 12 } }, { text: "Lower body passes through upstream", options: { bullet: true, fontSize: 12 } }, ], { x: 7.2, y: 5.1, w: 5.8, h: 2, color: GREY, fontFace: FONT_B, paraSpaceAfter: 3 }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 9 — GAZE // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Gaze Component", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Multi-layer look-at: body • head • eyes.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const layers = [ { name: "BODY", ratio: "full 360°", desc: "Actor yaw — follows target around the character" }, { name: "HEAD", ratio: "limited", desc: "Head bone yaw + pitch, clamped" }, { name: "EYES", ratio: "limited", desc: "ARKit eye curves, fine targeting" }, ]; layers.forEach((l, i) => { const y = 2.3 + i * 1.1; s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 2.2, h: 0.9, fill: { color: NAVY }, rectRadius: 0.08 }); s.addText(l.name, { x: 0.5, y: y + 0.05, w: 2.2, h: 0.4, fontSize: 18, bold: true, color: WHITE, align: "center", fontFace: FONT_H }); s.addText(l.ratio, { x: 0.5, y: y + 0.48, w: 2.2, h: 0.3, fontSize: 11, color: ACCENT, align: "center", italic: true, fontFace: FONT_B }); s.addText(l.desc, { x: 3.0, y: y + 0.15, w: 5.0, h: 0.7, fontSize: 13, color: GREY, valign: "middle", fontFace: FONT_B }); }); s.addText("Key Properties", { x: 8.5, y: 2.15, w: 4.5, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "TargetActor — who to look at", options: { bullet: true, fontSize: 12 } }, { text: "MaxHeadYaw / MaxHeadPitch", options: { bullet: true, fontSize: 12 } }, { text: "MaxEyeHorizontal / MaxEyeVertical", options: { bullet: true, fontSize: 12 } }, { text: "BodyInterpSpeed / HeadInterpSpeed / EyeInterpSpeed", options: { bullet: true, fontSize: 12 } }, { text: "HeadAnimationCompensation (0.9)", options: { bullet: true, fontSize: 12 } }, { text: "EyeAnimationCompensation (0.6)", options: { bullet: true, fontSize: 12 } }, { text: "BodyDriftCompensation (0.8)", options: { bullet: true, fontSize: 12 } }, { text: "NeckBoneChain — multi-bone distribution", options: { bullet: true, fontSize: 12 } }, ], { x: 8.5, y: 2.55, w: 4.5, h: 4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 3 }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 10 — INTERACTION COMPONENT // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Interaction Component (Player)", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 32, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Placed on the player pawn — selects NPCs, routes mic, manages gaze.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Selection Logic", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "Proximity + view cone detection", options: { bullet: true, fontSize: 13 } }, { text: "MaxInteractionDistance (300 cm default)", options: { bullet: true, fontSize: 13 } }, { text: "ViewConeHalfAngle (45° default)", options: { bullet: true, fontSize: 13 } }, { text: "SelectionStickyAngle — hysteresis", options: { bullet: true, fontSize: 13 } }, { text: "ConversationSwitchDelay — anti-flicker", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 2.55, w: 6, h: 3, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Auto-management", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "bAutoStartConversation — open WS on select", options: { bullet: true, fontSize: 13 } }, { text: "bAutoManageListening — mic open/close", options: { bullet: true, fontSize: 13 } }, { text: "bAutoManageGaze — set agent gaze to player", options: { bullet: true, fontSize: 13 } }, { text: "bAutoPassiveGaze — out-of-range passive tracking", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 2.55, w: 6, h: 2.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.8, w: 12.3, h: 1.2, fill: { color: NAVY }, rectRadius: 0.08 }); s.addText("Events: OnAgentSelected • OnAgentDeselected • OnNoAgentInRange", { x: 0.7, y: 5.95, w: 12, h: 0.3, fontSize: 13, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText("Methods: GetSelectedAgent() • ForceSelectAgent() • StartConversationWithSelectedAgent() • SendTextToSelectedAgent() • ClearSelection()", { x: 0.7, y: 6.3, w: 12, h: 0.55, fontSize: 11, color: ICE, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 11 — DATA ASSETS OVERVIEW // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Data Assets", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Reusable configuration assets for agent identity, animations, and tools.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const dataAssets = [ { name: "AgentConfig", desc: "Agent identity, voice, LLM model, tools", color: NAVY }, { name: "EmotionPoseMap", desc: "Emotion → facial AnimSequences (3 levels)", color: NAVY }, { name: "LipSyncPoseMap", desc: "15 OVR visemes → mouth poses", color: NAVY }, { name: "BodyPoseMap", desc: "Emotion → body gesture anim lists", color: NAVY }, { name: "ActionSet", desc: "Actions the agent can trigger", color: ICE_DEEP }, { name: "Tool", desc: "Custom LLM tools with parameters", color: ICE_DEEP }, ]; dataAssets.forEach((d, i) => { const col = i % 2; const row = Math.floor(i / 2); const x = 0.5 + col * 6.3; const y = 2.2 + row * 1.5; s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 1.3, fill: { color: d.color }, rectRadius: 0.1 }); s.addText(d.name, { x: x + 0.2, y: y + 0.2, w: 5.6, h: 0.4, fontSize: 18, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText(d.desc, { x: x + 0.2, y: y + 0.65, w: 5.6, h: 0.6, fontSize: 12, color: ICE, italic: true, fontFace: FONT_B }); }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 12 — AGENT CONFIG DETAILS // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("AgentConfig (ElevenLabs)", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Complete agent definition — editable in Content Browser with API buttons.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const configCats = [ { title: "Identity", items: ["AgentID", "AgentName"] }, { title: "Voice", items: ["VoiceID / VoiceName", "TTSModelID", "Stability • Similarity • Speed"] }, { title: "LLM", items: ["LLMModel (gemini-2.5-flash default)", "Language (en default)"] }, { title: "Behavior", items: ["CharacterPrompt (personality)", "FirstMessage (greeting)", "TurnTimeout, MaxTurns"] }, { title: "Latency", items: ["TurnEagerness (Eager/Normal/Patient)", "bSpeculativeTurn"] }, { title: "Tools", items: ["Tools[] array of Tool assets", "ActionSet integration"] }, ]; configCats.forEach((c, i) => { const col = i % 3; const row = Math.floor(i / 3); const x = 0.5 + col * 4.15; const y = 2.2 + row * 2.15; s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 3.95, h: 1.95, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 }); s.addText(c.title, { x: x + 0.15, y: y + 0.1, w: 3.65, h: 0.35, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: x + 0.15, y: y + 0.45, w: 0.35, h: 0.04, fill: { color: ACCENT } }); s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 11 } })), { x: x + 0.15, y: y + 0.55, w: 3.65, h: 1.3, color: GREY, fontFace: FONT_B, paraSpaceAfter: 2 }); }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.55, w: 12.3, h: 0.45, fill: { color: NAVY }, rectRadius: 0.05 }); s.addText("Editor: Voice picker • Model picker • LLM picker • Create / Update / Fetch Agent buttons", { x: 0.7, y: 6.6, w: 12, h: 0.35, fontSize: 11, color: ICE, fontFace: FONT_B, italic: true, bold: true }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 13 — POSE MAPS // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Pose Maps", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Three pose-map data assets map abstract states → AnimSequences.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); // Emotion s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 2.15, w: 4.0, h: 4.5, fill: { color: NAVY }, rectRadius: 0.1 }); s.addText("EmotionPoseMap", { x: 0.6, y: 2.3, w: 3.8, h: 0.4, fontSize: 17, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText("Emotion → facial AnimSequences\n(3 intensity levels per emotion)", { x: 0.6, y: 2.7, w: 3.8, h: 0.8, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B }); s.addText([ { text: "Joy (N / M / E)", options: { bullet: true, fontSize: 11 } }, { text: "Sadness (N / M / E)", options: { bullet: true, fontSize: 11 } }, { text: "Anger (N / M / E)", options: { bullet: true, fontSize: 11 } }, { text: "Fear (N / M / E)", options: { bullet: true, fontSize: 11 } }, { text: "Surprise (N / M / E)", options: { bullet: true, fontSize: 11 } }, { text: "Disgust (N / M / E)", options: { bullet: true, fontSize: 11 } }, { text: "Neutral (N / M / E)", options: { bullet: true, fontSize: 11 } }, ], { x: 0.7, y: 3.7, w: 3.7, h: 2.8, color: ICE, fontFace: FONT_B, paraSpaceAfter: 3 }); // LipSync s.addShape(pres.ShapeType.roundRect, { x: 4.7, y: 2.15, w: 4.0, h: 4.5, fill: { color: NAVY }, rectRadius: 0.1 }); s.addText("LipSyncPoseMap", { x: 4.8, y: 2.3, w: 3.8, h: 0.4, fontSize: 17, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText("15 OVR visemes →\nphoneme AnimSequences", { x: 4.8, y: 2.7, w: 3.8, h: 0.7, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B }); s.addText([ { text: "sil • PP • FF • TH • DD", options: { bullet: true, fontSize: 11 } }, { text: "kk • CH • SS • nn • RR", options: { bullet: true, fontSize: 11 } }, { text: "aa • E • ih • oh • ou", options: { bullet: true, fontSize: 11 } }, { text: "MetaHuman MHF_* recommended", options: { bullet: true, fontSize: 11 } }, { text: "Shareable across characters", options: { bullet: true, fontSize: 11 } }, ], { x: 4.9, y: 3.5, w: 3.7, h: 2.9, color: ICE, fontFace: FONT_B, paraSpaceAfter: 4 }); // Body s.addShape(pres.ShapeType.roundRect, { x: 8.9, y: 2.15, w: 4.0, h: 4.5, fill: { color: NAVY }, rectRadius: 0.1 }); s.addText("BodyPoseMap", { x: 9.0, y: 2.3, w: 3.8, h: 0.4, fontSize: 17, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText("Emotion → body anim lists\n(Idle / N / M / E per emotion)", { x: 9.0, y: 2.7, w: 3.8, h: 0.8, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B }); s.addText([ { text: "Idle: listening animations", options: { bullet: true, fontSize: 11 } }, { text: "Normal: light speaking", options: { bullet: true, fontSize: 11 } }, { text: "Medium: expressive speaking", options: { bullet: true, fontSize: 11 } }, { text: "Extreme: emphatic speaking", options: { bullet: true, fontSize: 11 } }, { text: "Random selection per loop", options: { bullet: true, fontSize: 11 } }, { text: "Continuous variety", options: { bullet: true, fontSize: 11 } }, ], { x: 9.0, y: 3.7, w: 3.8, h: 2.8, color: ICE, fontFace: FONT_B, paraSpaceAfter: 3 }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 14 — TOOLS & ACTIONSETS // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Tools & ActionSets", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("LLM-triggered actions that bridge conversation → gameplay.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Tool asset", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "ToolName — snake_case identifier", options: { bullet: true, fontSize: 13 } }, { text: "ToolDescription — when LLM should use it", options: { bullet: true, fontSize: 13 } }, { text: "PromptFragment — appended system prompt", options: { bullet: true, fontSize: 13 } }, { text: "Parameters[] — typed LLM arguments", options: { bullet: true, fontSize: 13 } }, { text: "ToolID auto-populated on Create", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 2.55, w: 6, h: 2.8, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Built-in Tools", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "set_emotion(emotion, intensity)", options: { bullet: true, fontSize: 13 } }, { text: "perform_action(action) — from ActionSet", options: { bullet: true, fontSize: 13 } }, { text: "Custom tools — any parameters", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 2.55, w: 6, h: 2, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 5.4, w: 12.3, h: 1.6, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 }); s.addText("Action flow", { x: 0.7, y: 5.5, w: 12, h: 0.3, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H }); s.addText("1. LLM decides to call a tool (e.g. perform_action(\"flee\"))\n2. Plugin fires OnAgentActionRequested(\"flee\")\n3. Game calls ForceDisableConversation(\"flee\") → animations blend to neutral\n4. OnReadyForAction(\"flee\") fires → play gameplay montage\n5. Game calls ForceEnableConversation() when done", { x: 0.7, y: 5.85, w: 12, h: 1.1, fontSize: 11, color: GREY, fontFace: "Consolas" }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 15 — ANIMGRAPH INTEGRATION // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("AnimGraph Integration", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Four AnimNodes auto-discover components — no manual wiring.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); // Face AnimBP chain s.addText("Face AnimBP", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); const faceChain = [ { label: "Live Link", color: ICE_DEEP }, { label: "Facial\nExpression", color: "E88B3E" }, { label: "Gaze\n(eyes only)", color: "5E7AC7" }, { label: "Lip Sync", color: "3EB086" }, { label: "mh_arkit\n_mapping", color: GREY }, ]; faceChain.forEach((n, i) => { const x = 0.5 + i * 2.55; s.addShape(pres.ShapeType.roundRect, { x: x, y: 2.65, w: 2.3, h: 1.0, fill: { color: n.color }, rectRadius: 0.1 }); s.addText(n.label, { x: x, y: 2.75, w: 2.3, h: 0.8, fontSize: 12, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H }); }); // Body AnimBP chain s.addText("Body AnimBP", { x: 0.5, y: 4.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); const bodyChain = [ { label: "Locomotion\n/ Idle", color: ICE_DEEP }, { label: "Gaze\n(head rot)", color: "5E7AC7" }, { label: "Body\nExpression", color: "B85042" }, { label: "Output Pose", color: GREY }, ]; bodyChain.forEach((n, i) => { const x = 0.5 + i * 3.2; s.addShape(pres.ShapeType.roundRect, { x: x, y: 4.65, w: 2.9, h: 1.0, fill: { color: n.color }, rectRadius: 0.1 }); s.addText(n.label, { x: x, y: 4.75, w: 2.9, h: 0.8, fontSize: 12, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: FONT_H }); }); s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: 6.15, w: 12.3, h: 0.85, fill: { color: NAVY }, rectRadius: 0.08 }); s.addText("Why this order?", { x: 0.7, y: 6.2, w: 12, h: 0.3, fontSize: 13, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText("Emotion curves set mood (brows, eyes, cheeks). Lip sync overrides mouth during speech. Gaze head rotation runs before body expression so it doesn't fight arms.", { x: 0.7, y: 6.55, w: 12, h: 0.4, fontSize: 11, color: ICE, italic: true, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 16 — PROJECT SETTINGS // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Project Settings", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Project Settings → Plugins → PS AI ConvAgent - ElevenLabs", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const settings = [ { name: "API_Key", desc: "ElevenLabs API key — required for authentication" }, { name: "ServerRegion", desc: "Global / US / EU / India" }, { name: "CustomWebSocketURL", desc: "Override WebSocket URL (advanced)" }, { name: "bVerboseLogging", desc: "Verbose WebSocket logging (development)" }, { name: "GlobalContextPrompt", desc: "Shared context injected as dynamic variables for all agents" }, ]; settings.forEach((setting, i) => { const y = 2.3 + i * 0.85; s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 3.5, h: 0.65, fill: { color: NAVY }, rectRadius: 0.05 }); s.addText(setting.name, { x: 0.5, y: y, w: 3.5, h: 0.65, fontSize: 13, bold: true, color: ACCENT, align: "center", valign: "middle", fontFace: "Consolas" }); s.addText(setting.desc, { x: 4.2, y: y + 0.05, w: 8.6, h: 0.55, fontSize: 12, color: GREY, valign: "middle", fontFace: FONT_B }); }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 17 — SETUP WORKFLOW // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Setup Workflow", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("From zero to a talking MetaHuman in 5 steps.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const steps = [ { num: "1", title: "Project Settings", desc: "Enter API_Key, pick ServerRegion." }, { num: "2", title: "Data Assets", desc: "Create AgentConfig, EmotionPoseMap, LipSyncPoseMap, BodyPoseMap." }, { num: "3", title: "NPC Components", desc: "Add ElevenLabs + Mic + LipSync + FacialExpr + BodyExpr + Gaze on the MetaHuman." }, { num: "4", title: "AnimGraphs", desc: "Wire AnimNodes in Face + Body AnimBPs. Components auto-discover." }, { num: "5", title: "Player Pawn", desc: "Add InteractionComponent. Bind selection events to UI. Play." }, ]; steps.forEach((step, i) => { const y = 2.25 + i * 0.9; s.addShape(pres.ShapeType.ellipse, { x: 0.5, y: y, w: 0.8, h: 0.8, fill: { color: ACCENT } }); s.addText(step.num, { x: 0.5, y: y, w: 0.8, h: 0.8, fontSize: 28, bold: true, color: NAVY, align: "center", valign: "middle", fontFace: FONT_H }); s.addShape(pres.ShapeType.roundRect, { x: 1.6, y: y, w: 11.2, h: 0.8, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.05 }); s.addText(step.title, { x: 1.8, y: y + 0.1, w: 3.5, h: 0.3, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H }); s.addText(step.desc, { x: 1.8, y: y + 0.4, w: 11, h: 0.35, fontSize: 11, color: GREY, italic: true, fontFace: FONT_B }); }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 18 — NETWORKING // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Networking & Multiplayer", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 34, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Listen Server model — server owns WebSocket, clients relay via RPCs.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); s.addText("Audio Pipeline", { x: 0.5, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "Client mic → Server RPC (Opus-encoded)", options: { bullet: true, fontSize: 13 } }, { text: "Server forwards to ElevenLabs WebSocket", options: { bullet: true, fontSize: 13 } }, { text: "Agent audio → Multicast RPC to all clients", options: { bullet: true, fontSize: 13 } }, { text: "Clients decode Opus → procedural sound wave", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 2.55, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Speaker Arbitration", { x: 7, y: 2.15, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "SpeakerSwitchHysteresis — min silence before switching", options: { bullet: true, fontSize: 13 } }, { text: "SpeakerIdleTimeout — clear active speaker", options: { bullet: true, fontSize: 13 } }, { text: "NetActiveSpeakerPawn replicated for gaze", options: { bullet: true, fontSize: 13 } }, { text: "Only active speaker's audio forwarded to LLM", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 2.55, w: 6, h: 2.4, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Level of Detail (LOD)", { x: 0.5, y: 5.1, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "AudioLODCullDistance — skip audio at distance", options: { bullet: true, fontSize: 13 } }, { text: "LipSyncLODDistance — skip lip sync at distance", options: { bullet: true, fontSize: 13 } }, { text: "Active speaker always full quality", options: { bullet: true, fontSize: 13 } }, ], { x: 0.5, y: 5.5, w: 6, h: 1.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); s.addText("Replicated State", { x: 7, y: 5.1, w: 6, h: 0.4, fontSize: 16, bold: true, color: NAVY, fontFace: FONT_H }); s.addText([ { text: "bNetIsConversing", options: { bullet: true, fontSize: 13 } }, { text: "NetConnectedPawns[]", options: { bullet: true, fontSize: 13 } }, { text: "CurrentEmotion + CurrentEmotionIntensity", options: { bullet: true, fontSize: 13 } }, ], { x: 7, y: 5.5, w: 6, h: 1.5, color: GREY, fontFace: FONT_B, paraSpaceAfter: 5 }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 19 — FORCE DISABLE FLOW // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("ForceDisableConversation Flow", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 30, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Clean handoff between conversation and gameplay actions.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const flow = [ { step: "LLM Tool Call", desc: "perform_action(\"flee\")" }, { step: "OnAgentActionRequested", desc: "Event fires on ElevenLabs component" }, { step: "ForceDisableConversation", desc: "Blend out face/body/gaze animations" }, { step: "OnReadyForAction", desc: "All components have reached neutral" }, { step: "Play Gameplay", desc: "Montage / behavior switch" }, { step: "ForceEnableConversation", desc: "Resume conversation when done" }, ]; flow.forEach((f, i) => { const y = 2.2 + i * 0.75; s.addShape(pres.ShapeType.roundRect, { x: 0.5, y: y, w: 4, h: 0.6, fill: { color: NAVY }, rectRadius: 0.08 }); s.addText(f.step, { x: 0.5, y: y, w: 4, h: 0.6, fontSize: 13, bold: true, color: WHITE, align: "center", valign: "middle", fontFace: "Consolas" }); s.addText("→", { x: 4.6, y: y + 0.05, w: 0.5, h: 0.5, fontSize: 22, color: ACCENT, align: "center", valign: "middle", bold: true, fontFace: FONT_H }); s.addText(f.desc, { x: 5.2, y: y + 0.05, w: 7.6, h: 0.5, fontSize: 13, color: GREY, italic: true, valign: "middle", fontFace: FONT_B }); }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 20 — EVENTS CHEAT SHEET // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide({ masterName: "CONTENT" }); s.addText("Events Cheat Sheet", { x: 0.5, y: 0.6, w: 12, h: 0.8, fontSize: 36, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: 0.5, y: 1.45, w: 0.6, h: 0.06, fill: { color: ACCENT } }); s.addText("Blueprint-assignable delegates on ElevenLabs component.", { x: 0.5, y: 1.65, w: 12, h: 0.4, fontSize: 14, color: GREY, italic: true, fontFace: FONT_B }); const eventCols = [ { title: "Connection", items: ["OnAgentConnected", "OnAgentDisconnected", "OnAgentError"] }, { title: "Dialogue", items: ["OnAgentTranscript", "OnAgentTextResponse", "OnAgentPartialResponse", "OnAgentStartedGenerating"] }, { title: "Audio", items: ["OnAgentStartedSpeaking", "OnAudioPlaybackStarted", "OnAgentStoppedSpeaking", "OnAgentInterrupted"] }, { title: "State / Actions", items: ["OnAgentEmotionChanged", "OnAgentActionRequested", "OnAgentClientToolCall", "OnActiveSpeakerChanged", "OnReadyForAction", "OnAgentResponseTimeout"] }, ]; eventCols.forEach((c, i) => { const col = i % 2; const row = Math.floor(i / 2); const x = 0.5 + col * 6.3; const y = 2.15 + row * 2.45; s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 6.0, h: 2.25, fill: { color: LIGHT_GREY }, line: { color: NAVY, width: 1 }, rectRadius: 0.08 }); s.addText(c.title, { x: x + 0.2, y: y + 0.1, w: 5.6, h: 0.3, fontSize: 14, bold: true, color: NAVY, fontFace: FONT_H }); s.addShape(pres.ShapeType.rect, { x: x + 0.2, y: y + 0.42, w: 0.35, h: 0.04, fill: { color: ACCENT } }); s.addText(c.items.map(t => ({ text: t, options: { bullet: true, fontSize: 11 } })), { x: x + 0.2, y: y + 0.52, w: 5.6, h: 1.7, color: GREY, fontFace: "Consolas", paraSpaceAfter: 2 }); }); // ═══════════════════════════════════════════════════════════════════ // SLIDE 21 — CLOSING / SUMMARY // ═══════════════════════════════════════════════════════════════════ s = pres.addSlide(); s.background = { color: NAVY }; s.addShape(pres.ShapeType.rect, { x: 0, y: 2.5, w: 13.333, h: 0.05, fill: { color: ACCENT } }); s.addText("Summary", { x: 0.6, y: 0.8, w: 12, h: 1.2, fontSize: 54, bold: true, color: WHITE, fontFace: FONT_H }); s.addText("A complete conversational AI stack for Unreal Engine.", { x: 0.6, y: 2.0, w: 12, h: 0.5, fontSize: 18, color: ICE, italic: true, fontFace: FONT_B }); const summary = [ { h: "Real-time", t: "Spectral lip sync, speculative TTS, adaptive pre-buffering" }, { h: "Expressive", t: "Emotion-driven face/body animation via AnimSequences" }, { h: "Multiplayer", t: "Listen Server + Opus codec + distance LOD + speaker arbitration" }, { h: "Data-driven", t: "Reusable PoseMaps, AgentConfigs, Tools, ActionSets" }, { h: "Bridgeable", t: "Behavior plugin gaze bridge, ForceDisable handoff for gameplay" }, { h: "BP-friendly", t: "All components blueprintable, rich event system" }, ]; summary.forEach((item, i) => { const col = i % 2; const row = Math.floor(i / 2); const x = 0.6 + col * 6.2; const y = 3.0 + row * 1.4; s.addShape(pres.ShapeType.roundRect, { x: x, y: y, w: 5.9, h: 1.2, fill: { color: NAVY_DARK }, line: { color: ACCENT, width: 1 }, rectRadius: 0.1 }); s.addText(item.h, { x: x + 0.2, y: y + 0.1, w: 5.5, h: 0.4, fontSize: 18, bold: true, color: ACCENT, fontFace: FONT_H }); s.addText(item.t, { x: x + 0.2, y: y + 0.55, w: 5.5, h: 0.6, fontSize: 12, color: ICE, italic: true, fontFace: FONT_B }); }); s.addText("ASTERION • PS_AI_ConvAgent • 2026", { x: 0.6, y: 6.9, w: 12, h: 0.4, fontSize: 12, color: ICE_DEEP, fontFace: FONT_B }); // ═══════════════════════════════════════════════════════════════════ // SAVE // ═══════════════════════════════════════════════════════════════════ pres.writeFile({ fileName: "PS_AI_ConvAgent_Documentation.pptx" }) .then(name => console.log(`Generated: ${name}`)) .catch(err => { console.error(err); process.exit(1); });