"""
TruGen Interactive Avatar Viewer
=================================
Connects to a TruGen AI session and displays the live video avatar in a GUI window.
Controls:
M / m — Toggle mic mute/unmute
Q / Esc — Close window and quit
"""
import os
import time
import textwrap
import numpy as np
import cv2
from dotenv import load_dotenv
from trugen import TruGenClient, TruGenRunner, TruGenState, TruGenEvent
load_dotenv()
# ── Display settings ───────────────────────────────────────────────────────────
WINDOW_TITLE = "TruGen AI — Live Avatar"
FRAME_W, FRAME_H = 1280, 720
CAPTION_MAX_CHARS = 80
CAPTION_HOLD_SECS = 5.0
FONT = cv2.FONT_HERSHEY_DUPLEX
FONT_SCALE = 0.65
FONT_THICK = 1
C_WHITE = (255, 255, 255)
C_BLACK = (0, 0, 0)
C_GREEN = (80, 200, 80)
C_RED = (60, 60, 220)
C_YELLOW = (0, 220, 220)
C_GRAY = (180, 180, 180)
C_OVERLAY = (20, 20, 20)
# ── GUI helpers ────────────────────────────────────────────────────────────────
def _overlay_rect(frame, x1, y1, x2, y2, color=C_OVERLAY, alpha=0.55):
roi = frame[y1:y2, x1:x2]
cv2.addWeighted(np.full_like(roi, color), alpha, roi, 1 - alpha, 0, roi)
frame[y1:y2, x1:x2] = roi
def _put_text(frame, text, x, y, color=C_WHITE, scale=FONT_SCALE, thick=FONT_THICK):
cv2.putText(frame, text, (x, y), FONT, scale, C_BLACK, thick + 1, cv2.LINE_AA)
cv2.putText(frame, text, (x, y), FONT, scale, color, thick, cv2.LINE_AA)
def _placeholder() -> np.ndarray:
frame = np.zeros((FRAME_H, FRAME_W, 3), dtype=np.uint8)
msg = "Waiting for video avatar…"
tw, th = cv2.getTextSize(msg, FONT, 0.9, 2)[0]
cv2.putText(frame, msg, ((FRAME_W - tw) // 2, (FRAME_H + th) // 2),
FONT, 0.9, C_GRAY, 2, cv2.LINE_AA)
return frame
# ── Session factory — user owns TruGenClient and session creation ──────────────
async def create_session():
"""Creates, connects, and returns a TruGenSession."""
client = TruGenClient(api_key=os.getenv("TRUGEN_API_KEY", ""))
session = await client.create_session(agent_id=os.getenv("TRUGEN_AGENT_ID", ""))
await session.connect()
await session.enable_audio_output() # AEC + speaker output
return session
# ── Runner setup ───────────────────────────────────────────────────────────────
runner = TruGenRunner(session_factory=create_session)
cv2.namedWindow(WINDOW_TITLE, cv2.WINDOW_NORMAL)
cv2.resizeWindow(WINDOW_TITLE, FRAME_W, FRAME_H)
@runner.on_state
def on_state(state: TruGenState) -> None:
pass
@runner.on_event(TruGenEvent.AGENT_SPEAKING_STARTED)
def on_speak_start() -> None:
pass
@runner.on_event(TruGenEvent.AGENT_SPEAKING_ENDED)
def on_speak_end() -> None:
pass
@runner.on_frame
def on_frame(frame: np.ndarray | None) -> None:
display = frame.copy() if frame is not None else _placeholder()
h, w = display.shape[:2]
# Status bar
_overlay_rect(display, 0, 0, w, 34)
state_color = C_GREEN if runner.session_state == TruGenState.CONNECTED else C_YELLOW
_put_text(display, f" {runner.session_state.value}", 10, 22,
color=state_color, scale=0.58)
mic_text = "🔇 MIC MUTED [M to unmute]" if runner.mic_muted else "🎤 MIC ON [M to mute]"
mic_color = C_RED if runner.mic_muted else C_GREEN
_put_text(display, mic_text, w - 280, 22, color=mic_color, scale=0.55)
_put_text(display, "Q / Esc — quit", w // 2 - 55, 22, color=C_GRAY, scale=0.50)
# Captions
caption, caption_ts = runner.get_caption()
if caption and time.monotonic() - caption_ts < CAPTION_HOLD_SECS:
lines = textwrap.wrap(caption, width=CAPTION_MAX_CHARS)
line_h = 28
y0 = h - (len(lines) * line_h + 16) - 10
_overlay_rect(display, 0, y0, w, h - 10)
for i, line in enumerate(lines):
tw = cv2.getTextSize(line, FONT, FONT_SCALE, FONT_THICK)[0][0]
_put_text(display, line, (w - tw) // 2, y0 + 20 + i * line_h)
# Render & key handling
cv2.imshow(WINDOW_TITLE, display)
key = cv2.waitKey(1) & 0xFF
if key in (ord("q"), ord("Q"), 27):
runner.stop()
elif key in (ord("m"), ord("M")):
runner.toggle_mute()
if cv2.getWindowProperty(WINDOW_TITLE, cv2.WND_PROP_VISIBLE) < 1:
runner.stop()
# ── Main ───────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("Controls: M = mute/unmute | Q / Esc = quit\n")
runner.run()
cv2.destroyAllWindows()