from __future__ import annotations import html import os from pathlib import Path from urllib.parse import quote def resolve_vrm_path() -> Path: """Return the configured VRM path, preferring the Space's static/Aiko.vrm.""" candidates = [ Path(os.getenv("AIKO_VRM_PATH", "static/Aiko.vrm")), Path("assets/Aiko.vrm"), ] for candidate in candidates: if candidate.exists(): return candidate.resolve() return candidates[0].resolve() def gradio_file_urls(path: Path) -> list[str]: """Build Gradio file-serving URL candidates for an allowed local path.""" encoded_path = quote(path.as_posix(), safe="/:") return [f"/gradio_api/file={encoded_path}", f"/file={encoded_path}"] def gradio_file_url(path: Path) -> str: """Return the preferred Gradio file-serving URL for compatibility callers.""" return gradio_file_urls(path)[0] def avatar_html(vrm_urls: str | list[str]) -> str: """Return an iframe containing the Three/VRM viewer. Camera is framed to a half-body shot (waist-up). The iframe exposes a postMessage API for expression, viseme, ttsText, and duration control. Caption display is handled by the parent chat overlay, not this iframe. postMessage API (JSON string or object): { expression: str, intensity?: float } โ set face expression { ttsText: str, duration?: float } โ set lip-sync text { speaking: bool } โ force speaking state { viseme: str, weight?: float } โ direct viseme override """ if isinstance(vrm_urls, str): vrm_urls = [vrm_urls] srcdoc = rf"""
loading VRMโฆ