diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..49630dd --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(node -e ' *)" + ] + } +} diff --git a/G1_Controller/__init__.py b/G1_Controller/__init__.py new file mode 100644 index 0000000..bee07a0 --- /dev/null +++ b/G1_Controller/__init__.py @@ -0,0 +1,12 @@ +"""G1_Controller — manual dashboard locomotion control (N2 Phase 1). + +`LocoController` wraps the Unitree `LocoClient` + `MotionSwitcherClient` for +operator-driven walking, postures and a discrete step pad. It reuses the arm +controller's single process-wide DDS init (one `ChannelFactoryInitialize`) and +is gated behind an in-memory "Enable movement" arm flag that defaults OFF every +boot. See dashboard/routes/controller.py for the REST surface. +""" + +from Project.Sanad.G1_Controller.loco_controller import LocoController + +__all__ = ["LocoController"] diff --git a/G1_Controller/loco_controller.py b/G1_Controller/loco_controller.py new file mode 100644 index 0000000..f2746db --- /dev/null +++ b/G1_Controller/loco_controller.py @@ -0,0 +1,567 @@ +"""LocoController — manual G1 locomotion via the Unitree LocoClient (N2 Phase 1). + +Ported from the proven scripts in G1_Lootah/Controller (g1_mode_controller.py, +keyboard_controller.py, hanger_boot_sequence.py). Design notes: + +* **One DDS init per process.** The arm controller owns the single + `ChannelFactoryInitialize(0, nic)` (motion/arm_controller.py). This class + NEVER initialises DDS — it lazily builds its `LocoClient` / + `MotionSwitcherClient` only after `arm._initialized` is True. +* **Default DISARMED.** `_armed` starts False every boot and gates every WRITE + method. Reads (status / fsm / joints), E-STOP and disarm are ALWAYS allowed. +* **StopMove watchdog.** Continuous `Move(..., True)` never self-terminates, so a + daemon thread StopMoves if no `move()` refresh arrives within + `watchdog_timeout_sec`. The frontend re-sends setpoints at ~10 Hz, so a tab + close / network drop trips the watchdog within the timeout. +* **Velocity caps.** Symmetric clamp on vx/vy/vyaw — Walk 0.6, Run 1.2. +* **Allow-anytime-warn.** move/step never hard-block on FSM; if not walk-ready + they still execute but return a `warning`. +* **Sim fallback.** When `unitree_sdk2py` is absent (workstation), every write + returns `{"simulated": True}` (never raises) so the whole UI is testable. + +SDK facts confirmed from source — do not "fix" them: +* `LocoClient.Move(vx, vy, vyaw, True)` — the continuous-mode kwarg is misspelled + `continous_move` (one n); we pass it POSITIONALLY to avoid a TypeError. +* `LocoClient` has NO StandUp()/Squat() — use SetFsmId(4)/SetFsmId(2). +* FSM id / mode are read via the private RPC `bot._Call(7001/7002, "{}")`. +""" + +from __future__ import annotations + +import json +import threading +import time +from typing import Any, Optional + +from Project.Sanad.core.config_loader import section as _cfg_section +from Project.Sanad.core.logger import get_logger + +log = get_logger("loco_controller") + +# -- SDK import (optional) ----------------------------------------------------- +try: + from unitree_sdk2py.g1.loco.g1_loco_client import LocoClient + from unitree_sdk2py.comm.motion_switcher.motion_switcher_client import ( + MotionSwitcherClient, + ) + _HAS_SDK = True +except ImportError: + LocoClient = None + MotionSwitcherClient = None + _HAS_SDK = False + log.warning("Unitree SDK not available — LocoController in simulation mode") + +# LocoClient general RPC api-ids for FSM read-back (stable across SDK builds). +ROBOT_API_ID_LOCO_GET_FSM_ID = 7001 +ROBOT_API_ID_LOCO_GET_FSM_MODE = 7002 + +# G1 29-DoF joint names for indices 12-28 (0-11 legs, 12-14 waist, 15-21 left +# arm, 22-28 right arm). Used by the Diagnostics joint read-out. +JOINT_NAMES = { + 12: "WAIST_YAW", 13: "WAIST_ROLL", 14: "WAIST_PITCH", + 15: "L_SHOULDER_PITCH", 16: "L_SHOULDER_ROLL", 17: "L_SHOULDER_YAW", + 18: "L_ELBOW", 19: "L_WRIST_ROLL", 20: "L_WRIST_PITCH", 21: "L_WRIST_YAW", + 22: "R_SHOULDER_PITCH", 23: "R_SHOULDER_ROLL", 24: "R_SHOULDER_YAW", + 25: "R_ELBOW", 26: "R_WRIST_ROLL", 27: "R_WRIST_PITCH", 28: "R_WRIST_YAW", +} + +# Discrete step pad — (vx, vy, vyaw) sign per direction; magnitude is +# step_speed_frac * cap_walk (a gentle single step). +_STEP_DIRS = { + "forward": (1.0, 0.0, 0.0), + "backward": (-1.0, 0.0, 0.0), + "slide_left": (0.0, 1.0, 0.0), + "slide_right": (0.0, -1.0, 0.0), + "rotate_left": (0.0, 0.0, 1.0), + "rotate_right": (0.0, 0.0, -1.0), +} + +_POSTURES = ( + "zero_torque", "damp", "stand_up", "squat", "sit", + "low_stand", "high_stand", "lie_to_stand", +) + + +class LocoController: + """Thread-safe manual locomotion control with a simulation fallback.""" + + def __init__(self, arm=None): + self._arm = arm # shared ArmController (owns the ONE DDS init) + self._bot = None # LocoClient (lazy) + self._msc = None # MotionSwitcherClient (lazy) + self._lc_ready = False + self._lock = threading.RLock() # serialise all loco client WRITE calls + self._armed = False # in-memory MANUAL gate — OFF every boot + + self._cur_v = (0.0, 0.0, 0.0) # last commanded (vx, vy, vyaw) + self._teleop_active = False + self._last_msc_mode: Optional[str] = None + + # watchdog + self._last_move_ts = 0.0 + self._wd_thread: Optional[threading.Thread] = None + self._wd_stop = threading.Event() + self._wd_stop.set() # not running until armed + # Monotonic stop-generation counter, bumped under _lock by + # estop/stop/disarm. move()/step()/prep_mode() capture it at start and + # bail the instant it changes — so E-STOP preempts an in-flight motion + # immediately AND can never be silently "un-cancelled" by a concurrent + # command (a lock-free Event clear() could; an int compare under the + # lock cannot). + self._stop_gen = 0 + # Serializes the discrete blocking operations (step/prep_mode) so two + # can't overlap and interleave Move commands. Continuous teleop move() + # is intentionally NOT guarded by this. + self._discrete_busy = False + + cfg = _cfg_section("motion", "loco_controller") + self._cap_walk = float(cfg.get("cap_walk", 0.6)) + self._cap_run = float(cfg.get("cap_run", 1.2)) + self._lin_step = float(cfg.get("lin_step", 0.05)) + self._ang_step = float(cfg.get("ang_step", 0.2)) + self._wd_timeout = float(cfg.get("watchdog_timeout_sec", 0.5)) + self._block_window = float(cfg.get("arm_block_window_sec", 1.5)) + self._step_dur = float(cfg.get("step_duration_sec", 0.6)) + self._step_frac = float(cfg.get("step_speed_frac", 0.5)) + self._loco_timeout = float(cfg.get("loco_timeout_sec", 10.0)) + self._msc_timeout = float(cfg.get("msc_timeout_sec", 5.0)) + + # ── client lifecycle ───────────────────────────────────────────────────── + + def _ensure_client(self) -> bool: + """Lazily build LocoClient + MotionSwitcherClient. Returns readiness. + + Never initialises DDS — requires the shared arm to have already run the + single ChannelFactoryInitialize. + """ + if not _HAS_SDK: + return False + if self._lc_ready: + return True + if self._arm is None or not getattr(self._arm, "_initialized", False): + return False + with self._lock: + if self._lc_ready: + return True + try: + bot = LocoClient() + bot.SetTimeout(self._loco_timeout) + bot.Init() + msc = MotionSwitcherClient() + msc.SetTimeout(self._msc_timeout) + msc.Init() + self._bot = bot + self._msc = msc + self._lc_ready = True + log.info("LocoClient + MotionSwitcherClient ready") + except Exception as exc: + log.error("LocoClient init failed: %s", exc) + self._lc_ready = False + return self._lc_ready + + def _safe_call(self, name: str, fn, *a, **kw): + try: + return True, fn(*a, **kw) + except Exception as exc: + log.error("%s failed: %s", name, exc) + return False, None + + def _rpc_get_int(self, api_id: int): + bot = self._bot + if bot is None: + return None + try: + code, data = bot._Call(api_id, "{}") + if code == 0 and data: + return json.loads(data).get("data") + except Exception: + pass + return None + + @staticmethod + def _clamp(v: float, cap: float) -> float: + return max(-cap, min(cap, float(v))) + + # ── FSM / readiness ────────────────────────────────────────────────────── + + def fsm_id(self): + return self._rpc_get_int(ROBOT_API_ID_LOCO_GET_FSM_ID) + + def fsm_mode(self): + return self._rpc_get_int(ROBOT_API_ID_LOCO_GET_FSM_MODE) + + def _walk_ready_warning(self) -> Optional[str]: + """allow-anytime-warn: None when ready, else a human message.""" + if not self._lc_ready: + return None + fid = self.fsm_id() + fmode = self.fsm_mode() + if fid == 200 and fmode not in (None, 2): + return None + return (f"Robot not in walk-ready FSM (id={fid}, mode={fmode}). " + f"Command sent anyway.") + + # ── arm flag + watchdog ────────────────────────────────────────────────── + + def is_armed(self) -> bool: + return self._armed + + def movement_active(self) -> bool: + """True when the robot may be walking: manual armed, teleop active, OR a + move/step issued within the block window. Used as the arm's motion-block + predicate so the arm never replays while the robot is (or just was) + moving — regardless of whether the MANUAL gate or the GEMINI gate + (Phase 3 voice dispatch, which calls move/step directly) triggered it.""" + if self._armed or self._teleop_active: + return True + return (time.monotonic() - self._last_move_ts) < self._block_window + + def arm_movement(self) -> dict: + """Unlock manual control. Cancels any in-flight arm motion first so the + arm and locomotion are never active simultaneously (movement wins).""" + try: + if self._arm is not None and getattr(self._arm, "is_busy", False): + log.info("arming movement — cancelling in-flight arm motion") + self._arm.cancel() + except Exception: + log.exception("arm.cancel() on arm_movement failed") + with self._lock: + self._armed = True + self._start_watchdog() + log.info("movement ARMED") + return {"ok": True, "armed": True} + + def disarm_movement(self) -> dict: + with self._lock: + self._stop_gen += 1 # break any in-flight step/prep/move + self._armed = False + self._teleop_active = False + self._wd_stop.set() + try: + self._raw_stop() + except Exception: + log.exception("StopMove on disarm failed") + log.info("movement DISARMED") + return {"ok": True, "armed": False} + + def _start_watchdog(self): + self._wd_stop.clear() + if self._wd_thread is None or not self._wd_thread.is_alive(): + self._wd_thread = threading.Thread( + target=self._watchdog_loop, daemon=True, name="loco-watchdog") + self._wd_thread.start() + + def _watchdog_loop(self): + period = max(0.02, min(0.1, self._wd_timeout / 2.0)) + while not self._wd_stop.is_set(): + fire = False + # Read-and-decide under the lock (atomic check-then-act); the actual + # StopMove runs after release so the critical section stays tiny. + with self._lock: + if self._teleop_active and (time.monotonic() - self._last_move_ts) > self._wd_timeout: + self._teleop_active = False + fire = True + if fire: + log.warning("watchdog: teleop setpoint stale (>%.2fs) — StopMove", + self._wd_timeout) + try: + self._raw_stop() + except Exception: + log.exception("watchdog StopMove failed") + self._wd_stop.wait(period) + + def _raw_stop(self) -> bool: + """Issue StopMove if the client is up; no-op in sim. Lock-light.""" + if not self._lc_ready or self._bot is None: + return False + with self._lock: + ok, _ = self._safe_call("StopMove", self._bot.StopMove) + return ok + + # ── movement ───────────────────────────────────────────────────────────── + + def move(self, vx: float, vy: float, vyaw: float, run: bool = False) -> dict: + cap = self._cap_run if run else self._cap_walk + cvx, cvy, cvyaw = self._clamp(vx, cap), self._clamp(vy, cap), self._clamp(vyaw, cap) + capped = (cvx, cvy, cvyaw) != (float(vx), float(vy), float(vyaw)) + warning = self._walk_ready_warning() + sent = {"vx": cvx, "vy": cvy, "vyaw": cvyaw} + + with self._lock: + my_gen = self._stop_gen # capture under lock + + if not self._ensure_client(): + with self._lock: # sim: record intent for UI/watchdog + self._cur_v = (cvx, cvy, cvyaw) + self._last_move_ts = time.monotonic() + self._teleop_active = True + self._start_watchdog() + return {"ok": True, "sent": sent, "capped": capped, + "warning": warning, "simulated": True} + with self._lock: + # If an E-STOP / stop / disarm landed since we captured my_gen, do NOT + # (re)command velocity — and do NOT stamp the motion flags (so a + # cancelled tick doesn't extend the arm-block window). + if self._stop_gen != my_gen: + return {"ok": False, "cancelled": True, "sent": sent, + "capped": capped, "warning": warning, "simulated": False} + self._cur_v = (cvx, cvy, cvyaw) + self._last_move_ts = time.monotonic() + self._teleop_active = True + self._safe_call("SetBalanceMode", self._bot.SetBalanceMode, 1) + ok, _ = self._safe_call("Move", self._bot.Move, cvx, cvy, cvyaw, True) + self._start_watchdog() + return {"ok": bool(ok), "sent": sent, "capped": capped, + "warning": warning, "simulated": False} + + def stop_move(self) -> dict: + """Halt translation/rotation. Allowed even when disarmed.""" + with self._lock: + self._stop_gen += 1 + self._teleop_active = False + if not self._ensure_client(): + return {"ok": True, "simulated": True} + ok = self._raw_stop() + return {"ok": bool(ok), "simulated": False} + + def estop(self) -> dict: + """Emergency stop = StopMove only (no Damp / FSM change → keeps posture). + ALWAYS allowed, even disarmed and in sim. Bumps the stop generation so any + in-flight move()/step()/prep_mode() bails immediately (no lock wait).""" + with self._lock: + self._stop_gen += 1 + self._teleop_active = False + self._cur_v = (0.0, 0.0, 0.0) + if not self._ensure_client(): + log.warning("E-STOP (sim)") + return {"ok": True, "simulated": True} + ok = self._raw_stop() + log.warning("E-STOP — StopMove issued") + return {"ok": bool(ok), "simulated": False} + + def step(self, direction: str) -> dict: + """Discrete one-step pad: Move for step_duration then StopMove. + Blocking (~step_duration); call via asyncio.to_thread from the route. + + The sleep loop does NOT hold self._lock, so E-STOP / StopMove (which take + the lock briefly) preempt it immediately; the loop also bails the moment + the stop generation changes.""" + if direction not in _STEP_DIRS: + return {"ok": False, "reason": f"unknown direction: {direction}"} + sx, sy, syaw = _STEP_DIRS[direction] + k = self._cap_walk * self._step_frac + vx, vy, vyaw = sx * k, sy * k, syaw * k + warning = self._walk_ready_warning() + with self._lock: + if self._discrete_busy: + return {"ok": False, "dir": direction, "reason": "busy", + "warning": warning, "simulated": not self._lc_ready} + self._discrete_busy = True + my_gen = self._stop_gen + self._last_move_ts = time.monotonic() + self._teleop_active = True + self._start_watchdog() + if not self._ensure_client(): + with self._lock: + self._teleop_active = False + self._discrete_busy = False + return {"ok": True, "dir": direction, "warning": warning, "simulated": True} + try: + with self._lock: + if self._stop_gen != my_gen: # stopped before we began + return {"ok": False, "dir": direction, "cancelled": True, + "warning": warning, "simulated": False} + self._safe_call("SetBalanceMode", self._bot.SetBalanceMode, 1) + self._safe_call("Move", self._bot.Move, vx, vy, vyaw, True) + t_end = time.monotonic() + self._step_dur + while time.monotonic() < t_end: + if self._stop_gen != my_gen: + break + with self._lock: + self._last_move_ts = time.monotonic() # keep watchdog fed + time.sleep(0.05) + finally: + with self._lock: + self._safe_call("StopMove", self._bot.StopMove) + self._teleop_active = False + self._discrete_busy = False + return {"ok": True, "dir": direction, "warning": warning, "simulated": False} + + # ── postures / modes ───────────────────────────────────────────────────── + + def prep_mode(self) -> dict: + """PREP — StopMove → Damp → StandUp(FSM4) → height ramp → BalanceStand(0). + Exact order from g1_mode_controller.prep_mode, minus the blocking input(). + Blocking (~1s); call via asyncio.to_thread.""" + if not self._ensure_client(): + return {"ok": True, "mode": "prep", "simulated": True} + with self._lock: + if self._discrete_busy: + return {"ok": False, "mode": "prep", "reason": "busy", "simulated": False} + self._discrete_busy = True + my_gen = self._stop_gen + self._safe_call("StopMove", self._bot.StopMove) + self._safe_call("Damp", self._bot.Damp) + self._safe_call("SetFsmId(4)", self._bot.SetFsmId, 4) + try: + # Height ramp OUTSIDE the lock so E-STOP can preempt at any time. + h = 0.02 + while h <= 0.5 + 1e-9: + if self._stop_gen != my_gen: + log.warning("PREP cancelled (E-STOP)") + return {"ok": False, "mode": "prep", "cancelled": True, "simulated": False} + with self._lock: + self._safe_call("SetStandHeight", self._bot.SetStandHeight, round(h, 3)) + time.sleep(0.03) + h += 0.02 + with self._lock: + self._safe_call("BalanceStand", self._bot.BalanceStand, 0) + self._safe_call("SetStandHeight", self._bot.SetStandHeight, 0.22) + finally: + with self._lock: + self._discrete_busy = False + log.info("PREP complete") + return {"ok": True, "mode": "prep", "simulated": False} + + def ready_start_mode(self) -> dict: + """READY = PREP then Start (FSM 200 / balance engaged).""" + self.prep_mode() + if not self._ensure_client(): + return {"ok": True, "mode": "ready", "simulated": True} + with self._lock: + if hasattr(self._bot, "Start"): + ok, _ = self._safe_call("Start", self._bot.Start) + else: + ok, _ = self._safe_call("SetFsmId(200)", self._bot.SetFsmId, 200) + log.info("READY/START complete") + return {"ok": bool(ok), "mode": "ready", "simulated": False} + + def posture(self, name: str) -> dict: + if name not in _POSTURES: + return {"ok": False, "reason": f"unknown posture: {name}"} + if not self._ensure_client(): + return {"ok": True, "posture": name, "simulated": True} + bot = self._bot + with self._lock: + if name == "zero_torque": + ok, _ = self._safe_call("ZeroTorque", bot.ZeroTorque) + elif name == "damp": + ok, _ = self._safe_call("Damp", bot.Damp) + elif name == "stand_up": + ok, _ = self._safe_call("SetFsmId(4)", bot.SetFsmId, 4) + elif name == "squat": + ok, _ = self._safe_call("SetFsmId(2)", bot.SetFsmId, 2) + elif name == "sit": + ok, _ = self._safe_call("Sit", bot.Sit) + elif name == "low_stand": + ok, _ = self._safe_call("LowStand", bot.LowStand) + elif name == "high_stand": + ok, _ = self._safe_call("HighStand", bot.HighStand) + elif name == "lie_to_stand": + if hasattr(bot, "Lie2StandUp"): + ok, _ = self._safe_call("Lie2StandUp", bot.Lie2StandUp) + else: + ok, _ = self._safe_call("SetFsmId(702)", bot.SetFsmId, 702) + else: # unreachable (guarded above) + ok = False + return {"ok": bool(ok), "posture": name, "simulated": False} + + def set_balance_mode(self, mode: int) -> dict: + if not self._ensure_client(): + return {"ok": True, "balance_mode": int(mode), "simulated": True} + with self._lock: + ok, _ = self._safe_call("SetBalanceMode", self._bot.SetBalanceMode, int(mode)) + return {"ok": bool(ok), "balance_mode": int(mode), "simulated": False} + + def set_stand_height(self, h: float) -> dict: + if not self._ensure_client(): + return {"ok": True, "height": float(h), "simulated": True} + with self._lock: + ok, _ = self._safe_call("SetStandHeight", self._bot.SetStandHeight, float(h)) + return {"ok": bool(ok), "height": float(h), "simulated": False} + + # ── MotionSwitcher ─────────────────────────────────────────────────────── + + def msc_check(self) -> dict: + if not self._ensure_client() or self._msc is None: + return {"mode_name": None, "simulated": not self._lc_ready} + try: + ret = self._msc.CheckMode() + name = None + if isinstance(ret, tuple) and len(ret) >= 2 and isinstance(ret[1], dict): + name = ret[1].get("name") + elif isinstance(ret, dict): + name = ret.get("name") + self._last_msc_mode = name + return {"mode_name": name} + except Exception as exc: + log.error("msc_check failed: %s", exc) + return {"mode_name": None} + + def msc_select_ai(self) -> dict: + if not self._ensure_client() or self._msc is None: + return {"ok": True, "simulated": True} + with self._lock: + ok, _ = self._safe_call("SelectMode(ai)", self._msc.SelectMode, "ai") + return {"ok": bool(ok), "simulated": False} + + def msc_release(self) -> dict: + if not self._ensure_client() or self._msc is None: + return {"ok": True, "simulated": True} + with self._lock: + ok, _ = self._safe_call("ReleaseMode", self._msc.ReleaseMode) + return {"ok": bool(ok), "simulated": False} + + def reconnect(self) -> dict: + """Drop and rebuild Loco + MSC clients (does NOT re-init the DDS factory).""" + with self._lock: + self._bot = None + self._msc = None + self._lc_ready = False + ok = self._ensure_client() + return {"ok": bool(ok), "lc_ready": self._lc_ready} + + # ── reads ──────────────────────────────────────────────────────────────── + + def joints(self) -> dict: + q: list = [] + try: + if self._arm is not None: + q = self._arm.get_current_q() + except Exception: + q = [] + out = [] + for idx in range(12, 29): + val = q[idx] if idx < len(q) else 0.0 + out.append({"idx": idx, "name": JOINT_NAMES.get(idx, f"motor_{idx}"), + "q": float(val)}) + return {"joints": out} + + def status(self) -> dict: + # Polling /status lazily brings up the client once arm DDS is ready. + self._ensure_client() + fid = self.fsm_id() if self._lc_ready else None + fmode = self.fsm_mode() if self._lc_ready else None + walk_ready = bool(self._lc_ready and fid == 200 and fmode not in (None, 2)) + return { + "sdk_available": _HAS_SDK, + "lc_ready": self._lc_ready, + "armed": self._armed, + "fsm_id": fid, + "fsm_mode": fmode, + "walk_ready": walk_ready, + "msc_mode": self._last_msc_mode, + "teleop_active": self._teleop_active, + "last_velocity": {"vx": self._cur_v[0], "vy": self._cur_v[1], "vyaw": self._cur_v[2]}, + "caps": {"walk": self._cap_walk, "run": self._cap_run}, + "arm_initialized": bool(self._arm is not None and getattr(self._arm, "_initialized", False)), + } + + # ── shutdown helper ────────────────────────────────────────────────────── + + def shutdown(self): + """Best-effort StopMove + disarm for process shutdown.""" + try: + self.estop() + finally: + self.disarm_movement() diff --git a/config/core_config.json b/config/core_config.json index be31af4..177616b 100644 --- a/config/core_config.json +++ b/config/core_config.json @@ -41,7 +41,7 @@ "model_ws_uri": "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent", "voice_name": "Charon", "ws_timeout_sec": 30, - "default_system_prompt": "You are Sanad (Bousandah), a wise and friendly Emirati assistant. Speak strictly in the UAE dialect (Khaleeji). Be helpful, concise, and use local greetings like 'Marhaba' and 'Ya Khoy'." + "default_system_prompt": "You are Bousandah, a wise and friendly Emirati assistant. Speak strictly in the UAE dialect (Khaleeji). Be helpful, concise, and use local greetings like 'Marhaba' and 'Ya Khoy'." }, "g1_hardware": { @@ -53,7 +53,7 @@ "script_files": { "_comment": "Filenames (under scripts/) used across voice + dashboard", - "persona": "sanad_v2", + "persona": "sanad_script.txt", "rules": "sanad_rule.txt", "arm_phrases": "sanad_arm.txt" }, diff --git a/config/motion_config.json b/config/motion_config.json index bcc3336..9070df1 100644 --- a/config/motion_config.json +++ b/config/motion_config.json @@ -13,6 +13,20 @@ "jsonl_id_start": 100 }, + "loco_controller": { + "_comment": "G1_Controller/loco_controller.py — manual locomotion. NIC is shared from the arm's DDS init (config core.dds / SANAD_DDS_INTERFACE), not set here.", + "cap_walk": 0.6, + "cap_run": 1.2, + "lin_step": 0.05, + "ang_step": 0.2, + "watchdog_timeout_sec": 0.5, + "arm_block_window_sec": 1.5, + "step_duration_sec": 0.6, + "step_speed_frac": 0.5, + "loco_timeout_sec": 10.0, + "msc_timeout_sec": 5.0 + }, + "macro_player": { "_comment": "motion/macro_player.py — JSONL playback", "ramp_in_steps": 60, diff --git a/dashboard/app.py b/dashboard/app.py index 5de8495..8261aab 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -51,9 +51,12 @@ _REST_ROUTES: list[tuple[str, str, str]] = [ ("live_subprocess", "/api/live-subprocess", "live-subprocess"), ("typed_replay", "/api/typed-replay", "typed-replay"), ("recognition", "/api/recognition", "recognition"), + ("zones", "/api/zones", "zones"), + ("temp_monitor", "/api/temp", "temperature"), + ("controller", "/api/controller", "controller"), ] -_WS_ROUTES: list[str] = ["log_stream"] +_WS_ROUTES: list[str] = ["log_stream", "motor_temps", "terminal"] _loaded_routes: list[str] = [] _failed_routes: dict[str, str] = {} diff --git a/dashboard/routes/audio_control.py b/dashboard/routes/audio_control.py index 3fc21a2..6ed28f1 100644 --- a/dashboard/routes/audio_control.py +++ b/dashboard/routes/audio_control.py @@ -473,3 +473,450 @@ async def apply_audio(): pass return result return await asyncio.to_thread(_do) + + +# ─────────────────────── Reset endpoints (Pulse + USB) ─────────────────────── +# +# Two distinct recovery paths for the dashboard's audio panel: +# +# POST /api/audio/reset — SOFT: restart pulseaudio / pipewire-pulse. +# Fixes Pulse-side state (stuck profile, lost default sink, crashed +# module). Cannot recover a kernel-side missing USB capture descriptor +# — snd-usb-audio parses those at probe time and Pulse can't influence +# that. Use for "devices look weird" failures. +# +# POST /api/audio/usb-reset — HARD: unbind+rebind snd-usb-audio scoped +# to the Anker VID:PID. Forces snd-usb-audio to re-parse UAC1 +# descriptors → input profile reappears even after the firmware/USB +# handshake dropped it. Use for "Anker mic missing from pactl" — the +# symptom soft-reset cannot fix. +# +# Both gate with module-level locks (no concurrent reset), refuse while Live +# Gemini is running or a record is mid-playback, and return structured +# before/after diagnostics so the dashboard can show meaningful toasts. + +_RESET_LOCK = threading.Lock() +_USB_RESET_LOCK = threading.Lock() + +# Anker PowerConf A3321 — used both for VID:PID matching in sysfs and for +# logging. Change here if you add support for a different USB conference +# device (Hollyland etc). +_USB_RESET_TARGETS = ( + {"vid": "291a", "pid": "3301", "label": "Anker PowerConf"}, +) + + +def _refuse_if_busy() -> None: + """Raise HTTPException(409) if Live Gemini is active or a record is playing. + + Used by both reset endpoints — a userspace audio restart mid-stream + leaves the active session in a broken state (PortAudio handle pointing + at a dead Pulse, in-flight write() raises, etc.). Cheaper to refuse + than to recover. + """ + try: + from Project.Sanad.main import live_sub + except Exception: + live_sub = None + if live_sub is not None: + try: + st = live_sub.status() or {} + except Exception: + st = {} + state = (st.get("state") or "").lower() + if st.get("running") or state not in ("", "stopped", "error"): + raise HTTPException( + 409, f"Stop Live Gemini before resetting audio (state={state or '?'}).", + ) + + try: + from Project.Sanad.main import audio_mgr + except Exception: + audio_mgr = None + if audio_mgr is not None and hasattr(audio_mgr, "playback_status"): + try: + ps = audio_mgr.playback_status() or {} + if ps.get("playing"): + raise HTTPException( + 409, "Stop the active playback before resetting audio.", + ) + except HTTPException: + raise + except Exception: + pass + + +def _detect_pa_flavour() -> str: + """Return 'pipewire' if pipewire-pulse is the active daemon, else 'pulse'.""" + try: + r = subprocess.run( + ["pgrep", "-x", "pipewire-pulse"], + check=False, capture_output=True, text=True, timeout=1.0, + ) + if r.returncode == 0 and (r.stdout or "").strip(): + return "pipewire" + except (FileNotFoundError, subprocess.SubprocessError): + pass + return "pulse" + + +def _kill_audio_daemon(flavour: str) -> dict: + """Issue the restart command for the detected daemon. Non-zero exit is a + soft warning (some installs return 1 when there's no daemon to kill).""" + if flavour == "pipewire": + cmd = ["systemctl", "--user", "restart", "pipewire-pulse.service"] + else: + cmd = ["pulseaudio", "-k"] + try: + r = subprocess.run(cmd, check=False, capture_output=True, + text=True, timeout=5.0) + info = {"cmd": " ".join(cmd), "returncode": r.returncode, + "stderr": (r.stderr or "").strip()[:300]} + if r.returncode != 0: + log.warning("audio reset: %s exited %d (%s)", + cmd[0], r.returncode, info["stderr"]) + return info + except FileNotFoundError as exc: + return {"cmd": " ".join(cmd), "returncode": -1, + "stderr": f"binary missing: {exc}"} + except subprocess.TimeoutExpired: + return {"cmd": " ".join(cmd), "returncode": -1, + "stderr": "timeout (>5s)"} + + +def _wait_for_pactl(deadline_s: float = 5.0, interval_s: float = 0.2) -> bool: + """Poll `pactl info` until it returns 0 or the deadline expires.""" + import time as _time + end = _time.monotonic() + deadline_s + while _time.monotonic() < end: + if ad.pactl_available(): + return True + _time.sleep(interval_s) + return False + + +@router.post("/reset") +async def reset_audio_subsystem(): + """SOFT reset — restart pulseaudio/pipewire-pulse and re-resolve devices. + + Use when devices look stuck, pactl is unavailable, or the wrong sink + is being selected. **Does NOT recover a kernel-side missing USB capture + descriptor** — for that symptom use /api/audio/usb-reset. + """ + if os.geteuid() == 0: + raise HTTPException( + 403, "Refusing to reset audio as root — Sanad must run as the " + "unitree user so the per-user PulseAudio session is reachable.", + ) + if not _RESET_LOCK.acquire(blocking=False): + raise HTTPException(429, "Reset already in progress.") + try: + _refuse_if_busy() + log.info( + "audio reset requested (uid=%s PULSE_RUNTIME_PATH=%s XDG_RUNTIME_DIR=%s)", + os.geteuid(), + os.environ.get("PULSE_RUNTIME_PATH") or "-", + os.environ.get("XDG_RUNTIME_DIR") or "-", + ) + try: + from Project.Sanad.main import audio_mgr + except Exception: + audio_mgr = None + + def _do() -> dict: + before = {"pactl_available": ad.pactl_available(), + "selection": ad.current_selection()} + + # Quiesce AudioManager so the next play_wav rebinds cleanly. + pya_closed = False + if audio_mgr is not None: + play_lock = getattr(audio_mgr, "play_lock", None) + acquired = False + if play_lock is not None: + acquired = play_lock.acquire(timeout=2.0) + try: + try: + audio_mgr.close() + pya_closed = True + except Exception as exc: + log.warning("audio reset: audio_mgr.close failed: %s", exc) + finally: + if acquired and play_lock is not None: + play_lock.release() + + flavour = _detect_pa_flavour() + kill_info = _kill_audio_daemon(flavour) + came_back = _wait_for_pactl(deadline_s=5.0) + if not came_back and flavour == "pulse": + # autospawn may be disabled — try an explicit start. + try: + subprocess.run(["pulseaudio", "--start"], check=False, + capture_output=True, text=True, timeout=3.0) + except (FileNotFoundError, subprocess.SubprocessError) as exc: + log.warning("audio reset: pulseaudio --start failed: %s", exc) + came_back = _wait_for_pactl(deadline_s=2.0) + if not came_back: + raise HTTPException(500, { + "error": "audio daemon did not return within ~7s", + "flavour": flavour, "kill": kill_info, + }) + + apply_result: dict = {} + try: + apply_result = ad.apply_current_selection() or {} + except Exception as exc: + log.warning("audio reset: apply_current_selection failed: %s", exc) + apply_result = {"error": str(exc)} + + if audio_mgr is not None: + try: + import pyaudio + audio_mgr.pya = pyaudio.PyAudio() + audio_mgr.refresh_devices() + except Exception as exc: + log.error("audio reset: PyAudio re-init failed: %s", exc) + raise HTTPException( + 500, f"PortAudio re-init failed after daemon restart: {exc}") + + after_sel = ad.current_selection() or {} + detected = ad.detect_plugged_profiles() or [] + after = { + "pactl_available": ad.pactl_available(), + "selection": after_sel, + "detected_profiles": [p.get("profile", {}).get("id") for p in detected], + } + return { + "ok": True, "best_effort": True, "flavour": flavour, + "kill": kill_info, "pya_reinitialized": pya_closed, + "apply_result": apply_result, + "input_recovered": bool(after_sel.get("source")), + "output_recovered": bool(after_sel.get("sink")), + "before": before, "after": after, + "hint": ("Soft reset only fixes Pulse-side state. If " + "input_recovered is False, try POST /api/audio/usb-reset " + "or physically replug the dongle."), + } + return await asyncio.to_thread(_do) + finally: + _RESET_LOCK.release() + + +def _find_usb_devices_by_vid_pid(vid: str, pid: str) -> list[str]: + """Return sysfs bus-id strings (e.g. '1-3') for every USB device whose + idVendor/idProduct match. Empty list when nothing matches. + + We read /sys/bus/usb/devices/* — every USB *device* (not interface) has + idVendor/idProduct files. Interfaces (paths with a colon, e.g. '1-3:1.1') + do not, so they're naturally skipped. + """ + import glob + hits: list[str] = [] + for path in glob.glob("/sys/bus/usb/devices/*"): + name = os.path.basename(path) + if ":" in name: + continue + try: + with open(os.path.join(path, "idVendor")) as f: + v = f.read().strip().lower() + with open(os.path.join(path, "idProduct")) as f: + p = f.read().strip().lower() + except OSError: + continue + if v == vid.lower() and p == pid.lower(): + hits.append(name) + return hits + + +def _snd_usb_interfaces_for_device(bus_id: str) -> list[str]: + """For USB device `bus_id` (e.g. '1-3'), return all interface names that + are currently bound to the snd-usb-audio driver (e.g. ['1-3:1.0']). + + Used so we unbind ONLY the audio interfaces and don't touch HID / HUB + interfaces on the same composite device. + """ + import glob + bound: list[str] = [] + base = f"/sys/bus/usb/devices/{bus_id}" + for iface in glob.glob(f"{base}/{bus_id}:*"): + driver_link = os.path.join(iface, "driver") + if not os.path.islink(driver_link): + continue + try: + driver = os.path.basename(os.readlink(driver_link)) + except OSError: + continue + if driver == "snd-usb-audio": + bound.append(os.path.basename(iface)) + return bound + + +def _write_sysfs(path: str, value: str) -> tuple[bool, str]: + """Write `value` to a sysfs file. Returns (success, error_message). + + Writes to /sys/bus/usb/drivers/snd-usb-audio/{bind,unbind} usually + require root. If permission denied, the caller should fall back to + invoking shell_scripts/reset_anker_usb.sh via sudo (one-time sudoers + setup documented in that script's header). + """ + try: + with open(path, "w") as f: + f.write(value) + return True, "" + except PermissionError as exc: + return False, f"permission denied: {path} ({exc})" + except OSError as exc: + return False, f"write failed: {path} ({exc})" + + +@router.post("/usb-reset") +async def usb_reset_anker(): + """HARD reset — unbind+rebind snd-usb-audio for the Anker (VID:PID + 291a:3301). Forces the kernel to re-parse the USB Audio Class + descriptors, which is the only way to recover a missing capture profile + on this Jetson without a physical replug. + + Tries two paths: + 1. Direct sysfs write (no sudo) — works if a udev rule has set + `audio` group ownership / world-write on the snd-usb-audio bind + files, or if Sanad runs as root (it shouldn't). + 2. Fallback to `sudo shell_scripts/reset_anker_usb.sh` — works after + a one-time sudoers entry; see that script's header for setup. + + Refuses while Live Gemini or a record playback is in flight (same + guard as the soft reset). + """ + if not _USB_RESET_LOCK.acquire(blocking=False): + raise HTTPException(429, "USB reset already in progress.") + try: + _refuse_if_busy() + + # Find candidate Anker USB devices currently enumerated. + candidates: list[dict] = [] + for tgt in _USB_RESET_TARGETS: + for bus_id in _find_usb_devices_by_vid_pid(tgt["vid"], tgt["pid"]): + candidates.append({"bus_id": bus_id, **tgt}) + if not candidates: + wanted = ", ".join( + "{}:{}".format(t["vid"], t["pid"]) for t in _USB_RESET_TARGETS + ) + raise HTTPException( + 404, + f"No matching USB device found (looked for {wanted}). " + "Plug the Anker dongle and try again.", + ) + + log.info("usb reset: candidates=%s", candidates) + + def _do() -> dict: + before_detected = [ + p.get("profile", {}).get("id") + for p in (ad.detect_plugged_profiles() or []) + ] + results: list[dict] = [] + for cand in candidates: + bus = cand["bus_id"] + ifaces = _snd_usb_interfaces_for_device(bus) + attempt = {"bus_id": bus, "label": cand["label"], + "snd_interfaces": ifaces, "method": None, + "ok": False, "error": ""} + if not ifaces: + attempt["error"] = ("no snd-usb-audio interfaces bound " + "to this device — already unbound or " + "kernel didn't claim it") + results.append(attempt) + continue + + # ─── Path 1: direct sysfs write ─── + unbind_path = "/sys/bus/usb/drivers/snd-usb-audio/unbind" + bind_path = "/sys/bus/usb/drivers/snd-usb-audio/bind" + direct_ok = True + direct_err = "" + for iface in ifaces: + ok, err = _write_sysfs(unbind_path, iface) + if not ok: + direct_ok = False + direct_err = err + break + if direct_ok: + import time as _time + _time.sleep(0.5) + for iface in ifaces: + ok, err = _write_sysfs(bind_path, iface) + if not ok: + direct_ok = False + direct_err = err + break + if direct_ok: + attempt.update({"method": "direct-sysfs", "ok": True}) + results.append(attempt) + continue + + # ─── Path 2: sudo helper script ─── + from pathlib import Path as _Path + helper = (_Path(__file__).resolve().parent.parent.parent + / "shell_scripts" / "reset_anker_usb.sh") + if not helper.exists(): + attempt.update({"method": "direct-sysfs", + "error": f"{direct_err}; helper not present " + f"at {helper}"}) + results.append(attempt) + continue + try: + r = subprocess.run( + ["sudo", "-n", str(helper), bus], + check=False, capture_output=True, text=True, timeout=10.0, + ) + attempt["method"] = "sudo-helper" + if r.returncode == 0: + attempt["ok"] = True + else: + attempt["error"] = ( + f"sudo helper exited {r.returncode}: " + f"{(r.stderr or r.stdout or '').strip()[:300]}" + ) + except subprocess.TimeoutExpired: + attempt["error"] = "sudo helper timed out (>10s)" + except FileNotFoundError as exc: + attempt["error"] = f"sudo not available: {exc}" + results.append(attempt) + + # Settle, then re-detect + import time as _time + _time.sleep(1.0) + try: + ad.apply_current_selection() + except Exception: + pass + try: + from Project.Sanad.main import audio_mgr + if audio_mgr is not None and hasattr(audio_mgr, "refresh_devices"): + audio_mgr.refresh_devices() + except Exception: + pass + + after_detected = [ + p.get("profile", {}).get("id") + for p in (ad.detect_plugged_profiles() or []) + ] + any_ok = any(r.get("ok") for r in results) + mic_now = any( + "anker" in (p.get("profile", {}).get("id") or "").lower() + for p in (ad.detect_plugged_profiles() or []) + ) + + return { + "ok": any_ok, + "candidates": results, + "before_detected_profiles": before_detected, + "after_detected_profiles": after_detected, + "input_recovered": mic_now, + "hint": ( + "If ok is False, the unbind/rebind path needs sudo. " + "Run `bash shell_scripts/reset_anker_usb.sh --setup-sudoers` " + "once on the robot to install the sudoers entry, then retry." + ) if not any_ok else None, + } + + return await asyncio.to_thread(_do) + finally: + _USB_RESET_LOCK.release() diff --git a/dashboard/routes/controller.py b/dashboard/routes/controller.py new file mode 100644 index 0000000..f0b998d --- /dev/null +++ b/dashboard/routes/controller.py @@ -0,0 +1,295 @@ +"""Controller tab — manual dashboard locomotion control (N2 Phase 1/2). + +Routes live under /api/controller. All WRITE actions (move / step / postures / +modes / MotionSwitcher) require the in-memory "Enable movement" arm flag and +return 409 when disarmed. Reads (/status, /joints, /msc, /status/summary), +E-STOP and the arm toggle are ALWAYS available. + +`/status/summary` is the aggregate the dashboard polls for the global subsystem +status strip (Camera / Face / Place / Movement). It is kept under /api/controller +(final path /api/controller/status/summary) so no second router is needed; note +/api/status (no /summary) is already used by the SPA, so the suffix matters. +""" + +from __future__ import annotations + +import asyncio + +from fastapi import APIRouter, HTTPException, Query +from pydantic import BaseModel + +from Project.Sanad.config import BASE_DIR +from Project.Sanad.core.logger import get_logger +from Project.Sanad.vision import recognition_state + +log = get_logger("controller_routes") + +router = APIRouter() + +STATE_PATH = BASE_DIR / "data" / ".recognition_state.json" + + +# ── lazy subsystem accessors ──────────────────────────────── + +def _get_loco(): + try: + from Project.Sanad.main import loco_controller # type: ignore + return loco_controller + except Exception: + return None + + +def _get_camera(): + try: + from Project.Sanad.main import camera # type: ignore + return camera + except Exception: + return None + + +def _get_live_sub(): + try: + from Project.Sanad.main import live_sub # type: ignore + return live_sub + except Exception: + return None + + +def _get_dispatch(): + try: + from Project.Sanad.main import movement_dispatch # type: ignore + return movement_dispatch + except Exception: + return None + + +def _require_loco(): + lc = _get_loco() + if lc is None: + raise HTTPException(503, "Locomotion controller subsystem unavailable.") + return lc + + +def _require_armed(lc): + if not lc.is_armed(): + raise HTTPException(409, "Movement is disarmed. Enable movement first.") + + +# ── reads ─────────────────────────────────────────────────── + +@router.get("/status") +async def get_status(): + lc = _require_loco() + return await asyncio.to_thread(lc.status) + + +@router.get("/joints") +async def get_joints(): + lc = _require_loco() + return await asyncio.to_thread(lc.joints) + + +@router.get("/msc") +async def get_msc(): + lc = _require_loco() + return await asyncio.to_thread(lc.msc_check) + + +# ── arm flag / E-STOP (always available) ──────────────────── + +@router.post("/arm") +async def set_arm(on: bool = Query(...)): + lc = _require_loco() + res = await asyncio.to_thread(lc.arm_movement if on else lc.disarm_movement) + return res + + +@router.post("/gemini-movement") +async def set_gemini_movement(on: bool = Query(...)): + """Enable / disable Gemini voice-driven locomotion (N2 Phase 3 gate). + + Writes recognition_state.movement_enabled — SEPARATE from the manual arm + flag. The Gemini child announces the toggle (spoken), and the parent + MovementDispatcher starts/stops acting on confirmation phrases. Default OFF. + """ + st = await asyncio.to_thread(recognition_state.mutate, STATE_PATH, + movement_enabled=bool(on)) + # Enabling Gemini movement also clears any E-STOP latch on the dispatcher. + if on: + md = _get_dispatch() + if md is not None: + try: + md.clear_estop() + except Exception: + log.exception("clear_estop failed") + log.info("gemini-movement %s", "ON" if on else "OFF") + return {"ok": True, "movement_enabled": st.movement_enabled} + + +@router.post("/estop") +async def estop(): + lc = _require_loco() + res = await asyncio.to_thread(lc.estop) + # Full stop: drop the manual arm flag AND latch the voice dispatcher off, so + # no source (teleop, step, or voice dispatch) can keep driving the robot. The + # dispatcher latch is used instead of flipping movement_enabled so the Gemini + # child does not deliver a spoken "movement disabled" line during an E-STOP. + try: + await asyncio.to_thread(lc.disarm_movement) + except Exception: + log.exception("estop disarm failed") + md = _get_dispatch() + if md is not None: + try: + md.emergency_stop() + except Exception: + log.exception("estop dispatcher latch failed") + return {"ok": True, **res} + + +@router.post("/stop") +async def stop(): + lc = _require_loco() + # Allowed even when disarmed — StopMove is always safe. + res = await asyncio.to_thread(lc.stop_move) + return res + + +# ── movement (armed) ──────────────────────────────────────── + +class MoveBody(BaseModel): + vx: float = 0.0 + vy: float = 0.0 + vyaw: float = 0.0 + run: bool = False + + +@router.post("/move") +async def move(body: MoveBody): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.move, body.vx, body.vy, body.vyaw, body.run) + + +@router.post("/step") +async def step(dir: str = Query(...)): + lc = _require_loco() + _require_armed(lc) + res = await asyncio.to_thread(lc.step, dir) + if not res.get("ok"): + raise HTTPException(400, res.get("reason", "step failed")) + return res + + +# ── modes / postures (armed) ──────────────────────────────── + +@router.post("/mode/prep") +async def mode_prep(): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.prep_mode) + + +@router.post("/mode/ready") +async def mode_ready(): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.ready_start_mode) + + +@router.post("/posture/{name}") +async def posture(name: str): + lc = _require_loco() + _require_armed(lc) + res = await asyncio.to_thread(lc.posture, name) + if not res.get("ok") and res.get("reason"): + raise HTTPException(400, res["reason"]) + return res + + +@router.post("/balance") +async def balance(mode: int = Query(...)): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.set_balance_mode, mode) + + +@router.post("/height") +async def height(h: float = Query(...)): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.set_stand_height, h) + + +# ── MotionSwitcher / reconnect (armed) ────────────────────── + +@router.post("/msc/select-ai") +async def msc_select_ai(): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.msc_select_ai) + + +@router.post("/msc/release") +async def msc_release(): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.msc_release) + + +@router.post("/reconnect") +async def reconnect(): + lc = _require_loco() + _require_armed(lc) + return await asyncio.to_thread(lc.reconnect) + + +# ── aggregate subsystem summary (always available) ────────── + +@router.get("/status/summary") +async def status_summary(): + """Live on/off state for the header status strip. Never raises.""" + try: + st = recognition_state.read(STATE_PATH) + except Exception: + st = recognition_state.RecognitionState() + + cam = _get_camera() + camera_running = False + try: + camera_running = bool(cam is not None and cam.is_running()) + except Exception: + camera_running = False + + lc = _get_loco() + movement_armed = False + try: + movement_armed = bool(lc is not None and lc.is_armed()) + except Exception: + movement_armed = False + + sub = _get_live_sub() + gemini_running = False + try: + runner = getattr(sub, "is_running", None) + gemini_running = bool(callable(runner) and runner()) + except Exception: + gemini_running = False + + # Effective Gemini-movement = the file flag AND not latched off by an E-STOP. + md = _get_dispatch() + estopped = False + try: + estopped = bool(md is not None and md.is_estopped()) + except Exception: + estopped = False + + return { + "vision_enabled": st.vision_enabled, + "camera_running": camera_running, + "face_rec_enabled": st.face_rec_enabled, + "zone_rec_enabled": st.zone_rec_enabled, + "movement_armed": movement_armed, + "gemini_movement_enabled": st.movement_enabled and not estopped, + "gemini_running": gemini_running, + } diff --git a/dashboard/routes/macros.py b/dashboard/routes/macros.py index c4fcaab..56c3042 100644 --- a/dashboard/routes/macros.py +++ b/dashboard/routes/macros.py @@ -15,6 +15,22 @@ log = get_logger("macros_route") router = APIRouter() +def _block_if_movement_armed(): + """409 when locomotion movement is armed — arm motion is mutually exclusive + with walking. The arm controller's motion-block is the safety net.""" + try: + from Project.Sanad.main import loco_controller # type: ignore + armed = loco_controller is not None and loco_controller.is_armed() + except HTTPException: + raise + except Exception: + return + if armed: + raise HTTPException( + 409, "Arm actions are disabled while movement is enabled. " + "Disable movement in the Controller tab first.") + + class MacroName(BaseModel): name: str @@ -63,6 +79,7 @@ async def stop_recording(): @router.post("/play") async def play_macro(payload: MacroName): from Project.Sanad.main import brain + _block_if_movement_armed() return await brain.play_macro(payload.name) @@ -157,6 +174,8 @@ async def play_combined(payload: ComboPlayPayload): has_motion = payload.action_id is not None or bool(payload.motion_file) if not has_audio and not has_motion: raise HTTPException(400, "pick at least one of audio_file / action_id / motion_file") + if has_motion: + _block_if_movement_armed() # audio-only combos still allowed while armed tasks = [] result: dict = { diff --git a/dashboard/routes/motion.py b/dashboard/routes/motion.py index 34ced42..58cea95 100644 --- a/dashboard/routes/motion.py +++ b/dashboard/routes/motion.py @@ -10,6 +10,23 @@ from pydantic import BaseModel router = APIRouter() +def _block_if_movement_armed(): + """409 if locomotion movement is armed — arm actions are mutually exclusive + with walking. The arm controller's own motion-block is the safety net; this + just gives the dashboard a clear message instead of a silent no-op.""" + try: + from Project.Sanad.main import loco_controller # type: ignore + armed = loco_controller is not None and loco_controller.is_armed() + except HTTPException: + raise + except Exception: + return + if armed: + raise HTTPException( + 409, "Arm actions are disabled while movement is enabled. " + "Disable movement in the Controller tab first.") + + @router.get("/status") async def motion_status(): from Project.Sanad.main import arm @@ -33,6 +50,7 @@ async def trigger_action(payload: TriggerPayload): from Project.Sanad.main import arm if arm is None: raise HTTPException(503, "Arm controller not attached.") + _block_if_movement_armed() speed = max(0.1, min(payload.speed, 5.0)) diff --git a/dashboard/routes/records.py b/dashboard/routes/records.py index a4d26c9..8846e6d 100644 --- a/dashboard/routes/records.py +++ b/dashboard/routes/records.py @@ -111,8 +111,42 @@ async def play_record(payload: RecordPlay): from Project.Sanad.main import audio_mgr import asyncio - await asyncio.to_thread(audio_mgr.play_wav, raw_path) - return {"ok": True, "record_name": payload.record_name, "file_kind": payload.file_kind, "path": str(raw_path)} + # Fire-and-forget — play_wav blocks for the clip duration on the G1 + # DDS path, and the dashboard's pause / resume / stop / status calls + # need to be served while it's running. Without this, /play wouldn't + # return until the clip finished and the UI couldn't interact with + # the in-flight playback. + asyncio.create_task(asyncio.to_thread( + audio_mgr.play_wav, raw_path, payload.record_name, + )) + return {"ok": True, "record_name": payload.record_name, + "file_kind": payload.file_kind, "path": str(raw_path)} + + +@router.post("/pause") +async def pause_playback(): + from Project.Sanad.main import audio_mgr + return audio_mgr.pause_playback() + + +@router.post("/resume") +async def resume_playback(): + from Project.Sanad.main import audio_mgr + return audio_mgr.resume_playback() + + +@router.post("/stop") +async def stop_playback(): + from Project.Sanad.main import audio_mgr + import asyncio + await asyncio.to_thread(audio_mgr.stop_playback) + return {"ok": True, "stopped": True} + + +@router.get("/playback-status") +async def playback_status(): + from Project.Sanad.main import audio_mgr + return audio_mgr.playback_status() class RecordRename(BaseModel): diff --git a/dashboard/routes/replay.py b/dashboard/routes/replay.py index 7d30c7f..698bcca 100644 --- a/dashboard/routes/replay.py +++ b/dashboard/routes/replay.py @@ -21,6 +21,22 @@ log = get_logger("replay_route") router = APIRouter() +def _block_if_movement_armed(): + """409 when locomotion movement is armed — arm motion (replay / teaching) is + mutually exclusive with walking.""" + try: + from Project.Sanad.main import loco_controller # type: ignore + armed = loco_controller is not None and loco_controller.is_armed() + except HTTPException: + raise + except Exception: + return + if armed: + raise HTTPException( + 409, "Arm actions are disabled while movement is enabled. " + "Disable movement in the Controller tab first.") + + # -- models -- class ReplayRequest(BaseModel): @@ -94,6 +110,7 @@ _BG_TASKS: set[asyncio.Task] = set() async def test_replay(payload: ReplayRequest): """Test-play a motion file at the given speed.""" from Project.Sanad.main import arm + _block_if_movement_armed() if arm.is_busy: raise HTTPException(409, "Arm is busy.") path = safe_path_under(MOTIONS_DIR, payload.name) @@ -142,6 +159,7 @@ async def start_teaching(payload: TeachRequest): from Project.Sanad.main import teacher if teacher is None: raise HTTPException(503, "Teaching module not available.") + _block_if_movement_armed() if teacher.is_recording: raise HTTPException(409, "Teaching session already active.") existing = MOTIONS_DIR / f"{payload.name}.jsonl" diff --git a/dashboard/routes/temp_monitor.py b/dashboard/routes/temp_monitor.py new file mode 100644 index 0000000..266e7b4 --- /dev/null +++ b/dashboard/routes/temp_monitor.py @@ -0,0 +1,67 @@ +"""REST endpoints backing the 3D motor-temperature dashboard (N1). + +Serves the motor name/mesh mapping + thresholds, and a one-shot temperature +snapshot (the front-end's initial fetch fallback). The live stream is over +/ws/motor-temps (dashboard/websockets/motor_temps.py). The 3D view itself is +the static page at /static/temp3d/index.html. +""" + +from __future__ import annotations + +import time + +from fastapi import APIRouter + +from Project.Sanad.dashboard.temp_motor_map import ( + MOTOR_NAMES, + MOTOR_TO_MESH, + TEMP_HOT_THRESHOLD, + TEMP_MAX, + TEMP_MIN, + TEMP_WARM_THRESHOLD, + build_payload, +) + +router = APIRouter() + + +def _get_arm(): + """Lazy import — avoids a circular import on dashboard load.""" + try: + from Project.Sanad.main import arm # type: ignore + return arm + except Exception: + return None + + +@router.get("/mapping") +async def motor_mapping(): + """Motor id → name / mesh map + the temperature gradient thresholds.""" + return { + "motor_names": MOTOR_NAMES, + "motor_to_mesh": MOTOR_TO_MESH, + "thresholds": { + "min": TEMP_MIN, + "max": TEMP_MAX, + "warm": TEMP_WARM_THRESHOLD, + "hot": TEMP_HOT_THRESHOLD, + }, + } + + +@router.get("/motors") +async def motors_snapshot(): + """One-shot motor temperature + position snapshot (Marcus payload shape).""" + arm = _get_arm() + temps: list = [] + positions: list = [] + if arm is not None: + try: + temps = arm.get_motor_temps() + except Exception: + temps = [] + try: + positions = arm.get_current_q() + except Exception: + positions = [] + return build_payload(temps, positions, time.time()) diff --git a/dashboard/routes/zones.py b/dashboard/routes/zones.py new file mode 100644 index 0000000..395077b --- /dev/null +++ b/dashboard/routes/zones.py @@ -0,0 +1,421 @@ +"""Zones tab — zone → place → linked-faces management + "go here" destination. + +Hierarchy (replaces the old flat places): + Zone (name + description) + └─ Place (name + description + optional reference photos + linked face ids) + +Routes live under /api/zones. Toggle + CRUD changes write +data/.recognition_state.json (the SAME file faces use); the Gemini child polls +it at 1 Hz and re-primes / announces mid-session. The "go here" endpoints set a +navigation target the robot will head to once N2 locomotion is wired — for now +they just record the target and feed Gemini the place's reference. +""" + +from __future__ import annotations + +import io +from typing import Optional + +from fastapi import APIRouter, File, HTTPException, Query, UploadFile +from fastapi.responses import FileResponse, StreamingResponse +from pydantic import BaseModel + +from Project.Sanad.config import BASE_DIR +from Project.Sanad.core.logger import get_logger +from Project.Sanad.dashboard.routes._safe_io import check_upload_size +from Project.Sanad.vision import recognition_state + +log = get_logger("zones_routes") + +router = APIRouter() + +STATE_PATH = BASE_DIR / "data" / ".recognition_state.json" + + +# ── lazy subsystem accessors ──────────────────────────────── + +def _get_camera(): + try: + from Project.Sanad.main import camera # type: ignore + return camera + except Exception: + return None + + +def _get_zone_gallery(): + try: + from Project.Sanad.main import zone_gallery # type: ignore + return zone_gallery + except Exception: + return None + + +def _get_face_gallery(): + try: + from Project.Sanad.main import gallery # type: ignore + return gallery + except Exception: + return None + + +def _require_zones(): + g = _get_zone_gallery() + if g is None: + raise HTTPException(503, "Zone gallery subsystem unavailable.") + return g + + +def _bump_zones_version() -> int: + cur = recognition_state.read(STATE_PATH) + v = cur.zones_version + 1 + recognition_state.mutate(STATE_PATH, zones_version=v) + return v + + +def _validate_image(content: bytes, filename: str | None = None) -> None: + check_upload_size(content) + if len(content) < 16: + raise HTTPException(400, "Image too small / empty.") + if not (content[:3] == b"\xff\xd8\xff" or content[:8] == b"\x89PNG\r\n\x1a\n"): + raise HTTPException(400, f"Only JPEG/PNG accepted (got {filename or 'unknown'}).") + + +def _safe_photo_name(name: str) -> None: + if "/" in name or ".." in name or "\x00" in name: + raise HTTPException(400, "Invalid photo name.") + + +def _resolve_faces(face_ids: list[int]) -> list[dict]: + """Turn linked face ids into [{id, name}] using the face gallery.""" + g = _get_face_gallery() + out = [] + for fid in face_ids: + name = None + if g is not None: + try: + e = g.get(fid) + name = e.name if e else None + except Exception: + name = None + out.append({"id": fid, "name": name}) + return out + + +def _place_to_dict(p) -> dict: + d = p.to_dict() + d["faces"] = _resolve_faces(p.face_ids) + return d + + +def _zone_to_dict(z) -> dict: + return { + "id": z.id, "name": z.name, "description": z.description, + "added_at": z.added_at, + "places": [_place_to_dict(p) for p in z.places], + } + + +def _nav_target_dict(st, gallery) -> Optional[dict]: + zid, pid = st.nav_target_zone_id, st.nav_target_place_id + if not zid or not pid: + return None + zone_name = place_name = None + if gallery is not None: + try: + z = gallery.get_zone(zid) + zone_name = z.name if z else None + p = gallery.get_place(zid, pid) + place_name = p.name if p else None + except Exception: + pass + return {"zone_id": zid, "place_id": pid, + "zone_name": zone_name, "place_name": place_name} + + +# ── state + toggle ────────────────────────────────────────── + +@router.get("/state") +async def get_state(): + st = recognition_state.read(STATE_PATH) + g = _get_zone_gallery() + zones_count = places_count = 0 + if g is not None: + try: + zones = g.list_zones() + zones_count = len(zones) + places_count = sum(len(z.places) for z in zones) + except Exception: + pass + return { + "zone_rec_enabled": st.zone_rec_enabled, + "zones_version": st.zones_version, + "zones_count": zones_count, + "places_count": places_count, + "nav_target": _nav_target_dict(st, g), + } + + +@router.post("/zone-rec") +async def set_zone_rec(on: bool = Query(...)): + """Enable / disable the robot's knowledge of zones & places (hot).""" + st = recognition_state.mutate(STATE_PATH, zone_rec_enabled=bool(on)) + log.info("zone recognition %s", "ON" if on else "OFF") + return {"ok": True, "zone_rec_enabled": st.zone_rec_enabled} + + +@router.post("/sync") +async def sync_zones(): + v = _bump_zones_version() + log.info("zones sync requested → v.%d", v) + return {"ok": True, "zones_version": v} + + +# ── zones CRUD ────────────────────────────────────────────── + +class NamePayload(BaseModel): + name: Optional[str] = None + + +class DescribePayload(BaseModel): + description: Optional[str] = None + + +class FacesPayload(BaseModel): + face_ids: list[int] = [] + + +@router.get("") +async def list_zones(): + g = _require_zones() + zones = g.list_zones() + return {"zones": [_zone_to_dict(z) for z in zones], "total": len(zones)} + + +@router.post("/create") +async def create_zone(name: Optional[str] = Query(default=None), + description: Optional[str] = Query(default=None)): + g = _require_zones() + if not (name or "").strip() and not (description or "").strip(): + raise HTTPException(400, "A zone needs at least a name or a description.") + z = g.create_zone(name=name, description=description) + _bump_zones_version() + return {"ok": True, "zone": _zone_to_dict(z)} + + +@router.post("/{zone_id}/rename") +async def rename_zone(zone_id: int, payload: NamePayload): + g = _require_zones() + try: + g.rename_zone(zone_id, payload.name) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "zone": _zone_to_dict(g.get_zone(zone_id))} + + +@router.post("/{zone_id}/describe") +async def describe_zone(zone_id: int, payload: DescribePayload): + g = _require_zones() + try: + g.describe_zone(zone_id, payload.description) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "zone": _zone_to_dict(g.get_zone(zone_id))} + + +@router.delete("/{zone_id}") +async def delete_zone(zone_id: int): + g = _require_zones() + try: + g.delete_zone(zone_id) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + # If the active destination was inside this zone, clear it. + st = recognition_state.read(STATE_PATH) + if st.nav_target_zone_id == zone_id: + recognition_state.mutate(STATE_PATH, nav_target_zone_id=0, nav_target_place_id=0) + _bump_zones_version() + return {"ok": True, "deleted": zone_id} + + +# ── places CRUD (within a zone) ───────────────────────────── + +@router.post("/{zone_id}/places/create") +async def create_place( + zone_id: int, + name: Optional[str] = Query(default=None), + description: Optional[str] = Query(default=None), + face_ids: list[int] = Query(default=[]), + files: Optional[list[UploadFile]] = File(default=None), +): + g = _require_zones() + if g.get_zone(zone_id) is None: + raise HTTPException(404, f"zone_{zone_id} not found") + if not (name or "").strip() and not (description or "").strip(): + raise HTTPException(400, "A place needs at least a name or a description.") + image_bytes: list[bytes] = [] + for f in (files or []): + content = await f.read() + if not content: + continue + _validate_image(content, f.filename) + image_bytes.append(content) + p = g.create_place(zone_id, name=name, description=description, + face_ids=face_ids, image_bytes_list=image_bytes or None) + _bump_zones_version() + return {"ok": True, "place": _place_to_dict(p)} + + +@router.post("/{zone_id}/places/{place_id}/rename") +async def rename_place(zone_id: int, place_id: int, payload: NamePayload): + g = _require_zones() + try: + g.rename_place(zone_id, place_id, payload.name) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "place": _place_to_dict(g.get_place(zone_id, place_id))} + + +@router.post("/{zone_id}/places/{place_id}/describe") +async def describe_place(zone_id: int, place_id: int, payload: DescribePayload): + g = _require_zones() + try: + g.describe_place(zone_id, place_id, payload.description) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "place": _place_to_dict(g.get_place(zone_id, place_id))} + + +@router.post("/{zone_id}/places/{place_id}/faces") +async def set_place_faces(zone_id: int, place_id: int, payload: FacesPayload): + """Replace the set of saved faces linked to this place.""" + g = _require_zones() + try: + g.set_place_faces(zone_id, place_id, payload.face_ids) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "place": _place_to_dict(g.get_place(zone_id, place_id))} + + +@router.post("/{zone_id}/places/{place_id}/capture") +async def capture_to_place(zone_id: int, place_id: int): + g = _require_zones() + cam = _get_camera() + if cam is None or not cam.is_running(): + raise HTTPException(409, "Camera is not running. Toggle Vision ON first.") + jpeg = cam.get_fresh_frame(max_age_s=0.5, timeout_s=1.5) + if not jpeg: + raise HTTPException(409, "Camera has no frame yet.") + try: + fname = g.add_photo(zone_id, place_id, jpeg) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "added": fname, "place": _place_to_dict(g.get_place(zone_id, place_id))} + + +@router.post("/{zone_id}/places/{place_id}/upload") +async def upload_to_place(zone_id: int, place_id: int, + files: list[UploadFile] = File(...)): + g = _require_zones() + if g.get_place(zone_id, place_id) is None: + raise HTTPException(404, f"zone_{zone_id}/place_{place_id} not found") + added: list[str] = [] + for f in files: + content = await f.read() + _validate_image(content, f.filename) + try: + added.append(g.add_photo(zone_id, place_id, content)) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "added": added, "place": _place_to_dict(g.get_place(zone_id, place_id))} + + +@router.delete("/{zone_id}/places/{place_id}") +async def delete_place(zone_id: int, place_id: int): + g = _require_zones() + try: + g.delete_place(zone_id, place_id) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + st = recognition_state.read(STATE_PATH) + if st.nav_target_zone_id == zone_id and st.nav_target_place_id == place_id: + recognition_state.mutate(STATE_PATH, nav_target_zone_id=0, nav_target_place_id=0) + _bump_zones_version() + return {"ok": True, "deleted": place_id} + + +@router.delete("/{zone_id}/places/{place_id}/photo/{photo_name}") +async def delete_place_photo(zone_id: int, place_id: int, photo_name: str): + g = _require_zones() + _safe_photo_name(photo_name) + try: + g.delete_photo(zone_id, place_id, photo_name) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + _bump_zones_version() + return {"ok": True, "deleted": photo_name} + + +@router.get("/{zone_id}/places/{place_id}/photo/{photo_name}") +async def get_place_photo(zone_id: int, place_id: int, photo_name: str, + download: int = Query(default=0)): + g = _require_zones() + _safe_photo_name(photo_name) + path = g.get_photo(zone_id, place_id, photo_name) + if path is None: + raise HTTPException(404, "Photo not found.") + media = "image/png" if path.suffix.lower() == ".png" else "image/jpeg" + headers = {} + if download: + headers["Content-Disposition"] = ( + f'attachment; filename="zone_{zone_id}_place_{place_id}_{photo_name}"') + return FileResponse(path, media_type=media, headers=headers) + + +@router.get("/{zone_id}/places/{place_id}/download.zip") +async def download_place_zip(zone_id: int, place_id: int): + g = _require_zones() + try: + data = g.zip_place(zone_id, place_id) + except FileNotFoundError as exc: + raise HTTPException(404, str(exc)) + return StreamingResponse( + io.BytesIO(data), media_type="application/zip", + headers={ + "Content-Disposition": f'attachment; filename="zone_{zone_id}_place_{place_id}.zip"', + "Content-Length": str(len(data)), + }, + ) + + +# ── "go here" navigation target ───────────────────────────── + +@router.post("/{zone_id}/places/{place_id}/go") +async def go_to_place(zone_id: int, place_id: int): + """Set this place as the active destination. Records the target and lets + the Gemini child pick it up (reference photo + goal). Actual robot motion + is wired by N2 locomotion — until then this just establishes the goal.""" + g = _require_zones() + p = g.get_place(zone_id, place_id) + if p is None: + raise HTTPException(404, f"zone_{zone_id}/place_{place_id} not found") + recognition_state.mutate(STATE_PATH, + nav_target_zone_id=zone_id, + nav_target_place_id=place_id) + log.info("nav target set → zone_%d/place_%d (%s)", zone_id, place_id, + p.name or "(unnamed)") + return {"ok": True, "nav_target": {"zone_id": zone_id, "place_id": place_id, + "place_name": p.name}} + + +@router.post("/nav/clear") +async def clear_nav_target(): + recognition_state.mutate(STATE_PATH, nav_target_zone_id=0, nav_target_place_id=0) + log.info("nav target cleared") + return {"ok": True, "nav_target": None} diff --git a/dashboard/static/index.html b/dashboard/static/index.html index 086b375..754fac6 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -5,7 +5,7 @@ Sanad Dashboard + + + + +
+
+
Loading G1 Robot Model
+
0%
+
+ + +
+ + +
+

🤖 G1 3D Temperature Monitor

+
+
+ Connecting... +
+
+ + +
+

📊 Statistics

+
+ Total Motors + 0 +
+
+ Avg Temp + --°C +
+
+ Max Temp + --°C +
+
+ Min Temp + --°C +
+
+ Last Update + -- +
+
+ + +
+

🎮 Controls

+ + + + +
+ + +
+

🌡️ Temperature Scale

+
+
+ 30°C + 75°C + 120°C +
+
+ + +
+

+ Motor Information + +

+
+
+ + + + + \ No newline at end of file diff --git a/dashboard/static/temp3d/js/OrbitControls.js b/dashboard/static/temp3d/js/OrbitControls.js new file mode 100644 index 0000000..0873097 --- /dev/null +++ b/dashboard/static/temp3d/js/OrbitControls.js @@ -0,0 +1,1045 @@ +( function () { + + // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). + // + // Orbit - left mouse / touch: one-finger move + // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish + // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move + + const _changeEvent = { + type: 'change' + }; + const _startEvent = { + type: 'start' + }; + const _endEvent = { + type: 'end' + }; + + class OrbitControls extends THREE.EventDispatcher { + + constructor( object, domElement ) { + + super(); + if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); + if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); + this.object = object; + this.domElement = domElement; // Set to false to disable this control + + this.enabled = true; // "target" sets the location of focus, where the object orbits around + + this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only ) + + this.minDistance = 0; + this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only ) + + this.minZoom = 0; + this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits. + // Range is 0 to Math.PI radians. + + this.minPolarAngle = 0; // radians + + this.maxPolarAngle = Math.PI; // radians + // How far you can orbit horizontally, upper and lower limits. + // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) + + this.minAzimuthAngle = - Infinity; // radians + + this.maxAzimuthAngle = Infinity; // radians + // Set to true to enable damping (inertia) + // If damping is enabled, you must call controls.update() in your animation loop + + this.enableDamping = false; + this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. + // Set to false to disable zooming + + this.enableZoom = true; + this.zoomSpeed = 1.0; // Set to false to disable rotating + + this.enableRotate = true; + this.rotateSpeed = 1.0; // Set to false to disable panning + + this.enablePan = true; + this.panSpeed = 1.0; + this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up + + this.keyPanSpeed = 7.0; // pixels moved per arrow key push + // Set to true to automatically rotate around the target + // If auto-rotate is enabled, you must call controls.update() in your animation loop + + this.autoRotate = false; + this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 + // The four arrow keys + + this.keys = { + LEFT: 'ArrowLeft', + UP: 'ArrowUp', + RIGHT: 'ArrowRight', + BOTTOM: 'ArrowDown' + }; // Mouse buttons + + this.mouseButtons = { + LEFT: THREE.MOUSE.ROTATE, + MIDDLE: THREE.MOUSE.DOLLY, + RIGHT: THREE.MOUSE.PAN + }; // Touch fingers + + this.touches = { + ONE: THREE.TOUCH.ROTATE, + TWO: THREE.TOUCH.DOLLY_PAN + }; // for reset + + this.target0 = this.target.clone(); + this.position0 = this.object.position.clone(); + this.zoom0 = this.object.zoom; // the target DOM element for key events + + this._domElementKeyEvents = null; // + // public methods + // + + this.getPolarAngle = function () { + + return spherical.phi; + + }; + + this.getAzimuthalAngle = function () { + + return spherical.theta; + + }; + + this.listenToKeyEvents = function ( domElement ) { + + domElement.addEventListener( 'keydown', onKeyDown ); + this._domElementKeyEvents = domElement; + + }; + + this.saveState = function () { + + scope.target0.copy( scope.target ); + scope.position0.copy( scope.object.position ); + scope.zoom0 = scope.object.zoom; + + }; + + this.reset = function () { + + scope.target.copy( scope.target0 ); + scope.object.position.copy( scope.position0 ); + scope.object.zoom = scope.zoom0; + scope.object.updateProjectionMatrix(); + scope.dispatchEvent( _changeEvent ); + scope.update(); + state = STATE.NONE; + + }; // this method is exposed, but perhaps it would be better if we can make it private... + + + this.update = function () { + + const offset = new THREE.Vector3(); // so camera.up is the orbit axis + + const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); + const quatInverse = quat.clone().invert(); + const lastPosition = new THREE.Vector3(); + const lastQuaternion = new THREE.Quaternion(); + const twoPI = 2 * Math.PI; + return function update() { + + const position = scope.object.position; + offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space + + offset.applyQuaternion( quat ); // angle from z-axis around y-axis + + spherical.setFromVector3( offset ); + + if ( scope.autoRotate && state === STATE.NONE ) { + + rotateLeft( getAutoRotationAngle() ); + + } + + if ( scope.enableDamping ) { + + spherical.theta += sphericalDelta.theta * scope.dampingFactor; + spherical.phi += sphericalDelta.phi * scope.dampingFactor; + + } else { + + spherical.theta += sphericalDelta.theta; + spherical.phi += sphericalDelta.phi; + + } // restrict theta to be between desired limits + + + let min = scope.minAzimuthAngle; + let max = scope.maxAzimuthAngle; + + if ( isFinite( min ) && isFinite( max ) ) { + + if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; + if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; + + if ( min <= max ) { + + spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); + + } else { + + spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta ); + + } + + } // restrict phi to be between desired limits + + + spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); + spherical.makeSafe(); + spherical.radius *= scale; // restrict radius to be between desired limits + + spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location + + if ( scope.enableDamping === true ) { + + scope.target.addScaledVector( panOffset, scope.dampingFactor ); + + } else { + + scope.target.add( panOffset ); + + } + + offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space + + offset.applyQuaternion( quatInverse ); + position.copy( scope.target ).add( offset ); + scope.object.lookAt( scope.target ); + + if ( scope.enableDamping === true ) { + + sphericalDelta.theta *= 1 - scope.dampingFactor; + sphericalDelta.phi *= 1 - scope.dampingFactor; + panOffset.multiplyScalar( 1 - scope.dampingFactor ); + + } else { + + sphericalDelta.set( 0, 0, 0 ); + panOffset.set( 0, 0, 0 ); + + } + + scale = 1; // update condition is: + // min(camera displacement, camera rotation in radians)^2 > EPS + // using small-angle approximation cos(x/2) = 1 - x^2 / 8 + + if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { + + scope.dispatchEvent( _changeEvent ); + lastPosition.copy( scope.object.position ); + lastQuaternion.copy( scope.object.quaternion ); + zoomChanged = false; + return true; + + } + + return false; + + }; + + }(); + + this.dispose = function () { + + scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); + scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); + scope.domElement.removeEventListener( 'wheel', onMouseWheel ); + scope.domElement.removeEventListener( 'touchstart', onTouchStart ); + scope.domElement.removeEventListener( 'touchend', onTouchEnd ); + scope.domElement.removeEventListener( 'touchmove', onTouchMove ); + scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); + scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); + + if ( scope._domElementKeyEvents !== null ) { + + scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); + + } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? + + }; // + // internals + // + + + const scope = this; + const STATE = { + NONE: - 1, + ROTATE: 0, + DOLLY: 1, + PAN: 2, + TOUCH_ROTATE: 3, + TOUCH_PAN: 4, + TOUCH_DOLLY_PAN: 5, + TOUCH_DOLLY_ROTATE: 6 + }; + let state = STATE.NONE; + const EPS = 0.000001; // current position in spherical coordinates + + const spherical = new THREE.Spherical(); + const sphericalDelta = new THREE.Spherical(); + let scale = 1; + const panOffset = new THREE.Vector3(); + let zoomChanged = false; + const rotateStart = new THREE.Vector2(); + const rotateEnd = new THREE.Vector2(); + const rotateDelta = new THREE.Vector2(); + const panStart = new THREE.Vector2(); + const panEnd = new THREE.Vector2(); + const panDelta = new THREE.Vector2(); + const dollyStart = new THREE.Vector2(); + const dollyEnd = new THREE.Vector2(); + const dollyDelta = new THREE.Vector2(); + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + + } + + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); + + } + + function rotateLeft( angle ) { + + sphericalDelta.theta -= angle; + + } + + function rotateUp( angle ) { + + sphericalDelta.phi -= angle; + + } + + const panLeft = function () { + + const v = new THREE.Vector3(); + return function panLeft( distance, objectMatrix ) { + + v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix + + v.multiplyScalar( - distance ); + panOffset.add( v ); + + }; + + }(); + + const panUp = function () { + + const v = new THREE.Vector3(); + return function panUp( distance, objectMatrix ) { + + if ( scope.screenSpacePanning === true ) { + + v.setFromMatrixColumn( objectMatrix, 1 ); + + } else { + + v.setFromMatrixColumn( objectMatrix, 0 ); + v.crossVectors( scope.object.up, v ); + + } + + v.multiplyScalar( distance ); + panOffset.add( v ); + + }; + + }(); // deltaX and deltaY are in pixels; right and down are positive + + + const pan = function () { + + const offset = new THREE.Vector3(); + return function pan( deltaX, deltaY ) { + + const element = scope.domElement; + + if ( scope.object.isPerspectiveCamera ) { + + // perspective + const position = scope.object.position; + offset.copy( position ).sub( scope.target ); + let targetDistance = offset.length(); // half of the fov is center to top of screen + + targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed + + panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); + panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); + + } else if ( scope.object.isOrthographicCamera ) { + + // orthographic + panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); + panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); + + } else { + + // camera neither orthographic nor perspective + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); + scope.enablePan = false; + + } + + }; + + }(); + + function dollyOut( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale /= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } + + function dollyIn( dollyScale ) { + + if ( scope.object.isPerspectiveCamera ) { + + scale *= dollyScale; + + } else if ( scope.object.isOrthographicCamera ) { + + scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); + scope.object.updateProjectionMatrix(); + zoomChanged = true; + + } else { + + console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); + scope.enableZoom = false; + + } + + } // + // event callbacks - update the object state + // + + + function handleMouseDownRotate( event ) { + + rotateStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownDolly( event ) { + + dollyStart.set( event.clientX, event.clientY ); + + } + + function handleMouseDownPan( event ) { + + panStart.set( event.clientX, event.clientY ); + + } + + function handleMouseMoveRotate( event ) { + + rotateEnd.set( event.clientX, event.clientY ); + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + const element = scope.domElement; + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height + + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + rotateStart.copy( rotateEnd ); + scope.update(); + + } + + function handleMouseMoveDolly( event ) { + + dollyEnd.set( event.clientX, event.clientY ); + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + dollyOut( getZoomScale() ); + + } else if ( dollyDelta.y < 0 ) { + + dollyIn( getZoomScale() ); + + } + + dollyStart.copy( dollyEnd ); + scope.update(); + + } + + function handleMouseMovePan( event ) { + + panEnd.set( event.clientX, event.clientY ); + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + pan( panDelta.x, panDelta.y ); + panStart.copy( panEnd ); + scope.update(); + + } + + function handleMouseUp( ) { // no-op + } + + function handleMouseWheel( event ) { + + if ( event.deltaY < 0 ) { + + dollyIn( getZoomScale() ); + + } else if ( event.deltaY > 0 ) { + + dollyOut( getZoomScale() ); + + } + + scope.update(); + + } + + function handleKeyDown( event ) { + + let needsUpdate = false; + + switch ( event.code ) { + + case scope.keys.UP: + pan( 0, scope.keyPanSpeed ); + needsUpdate = true; + break; + + case scope.keys.BOTTOM: + pan( 0, - scope.keyPanSpeed ); + needsUpdate = true; + break; + + case scope.keys.LEFT: + pan( scope.keyPanSpeed, 0 ); + needsUpdate = true; + break; + + case scope.keys.RIGHT: + pan( - scope.keyPanSpeed, 0 ); + needsUpdate = true; + break; + + } + + if ( needsUpdate ) { + + // prevent the browser from scrolling on cursor keys + event.preventDefault(); + scope.update(); + + } + + } + + function handleTouchStartRotate( event ) { + + if ( event.touches.length == 1 ) { + + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + rotateStart.set( x, y ); + + } + + } + + function handleTouchStartPan( event ) { + + if ( event.touches.length == 1 ) { + + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + panStart.set( x, y ); + + } + + } + + function handleTouchStartDolly( event ) { + + const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + const distance = Math.sqrt( dx * dx + dy * dy ); + dollyStart.set( 0, distance ); + + } + + function handleTouchStartDollyPan( event ) { + + if ( scope.enableZoom ) handleTouchStartDolly( event ); + if ( scope.enablePan ) handleTouchStartPan( event ); + + } + + function handleTouchStartDollyRotate( event ) { + + if ( scope.enableZoom ) handleTouchStartDolly( event ); + if ( scope.enableRotate ) handleTouchStartRotate( event ); + + } + + function handleTouchMoveRotate( event ) { + + if ( event.touches.length == 1 ) { + + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + rotateEnd.set( x, y ); + + } + + rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); + const element = scope.domElement; + rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height + + rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); + rotateStart.copy( rotateEnd ); + + } + + function handleTouchMovePan( event ) { + + if ( event.touches.length == 1 ) { + + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + + } else { + + const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); + const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); + panEnd.set( x, y ); + + } + + panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); + pan( panDelta.x, panDelta.y ); + panStart.copy( panEnd ); + + } + + function handleTouchMoveDolly( event ) { + + const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + const distance = Math.sqrt( dx * dx + dy * dy ); + dollyEnd.set( 0, distance ); + dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); + dollyOut( dollyDelta.y ); + dollyStart.copy( dollyEnd ); + + } + + function handleTouchMoveDollyPan( event ) { + + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + if ( scope.enablePan ) handleTouchMovePan( event ); + + } + + function handleTouchMoveDollyRotate( event ) { + + if ( scope.enableZoom ) handleTouchMoveDolly( event ); + if ( scope.enableRotate ) handleTouchMoveRotate( event ); + + } + + function handleTouchEnd( ) { // no-op + } // + // event handlers - FSM: listen for events and reset state + // + + + function onPointerDown( event ) { + + if ( scope.enabled === false ) return; + + switch ( event.pointerType ) { + + case 'mouse': + case 'pen': + onMouseDown( event ); + break; + // TODO touch + + } + + } + + function onPointerMove( event ) { + + if ( scope.enabled === false ) return; + + switch ( event.pointerType ) { + + case 'mouse': + case 'pen': + onMouseMove( event ); + break; + // TODO touch + + } + + } + + function onPointerUp( event ) { + + switch ( event.pointerType ) { + + case 'mouse': + case 'pen': + onMouseUp( event ); + break; + // TODO touch + + } + + } + + function onMouseDown( event ) { + + // Prevent the browser from scrolling. + event.preventDefault(); // Manually set the focus since calling preventDefault above + // prevents the browser from setting it automatically. + + scope.domElement.focus ? scope.domElement.focus() : window.focus(); + let mouseAction; + + switch ( event.button ) { + + case 0: + mouseAction = scope.mouseButtons.LEFT; + break; + + case 1: + mouseAction = scope.mouseButtons.MIDDLE; + break; + + case 2: + mouseAction = scope.mouseButtons.RIGHT; + break; + + default: + mouseAction = - 1; + + } + + switch ( mouseAction ) { + + case THREE.MOUSE.DOLLY: + if ( scope.enableZoom === false ) return; + handleMouseDownDolly( event ); + state = STATE.DOLLY; + break; + + case THREE.MOUSE.ROTATE: + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( scope.enablePan === false ) return; + handleMouseDownPan( event ); + state = STATE.PAN; + + } else { + + if ( scope.enableRotate === false ) return; + handleMouseDownRotate( event ); + state = STATE.ROTATE; + + } + + break; + + case THREE.MOUSE.PAN: + if ( event.ctrlKey || event.metaKey || event.shiftKey ) { + + if ( scope.enableRotate === false ) return; + handleMouseDownRotate( event ); + state = STATE.ROTATE; + + } else { + + if ( scope.enablePan === false ) return; + handleMouseDownPan( event ); + state = STATE.PAN; + + } + + break; + + default: + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); + scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); + scope.dispatchEvent( _startEvent ); + + } + + } + + function onMouseMove( event ) { + + if ( scope.enabled === false ) return; + event.preventDefault(); + + switch ( state ) { + + case STATE.ROTATE: + if ( scope.enableRotate === false ) return; + handleMouseMoveRotate( event ); + break; + + case STATE.DOLLY: + if ( scope.enableZoom === false ) return; + handleMouseMoveDolly( event ); + break; + + case STATE.PAN: + if ( scope.enablePan === false ) return; + handleMouseMovePan( event ); + break; + + } + + } + + function onMouseUp( event ) { + + scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); + scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); + if ( scope.enabled === false ) return; + handleMouseUp( event ); + scope.dispatchEvent( _endEvent ); + state = STATE.NONE; + + } + + function onMouseWheel( event ) { + + if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return; + event.preventDefault(); + scope.dispatchEvent( _startEvent ); + handleMouseWheel( event ); + scope.dispatchEvent( _endEvent ); + + } + + function onKeyDown( event ) { + + if ( scope.enabled === false || scope.enablePan === false ) return; + handleKeyDown( event ); + + } + + function onTouchStart( event ) { + + if ( scope.enabled === false ) return; + event.preventDefault(); // prevent scrolling + + switch ( event.touches.length ) { + + case 1: + switch ( scope.touches.ONE ) { + + case THREE.TOUCH.ROTATE: + if ( scope.enableRotate === false ) return; + handleTouchStartRotate( event ); + state = STATE.TOUCH_ROTATE; + break; + + case THREE.TOUCH.PAN: + if ( scope.enablePan === false ) return; + handleTouchStartPan( event ); + state = STATE.TOUCH_PAN; + break; + + default: + state = STATE.NONE; + + } + + break; + + case 2: + switch ( scope.touches.TWO ) { + + case THREE.TOUCH.DOLLY_PAN: + if ( scope.enableZoom === false && scope.enablePan === false ) return; + handleTouchStartDollyPan( event ); + state = STATE.TOUCH_DOLLY_PAN; + break; + + case THREE.TOUCH.DOLLY_ROTATE: + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + handleTouchStartDollyRotate( event ); + state = STATE.TOUCH_DOLLY_ROTATE; + break; + + default: + state = STATE.NONE; + + } + + break; + + default: + state = STATE.NONE; + + } + + if ( state !== STATE.NONE ) { + + scope.dispatchEvent( _startEvent ); + + } + + } + + function onTouchMove( event ) { + + if ( scope.enabled === false ) return; + event.preventDefault(); // prevent scrolling + + switch ( state ) { + + case STATE.TOUCH_ROTATE: + if ( scope.enableRotate === false ) return; + handleTouchMoveRotate( event ); + scope.update(); + break; + + case STATE.TOUCH_PAN: + if ( scope.enablePan === false ) return; + handleTouchMovePan( event ); + scope.update(); + break; + + case STATE.TOUCH_DOLLY_PAN: + if ( scope.enableZoom === false && scope.enablePan === false ) return; + handleTouchMoveDollyPan( event ); + scope.update(); + break; + + case STATE.TOUCH_DOLLY_ROTATE: + if ( scope.enableZoom === false && scope.enableRotate === false ) return; + handleTouchMoveDollyRotate( event ); + scope.update(); + break; + + default: + state = STATE.NONE; + + } + + } + + function onTouchEnd( event ) { + + if ( scope.enabled === false ) return; + handleTouchEnd( event ); + scope.dispatchEvent( _endEvent ); + state = STATE.NONE; + + } + + function onContextMenu( event ) { + + if ( scope.enabled === false ) return; + event.preventDefault(); + + } // + + + scope.domElement.addEventListener( 'contextmenu', onContextMenu ); + scope.domElement.addEventListener( 'pointerdown', onPointerDown ); + scope.domElement.addEventListener( 'wheel', onMouseWheel, { + passive: false + } ); + scope.domElement.addEventListener( 'touchstart', onTouchStart, { + passive: false + } ); + scope.domElement.addEventListener( 'touchend', onTouchEnd ); + scope.domElement.addEventListener( 'touchmove', onTouchMove, { + passive: false + } ); // force an update at start + + this.update(); + + } + + } // This set of controls performs orbiting, dollying (zooming), and panning. + // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). + // This is very similar to OrbitControls, another set of touch behavior + // + // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate + // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish + // Pan - left mouse, or arrow keys / touch: one-finger move + + + class MapControls extends OrbitControls { + + constructor( object, domElement ) { + + super( object, domElement ); + this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up + + this.mouseButtons.LEFT = THREE.MOUSE.PAN; + this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; + this.touches.ONE = THREE.TOUCH.PAN; + this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; + + } + + } + + THREE.MapControls = MapControls; + THREE.OrbitControls = OrbitControls; + +} )(); diff --git a/dashboard/static/temp3d/js/STLLoader.js b/dashboard/static/temp3d/js/STLLoader.js new file mode 100644 index 0000000..5c50fc2 --- /dev/null +++ b/dashboard/static/temp3d/js/STLLoader.js @@ -0,0 +1,371 @@ +( function () { + + /** + * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs. + * + * Supports both binary and ASCII encoded files, with automatic detection of type. + * + * The loader returns a non-indexed buffer geometry. + * + * Limitations: + * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL). + * There is perhaps some question as to how valid it is to always assume little-endian-ness. + * ASCII decoding assumes file is UTF-8. + * + * Usage: + * const loader = new STLLoader(); + * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) { + * scene.add( new THREE.Mesh( geometry ) ); + * }); + * + * For binary STLs geometry might contain colors for vertices. To use it: + * // use the same code to load STL as above + * if (geometry.hasColors) { + * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true }); + * } else { .... } + * const mesh = new THREE.Mesh( geometry, material ); + * + * For ASCII STLs containing multiple solids, each solid is assigned to a different group. + * Groups can be used to assign a different color by defining an array of materials with the same length of + * geometry.groups and passing it to the Mesh constructor: + * + * const mesh = new THREE.Mesh( geometry, material ); + * + * For example: + * + * const materials = []; + * const nGeometryGroups = geometry.groups.length; + * + * const colorMap = ...; // Some logic to index colors. + * + * for (let i = 0; i < nGeometryGroups; i++) { + * + * const material = new THREE.MeshPhongMaterial({ + * color: colorMap[i], + * wireframe: false + * }); + * + * } + * + * materials.push(material); + * const mesh = new THREE.Mesh(geometry, materials); + */ + + class STLLoader extends THREE.Loader { + + constructor( manager ) { + + super( manager ); + + } + + load( url, onLoad, onProgress, onError ) { + + const scope = this; + const loader = new THREE.FileLoader( this.manager ); + loader.setPath( this.path ); + loader.setResponseType( 'arraybuffer' ); + loader.setRequestHeader( this.requestHeader ); + loader.setWithCredentials( this.withCredentials ); + loader.load( url, function ( text ) { + + try { + + onLoad( scope.parse( text ) ); + + } catch ( e ) { + + if ( onError ) { + + onError( e ); + + } else { + + console.error( e ); + + } + + scope.manager.itemError( url ); + + } + + }, onProgress, onError ); + + } + + parse( data ) { + + function isBinary( data ) { + + const reader = new DataView( data ); + const face_size = 32 / 8 * 3 + 32 / 8 * 3 * 3 + 16 / 8; + const n_faces = reader.getUint32( 80, true ); + const expect = 80 + 32 / 8 + n_faces * face_size; + + if ( expect === reader.byteLength ) { + + return true; + + } // An ASCII STL data must begin with 'solid ' as the first six bytes. + // However, ASCII STLs lacking the SPACE after the 'd' are known to be + // plentiful. So, check the first 5 bytes for 'solid'. + // Several encodings, such as UTF-8, precede the text with up to 5 bytes: + // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding + // Search for "solid" to start anywhere after those prefixes. + // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd' + + + const solid = [ 115, 111, 108, 105, 100 ]; + + for ( let off = 0; off < 5; off ++ ) { + + // If "solid" text is matched to the current offset, declare it to be an ASCII STL. + if ( matchDataViewAt( solid, reader, off ) ) return false; + + } // Couldn't find "solid" text at the beginning; it is binary STL. + + + return true; + + } + + function matchDataViewAt( query, reader, offset ) { + + // Check if each byte in query matches the corresponding byte from the current offset + for ( let i = 0, il = query.length; i < il; i ++ ) { + + if ( query[ i ] !== reader.getUint8( offset + i, false ) ) return false; + + } + + return true; + + } + + function parseBinary( data ) { + + const reader = new DataView( data ); + const faces = reader.getUint32( 80, true ); + let r, + g, + b, + hasColors = false, + colors; + let defaultR, defaultG, defaultB, alpha; // process STL header + // check for default color in header ("COLOR=rgba" sequence). + + for ( let index = 0; index < 80 - 10; index ++ ) { + + if ( reader.getUint32( index, false ) == 0x434F4C4F + /*COLO*/ + && reader.getUint8( index + 4 ) == 0x52 + /*'R'*/ + && reader.getUint8( index + 5 ) == 0x3D + /*'='*/ + ) { + + hasColors = true; + colors = new Float32Array( faces * 3 * 3 ); + defaultR = reader.getUint8( index + 6 ) / 255; + defaultG = reader.getUint8( index + 7 ) / 255; + defaultB = reader.getUint8( index + 8 ) / 255; + alpha = reader.getUint8( index + 9 ) / 255; + + } + + } + + const dataOffset = 84; + const faceLength = 12 * 4 + 2; + const geometry = new THREE.BufferGeometry(); + const vertices = new Float32Array( faces * 3 * 3 ); + const normals = new Float32Array( faces * 3 * 3 ); + + for ( let face = 0; face < faces; face ++ ) { + + const start = dataOffset + face * faceLength; + const normalX = reader.getFloat32( start, true ); + const normalY = reader.getFloat32( start + 4, true ); + const normalZ = reader.getFloat32( start + 8, true ); + + if ( hasColors ) { + + const packedColor = reader.getUint16( start + 48, true ); + + if ( ( packedColor & 0x8000 ) === 0 ) { + + // facet has its own unique color + r = ( packedColor & 0x1F ) / 31; + g = ( packedColor >> 5 & 0x1F ) / 31; + b = ( packedColor >> 10 & 0x1F ) / 31; + + } else { + + r = defaultR; + g = defaultG; + b = defaultB; + + } + + } + + for ( let i = 1; i <= 3; i ++ ) { + + const vertexstart = start + i * 12; + const componentIdx = face * 3 * 3 + ( i - 1 ) * 3; + vertices[ componentIdx ] = reader.getFloat32( vertexstart, true ); + vertices[ componentIdx + 1 ] = reader.getFloat32( vertexstart + 4, true ); + vertices[ componentIdx + 2 ] = reader.getFloat32( vertexstart + 8, true ); + normals[ componentIdx ] = normalX; + normals[ componentIdx + 1 ] = normalY; + normals[ componentIdx + 2 ] = normalZ; + + if ( hasColors ) { + + colors[ componentIdx ] = r; + colors[ componentIdx + 1 ] = g; + colors[ componentIdx + 2 ] = b; + + } + + } + + } + + geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) ); + geometry.setAttribute( 'normal', new THREE.BufferAttribute( normals, 3 ) ); + + if ( hasColors ) { + + geometry.setAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) ); + geometry.hasColors = true; + geometry.alpha = alpha; + + } + + return geometry; + + } + + function parseASCII( data ) { + + const geometry = new THREE.BufferGeometry(); + const patternSolid = /solid([\s\S]*?)endsolid/g; + const patternFace = /facet([\s\S]*?)endfacet/g; + let faceCounter = 0; + const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/.source; + const patternVertex = new RegExp( 'vertex' + patternFloat + patternFloat + patternFloat, 'g' ); + const patternNormal = new RegExp( 'normal' + patternFloat + patternFloat + patternFloat, 'g' ); + const vertices = []; + const normals = []; + const normal = new THREE.Vector3(); + let result; + let groupCount = 0; + let startVertex = 0; + let endVertex = 0; + + while ( ( result = patternSolid.exec( data ) ) !== null ) { + + startVertex = endVertex; + const solid = result[ 0 ]; + + while ( ( result = patternFace.exec( solid ) ) !== null ) { + + let vertexCountPerFace = 0; + let normalCountPerFace = 0; + const text = result[ 0 ]; + + while ( ( result = patternNormal.exec( text ) ) !== null ) { + + normal.x = parseFloat( result[ 1 ] ); + normal.y = parseFloat( result[ 2 ] ); + normal.z = parseFloat( result[ 3 ] ); + normalCountPerFace ++; + + } + + while ( ( result = patternVertex.exec( text ) ) !== null ) { + + vertices.push( parseFloat( result[ 1 ] ), parseFloat( result[ 2 ] ), parseFloat( result[ 3 ] ) ); + normals.push( normal.x, normal.y, normal.z ); + vertexCountPerFace ++; + endVertex ++; + + } // every face have to own ONE valid normal + + + if ( normalCountPerFace !== 1 ) { + + console.error( 'THREE.STLLoader: Something isn\'t right with the normal of face number ' + faceCounter ); + + } // each face have to own THREE valid vertices + + + if ( vertexCountPerFace !== 3 ) { + + console.error( 'THREE.STLLoader: Something isn\'t right with the vertices of face number ' + faceCounter ); + + } + + faceCounter ++; + + } + + const start = startVertex; + const count = endVertex - startVertex; + geometry.addGroup( start, count, groupCount ); + groupCount ++; + + } + + geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) ); + geometry.setAttribute( 'normal', new THREE.Float32BufferAttribute( normals, 3 ) ); + return geometry; + + } + + function ensureString( buffer ) { + + if ( typeof buffer !== 'string' ) { + + return THREE.LoaderUtils.decodeText( new Uint8Array( buffer ) ); + + } + + return buffer; + + } + + function ensureBinary( buffer ) { + + if ( typeof buffer === 'string' ) { + + const array_buffer = new Uint8Array( buffer.length ); + + for ( let i = 0; i < buffer.length; i ++ ) { + + array_buffer[ i ] = buffer.charCodeAt( i ) & 0xff; // implicitly assumes little-endian + + } + + return array_buffer.buffer || array_buffer; + + } else { + + return buffer; + + } + + } // start + + + const binData = ensureBinary( data ); + return isBinary( binData ) ? parseBinary( binData ) : parseASCII( ensureString( data ) ); + + } + + } + + THREE.STLLoader = STLLoader; + +} )(); diff --git a/dashboard/static/temp3d/js/three.min.js b/dashboard/static/temp3d/js/three.min.js new file mode 100644 index 0000000..b14f47c --- /dev/null +++ b/dashboard/static/temp3d/js/three.min.js @@ -0,0 +1,6 @@ +/** + * @license + * Copyright 2010-2021 Three.js Authors + * SPDX-License-Identifier: MIT + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).THREE={})}(this,(function(t){"use strict";const e="128",n=100,i=300,r=301,s=302,a=303,o=304,l=306,c=307,h=1e3,u=1001,d=1002,p=1003,m=1004,f=1005,g=1006,v=1007,y=1008,x=1009,_=1012,w=1014,b=1015,M=1016,S=1020,T=1022,E=1023,A=1026,L=1027,R=33776,C=33777,P=33778,D=33779,I=35840,N=35841,B=35842,z=35843,F=37492,O=37496,H=2300,G=2301,U=2302,k=2400,V=2401,W=2402,j=2500,q=2501,X=3e3,Y=3001,Z=3007,J=3002,Q=3004,K=3005,$=3006,tt=7680,et=35044,nt=35048,it="300 es";class rt{addEventListener(t,e){void 0===this._listeners&&(this._listeners={});const n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)}hasEventListener(t,e){if(void 0===this._listeners)return!1;const n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)}removeEventListener(t,e){if(void 0===this._listeners)return;const n=this._listeners[t];if(void 0!==n){const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}dispatchEvent(t){if(void 0===this._listeners)return;const e=this._listeners[t.type];if(void 0!==e){t.target=this;const n=e.slice(0);for(let e=0,i=n.length;e>8&255]+st[t>>16&255]+st[t>>24&255]+"-"+st[255&e]+st[e>>8&255]+"-"+st[e>>16&15|64]+st[e>>24&255]+"-"+st[63&n|128]+st[n>>8&255]+"-"+st[n>>16&255]+st[n>>24&255]+st[255&i]+st[i>>8&255]+st[i>>16&255]+st[i>>24&255]).toUpperCase()}function ht(t,e,n){return Math.max(e,Math.min(n,t))}function ut(t,e){return(t%e+e)%e}function dt(t,e,n){return(1-n)*t+n*e}function pt(t){return 0==(t&t-1)&&0!==t}function mt(t){return Math.pow(2,Math.ceil(Math.log(t)/Math.LN2))}function ft(t){return Math.pow(2,Math.floor(Math.log(t)/Math.LN2))}var gt=Object.freeze({__proto__:null,DEG2RAD:ot,RAD2DEG:lt,generateUUID:ct,clamp:ht,euclideanModulo:ut,mapLinear:function(t,e,n,i,r){return i+(t-e)*(r-i)/(n-e)},inverseLerp:function(t,e,n){return t!==e?(n-t)/(e-t):0},lerp:dt,damp:function(t,e,n,i){return dt(t,e,1-Math.exp(-n*i))},pingpong:function(t,e=1){return e-Math.abs(ut(t,2*e)-e)},smoothstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*(3-2*t)},smootherstep:function(t,e,n){return t<=e?0:t>=n?1:(t=(t-e)/(n-e))*t*t*(t*(6*t-15)+10)},randInt:function(t,e){return t+Math.floor(Math.random()*(e-t+1))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},seededRandom:function(t){return void 0!==t&&(at=t%2147483647),at=16807*at%2147483647,(at-1)/2147483646},degToRad:function(t){return t*ot},radToDeg:function(t){return t*lt},isPowerOfTwo:pt,ceilPowerOfTwo:mt,floorPowerOfTwo:ft,setQuaternionFromProperEuler:function(t,e,n,i,r){const s=Math.cos,a=Math.sin,o=s(n/2),l=a(n/2),c=s((e+i)/2),h=a((e+i)/2),u=s((e-i)/2),d=a((e-i)/2),p=s((i-e)/2),m=a((i-e)/2);switch(r){case"XYX":t.set(o*h,l*u,l*d,o*c);break;case"YZY":t.set(l*d,o*h,l*u,o*c);break;case"ZXZ":t.set(l*u,l*d,o*h,o*c);break;case"XZX":t.set(o*h,l*m,l*p,o*c);break;case"YXY":t.set(l*p,o*h,l*m,o*c);break;case"ZYZ":t.set(l*m,l*p,o*h,o*c);break;default:console.warn("THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order: "+r)}}});class vt{constructor(t=0,e=0){this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t,e){return void 0!==e?(console.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this)}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t,e){return void 0!==e?(console.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this)}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,n=this.y,i=t.elements;return this.x=i[0]*e+i[3]*n+i[6],this.y=i[1]*e+i[4]*n+i[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(t,Math.min(e,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y;return e*e+n*n}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e,n){return void 0!==n&&console.warn("THREE.Vector2: offset has been removed from .fromBufferAttribute()."),this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const n=Math.cos(e),i=Math.sin(e),r=this.x-t.x,s=this.y-t.y;return this.x=r*n-s*i+t.x,this.y=r*i+s*n+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}}vt.prototype.isVector2=!0;class yt{constructor(){this.elements=[1,0,0,0,1,0,0,0,1],arguments.length>0&&console.error("THREE.Matrix3: the constructor no longer reads arguments. use .set() instead.")}set(t,e,n,i,r,s,a,o,l){const c=this.elements;return c[0]=t,c[1]=i,c[2]=a,c[3]=e,c[4]=r,c[5]=o,c[6]=n,c[7]=s,c[8]=l,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],this}extractBasis(t,e,n){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,i=e.elements,r=this.elements,s=n[0],a=n[3],o=n[6],l=n[1],c=n[4],h=n[7],u=n[2],d=n[5],p=n[8],m=i[0],f=i[3],g=i[6],v=i[1],y=i[4],x=i[7],_=i[2],w=i[5],b=i[8];return r[0]=s*m+a*v+o*_,r[3]=s*f+a*y+o*w,r[6]=s*g+a*x+o*b,r[1]=l*m+c*v+h*_,r[4]=l*f+c*y+h*w,r[7]=l*g+c*x+h*b,r[2]=u*m+d*v+p*_,r[5]=u*f+d*y+p*w,r[8]=u*g+d*x+p*b,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8];return e*s*c-e*a*l-n*r*c+n*a*o+i*r*l-i*s*o}invert(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8],h=c*s-a*l,u=a*o-c*r,d=l*r-s*o,p=e*h+n*u+i*d;if(0===p)return this.set(0,0,0,0,0,0,0,0,0);const m=1/p;return t[0]=h*m,t[1]=(i*l-c*n)*m,t[2]=(a*n-i*s)*m,t[3]=u*m,t[4]=(c*e-i*o)*m,t[5]=(i*r-a*e)*m,t[6]=d*m,t[7]=(n*o-l*e)*m,t[8]=(s*e-n*r)*m,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,n,i,r,s,a){const o=Math.cos(r),l=Math.sin(r);return this.set(n*o,n*l,-n*(o*s+l*a)+s+t,-i*l,i*o,-i*(-l*s+o*a)+a+e,0,0,1),this}scale(t,e){const n=this.elements;return n[0]*=t,n[3]*=t,n[6]*=t,n[1]*=e,n[4]*=e,n[7]*=e,this}rotate(t){const e=Math.cos(t),n=Math.sin(t),i=this.elements,r=i[0],s=i[3],a=i[6],o=i[1],l=i[4],c=i[7];return i[0]=e*r+n*o,i[3]=e*s+n*l,i[6]=e*a+n*c,i[1]=-n*r+e*o,i[4]=-n*s+e*l,i[7]=-n*a+e*c,this}translate(t,e){const n=this.elements;return n[0]+=t*n[2],n[3]+=t*n[5],n[6]+=t*n[8],n[1]+=e*n[2],n[4]+=e*n[5],n[7]+=e*n[8],this}equals(t){const e=this.elements,n=t.elements;for(let t=0;t<9;t++)if(e[t]!==n[t])return!1;return!0}fromArray(t,e=0){for(let n=0;n<9;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t}clone(){return(new this.constructor).fromArray(this.elements)}}let xt;yt.prototype.isMatrix3=!0;class _t{static getDataURL(t){if(/^data:/i.test(t.src))return t.src;if("undefined"==typeof HTMLCanvasElement)return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{void 0===xt&&(xt=document.createElementNS("http://www.w3.org/1999/xhtml","canvas")),xt.width=t.width,xt.height=t.height;const n=xt.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=xt}return e.width>2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}}let wt=0;class bt extends rt{constructor(t=bt.DEFAULT_IMAGE,e=bt.DEFAULT_MAPPING,n=1001,i=1001,r=1006,s=1008,a=1023,o=1009,l=1,c=3e3){super(),Object.defineProperty(this,"id",{value:wt++}),this.uuid=ct(),this.name="",this.image=t,this.mipmaps=[],this.mapping=e,this.wrapS=n,this.wrapT=i,this.magFilter=r,this.minFilter=s,this.anisotropy=l,this.format=a,this.internalFormat=null,this.type=o,this.offset=new vt(0,0),this.repeat=new vt(1,1),this.center=new vt(0,0),this.rotation=0,this.matrixAutoUpdate=!0,this.matrix=new yt,this.generateMipmaps=!0,this.premultiplyAlpha=!1,this.flipY=!0,this.unpackAlignment=4,this.encoding=c,this.version=0,this.onUpdate=null}updateMatrix(){this.matrix.setUvTransform(this.offset.x,this.offset.y,this.repeat.x,this.repeat.y,this.rotation,this.center.x,this.center.y)}clone(){return(new this.constructor).copy(this)}copy(t){return this.name=t.name,this.image=t.image,this.mipmaps=t.mipmaps.slice(0),this.mapping=t.mapping,this.wrapS=t.wrapS,this.wrapT=t.wrapT,this.magFilter=t.magFilter,this.minFilter=t.minFilter,this.anisotropy=t.anisotropy,this.format=t.format,this.internalFormat=t.internalFormat,this.type=t.type,this.offset.copy(t.offset),this.repeat.copy(t.repeat),this.center.copy(t.center),this.rotation=t.rotation,this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrix.copy(t.matrix),this.generateMipmaps=t.generateMipmaps,this.premultiplyAlpha=t.premultiplyAlpha,this.flipY=t.flipY,this.unpackAlignment=t.unpackAlignment,this.encoding=t.encoding,this}toJSON(t){const e=void 0===t||"string"==typeof t;if(!e&&void 0!==t.textures[this.uuid])return t.textures[this.uuid];const n={metadata:{version:4.5,type:"Texture",generator:"Texture.toJSON"},uuid:this.uuid,name:this.name,mapping:this.mapping,repeat:[this.repeat.x,this.repeat.y],offset:[this.offset.x,this.offset.y],center:[this.center.x,this.center.y],rotation:this.rotation,wrap:[this.wrapS,this.wrapT],format:this.format,type:this.type,encoding:this.encoding,minFilter:this.minFilter,magFilter:this.magFilter,anisotropy:this.anisotropy,flipY:this.flipY,premultiplyAlpha:this.premultiplyAlpha,unpackAlignment:this.unpackAlignment};if(void 0!==this.image){const i=this.image;if(void 0===i.uuid&&(i.uuid=ct()),!e&&void 0===t.images[i.uuid]){let e;if(Array.isArray(i)){e=[];for(let t=0,n=i.length;t1)switch(this.wrapS){case h:t.x=t.x-Math.floor(t.x);break;case u:t.x=t.x<0?0:1;break;case d:1===Math.abs(Math.floor(t.x)%2)?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x)}if(t.y<0||t.y>1)switch(this.wrapT){case h:t.y=t.y-Math.floor(t.y);break;case u:t.y=t.y<0?0:1;break;case d:1===Math.abs(Math.floor(t.y)%2)?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y)}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){!0===t&&this.version++}}function Mt(t){return"undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap?_t.getDataURL(t):t.data?{data:Array.prototype.slice.call(t.data),width:t.width,height:t.height,type:t.data.constructor.name}:(console.warn("THREE.Texture: Unable to serialize Texture."),{})}bt.DEFAULT_IMAGE=void 0,bt.DEFAULT_MAPPING=i,bt.prototype.isTexture=!0;class St{constructor(t=0,e=0,n=0,i=1){this.x=t,this.y=e,this.z=n,this.w=i}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,n,i){return this.x=t,this.y=e,this.z=n,this.w=i,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=void 0!==t.w?t.w:1,this}add(t,e){return void 0!==e?(console.warn("THREE.Vector4: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this)}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t,e){return void 0!==e?(console.warn("THREE.Vector4: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this)}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,n=this.y,i=this.z,r=this.w,s=t.elements;return this.x=s[0]*e+s[4]*n+s[8]*i+s[12]*r,this.y=s[1]*e+s[5]*n+s[9]*i+s[13]*r,this.z=s[2]*e+s[6]*n+s[10]*i+s[14]*r,this.w=s[3]*e+s[7]*n+s[11]*i+s[15]*r,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,n,i,r;const s=.01,a=.1,o=t.elements,l=o[0],c=o[4],h=o[8],u=o[1],d=o[5],p=o[9],m=o[2],f=o[6],g=o[10];if(Math.abs(c-u)o&&t>v?tv?o=0?1:-1,i=1-e*e;if(i>Number.EPSILON){const r=Math.sqrt(i),s=Math.atan2(r,e*n);t=Math.sin(t*s)/r,a=Math.sin(a*s)/r}const r=a*n;if(o=o*t+u*r,l=l*t+d*r,c=c*t+p*r,h=h*t+m*r,t===1-a){const t=1/Math.sqrt(o*o+l*l+c*c+h*h);o*=t,l*=t,c*=t,h*=t}}t[e]=o,t[e+1]=l,t[e+2]=c,t[e+3]=h}static multiplyQuaternionsFlat(t,e,n,i,r,s){const a=n[i],o=n[i+1],l=n[i+2],c=n[i+3],h=r[s],u=r[s+1],d=r[s+2],p=r[s+3];return t[e]=a*p+c*h+o*d-l*u,t[e+1]=o*p+c*u+l*h-a*d,t[e+2]=l*p+c*d+a*u-o*h,t[e+3]=c*p-a*h-o*u-l*d,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,n,i){return this._x=t,this._y=e,this._z=n,this._w=i,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e){if(!t||!t.isEuler)throw new Error("THREE.Quaternion: .setFromEuler() now expects an Euler rotation rather than a Vector3 and order.");const n=t._x,i=t._y,r=t._z,s=t._order,a=Math.cos,o=Math.sin,l=a(n/2),c=a(i/2),h=a(r/2),u=o(n/2),d=o(i/2),p=o(r/2);switch(s){case"XYZ":this._x=u*c*h+l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h-u*d*p;break;case"YXZ":this._x=u*c*h+l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h+u*d*p;break;case"ZXY":this._x=u*c*h-l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h-u*d*p;break;case"ZYX":this._x=u*c*h-l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h+u*d*p;break;case"YZX":this._x=u*c*h+l*d*p,this._y=l*d*h+u*c*p,this._z=l*c*p-u*d*h,this._w=l*c*h-u*d*p;break;case"XZY":this._x=u*c*h-l*d*p,this._y=l*d*h-u*c*p,this._z=l*c*p+u*d*h,this._w=l*c*h+u*d*p;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+s)}return!1!==e&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const n=e/2,i=Math.sin(n);return this._x=t.x*i,this._y=t.y*i,this._z=t.z*i,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,n=e[0],i=e[4],r=e[8],s=e[1],a=e[5],o=e[9],l=e[2],c=e[6],h=e[10],u=n+a+h;if(u>0){const t=.5/Math.sqrt(u+1);this._w=.25/t,this._x=(c-o)*t,this._y=(r-l)*t,this._z=(s-i)*t}else if(n>a&&n>h){const t=2*Math.sqrt(1+n-a-h);this._w=(c-o)/t,this._x=.25*t,this._y=(i+s)/t,this._z=(r+l)/t}else if(a>h){const t=2*Math.sqrt(1+a-n-h);this._w=(r-l)/t,this._x=(i+s)/t,this._y=.25*t,this._z=(o+c)/t}else{const t=2*Math.sqrt(1+h-n-a);this._w=(s-i)/t,this._x=(r+l)/t,this._y=(o+c)/t,this._z=.25*t}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let n=t.dot(e)+1;return nMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=n):(this._x=0,this._y=-t.z,this._z=t.y,this._w=n)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=n),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(ht(this.dot(t),-1,1)))}rotateTowards(t,e){const n=this.angleTo(t);if(0===n)return this;const i=Math.min(1,e/n);return this.slerp(t,i),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return 0===t?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t,e){return void 0!==e?(console.warn("THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."),this.multiplyQuaternions(t,e)):this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const n=t._x,i=t._y,r=t._z,s=t._w,a=e._x,o=e._y,l=e._z,c=e._w;return this._x=n*c+s*a+i*l-r*o,this._y=i*c+s*o+r*a-n*l,this._z=r*c+s*l+n*o-i*a,this._w=s*c-n*a-i*o-r*l,this._onChangeCallback(),this}slerp(t,e){if(0===e)return this;if(1===e)return this.copy(t);const n=this._x,i=this._y,r=this._z,s=this._w;let a=s*t._w+n*t._x+i*t._y+r*t._z;if(a<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,a=-a):this.copy(t),a>=1)return this._w=s,this._x=n,this._y=i,this._z=r,this;const o=1-a*a;if(o<=Number.EPSILON){const t=1-e;return this._w=t*s+e*this._w,this._x=t*n+e*this._x,this._y=t*i+e*this._y,this._z=t*r+e*this._z,this.normalize(),this._onChangeCallback(),this}const l=Math.sqrt(o),c=Math.atan2(l,a),h=Math.sin((1-e)*c)/l,u=Math.sin(e*c)/l;return this._w=s*h+this._w*u,this._x=n*h+this._x*u,this._y=i*h+this._y*u,this._z=r*h+this._z*u,this._onChangeCallback(),this}slerpQuaternions(t,e,n){this.copy(t).slerp(e,n)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}}At.prototype.isQuaternion=!0;class Lt{constructor(t=0,e=0,n=0){this.x=t,this.y=e,this.z=n}set(t,e,n){return void 0===n&&(n=this.z),this.x=t,this.y=e,this.z=n,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t,e){return void 0!==e?(console.warn("THREE.Vector3: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this.z+=t.z,this)}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t,e){return void 0!==e?(console.warn("THREE.Vector3: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this.z-=t.z,this)}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t,e){return void 0!==e?(console.warn("THREE.Vector3: .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead."),this.multiplyVectors(t,e)):(this.x*=t.x,this.y*=t.y,this.z*=t.z,this)}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return t&&t.isEuler||console.error("THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order."),this.applyQuaternion(Ct.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(Ct.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,n=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[3]*n+r[6]*i,this.y=r[1]*e+r[4]*n+r[7]*i,this.z=r[2]*e+r[5]*n+r[8]*i,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,n=this.y,i=this.z,r=t.elements,s=1/(r[3]*e+r[7]*n+r[11]*i+r[15]);return this.x=(r[0]*e+r[4]*n+r[8]*i+r[12])*s,this.y=(r[1]*e+r[5]*n+r[9]*i+r[13])*s,this.z=(r[2]*e+r[6]*n+r[10]*i+r[14])*s,this}applyQuaternion(t){const e=this.x,n=this.y,i=this.z,r=t.x,s=t.y,a=t.z,o=t.w,l=o*e+s*i-a*n,c=o*n+a*e-r*i,h=o*i+r*n-s*e,u=-r*e-s*n-a*i;return this.x=l*o+u*-r+c*-a-h*-s,this.y=c*o+u*-s+h*-r-l*-a,this.z=h*o+u*-a+l*-s-c*-r,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,n=this.y,i=this.z,r=t.elements;return this.x=r[0]*e+r[4]*n+r[8]*i,this.y=r[1]*e+r[5]*n+r[9]*i,this.z=r[2]*e+r[6]*n+r[10]*i,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=Math.max(t.x,Math.min(e.x,this.x)),this.y=Math.max(t.y,Math.min(e.y,this.y)),this.z=Math.max(t.z,Math.min(e.z,this.z)),this}clampScalar(t,e){return this.x=Math.max(t,Math.min(e,this.x)),this.y=Math.max(t,Math.min(e,this.y)),this.z=Math.max(t,Math.min(e,this.z)),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Math.max(t,Math.min(e,n)))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this.z=this.z<0?Math.ceil(this.z):Math.floor(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this}cross(t,e){return void 0!==e?(console.warn("THREE.Vector3: .cross() now only accepts one argument. Use .crossVectors( a, b ) instead."),this.crossVectors(t,e)):this.crossVectors(this,t)}crossVectors(t,e){const n=t.x,i=t.y,r=t.z,s=e.x,a=e.y,o=e.z;return this.x=i*o-r*a,this.y=r*s-n*o,this.z=n*a-i*s,this}projectOnVector(t){const e=t.lengthSq();if(0===e)return this.set(0,0,0);const n=t.dot(this)/e;return this.copy(t).multiplyScalar(n)}projectOnPlane(t){return Rt.copy(this).projectOnVector(t),this.sub(Rt)}reflect(t){return this.sub(Rt.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(0===e)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(ht(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y,i=this.z-t.z;return e*e+n*n+i*i}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,n){const i=Math.sin(e)*t;return this.x=i*Math.sin(n),this.y=Math.cos(e)*t,this.z=i*Math.cos(n),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,n){return this.x=t*Math.sin(e),this.y=n,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),n=this.setFromMatrixColumn(t,1).length(),i=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=n,this.z=i,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,4*e)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,3*e)}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e,n){return void 0!==n&&console.warn("THREE.Vector3: offset has been removed from .fromBufferAttribute()."),this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}}Lt.prototype.isVector3=!0;const Rt=new Lt,Ct=new At;class Pt{constructor(t=new Lt(1/0,1/0,1/0),e=new Lt(-1/0,-1/0,-1/0)){this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){let e=1/0,n=1/0,i=1/0,r=-1/0,s=-1/0,a=-1/0;for(let o=0,l=t.length;or&&(r=l),c>s&&(s=c),h>a&&(a=h)}return this.min.set(e,n,i),this.max.set(r,s,a),this}setFromBufferAttribute(t){let e=1/0,n=1/0,i=1/0,r=-1/0,s=-1/0,a=-1/0;for(let o=0,l=t.count;or&&(r=l),c>s&&(s=c),h>a&&(a=h)}return this.min.set(e,n,i),this.max.set(r,s,a),this}setFromPoints(t){this.makeEmpty();for(let e=0,n=t.length;ethis.max.x||t.ythis.max.y||t.zthis.max.z)}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return void 0===e&&(console.warn("THREE.Box3: .getParameter() target is now required"),e=new Lt),e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return!(t.max.xthis.max.x||t.max.ythis.max.y||t.max.zthis.max.z)}intersectsSphere(t){return this.clampPoint(t.center,It),It.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,n;return t.normal.x>0?(e=t.normal.x*this.min.x,n=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,n=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,n+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,n+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,n+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,n+=t.normal.z*this.min.z),e<=-t.constant&&n>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(Ut),kt.subVectors(this.max,Ut),Bt.subVectors(t.a,Ut),zt.subVectors(t.b,Ut),Ft.subVectors(t.c,Ut),Ot.subVectors(zt,Bt),Ht.subVectors(Ft,zt),Gt.subVectors(Bt,Ft);let e=[0,-Ot.z,Ot.y,0,-Ht.z,Ht.y,0,-Gt.z,Gt.y,Ot.z,0,-Ot.x,Ht.z,0,-Ht.x,Gt.z,0,-Gt.x,-Ot.y,Ot.x,0,-Ht.y,Ht.x,0,-Gt.y,Gt.x,0];return!!jt(e,Bt,zt,Ft,kt)&&(e=[1,0,0,0,1,0,0,0,1],!!jt(e,Bt,zt,Ft,kt)&&(Vt.crossVectors(Ot,Ht),e=[Vt.x,Vt.y,Vt.z],jt(e,Bt,zt,Ft,kt)))}clampPoint(t,e){return void 0===e&&(console.warn("THREE.Box3: .clampPoint() target is now required"),e=new Lt),e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return It.copy(t).clamp(this.min,this.max).sub(t).length()}getBoundingSphere(t){return void 0===t&&console.error("THREE.Box3: .getBoundingSphere() target is now required"),this.getCenter(t.center),t.radius=.5*this.getSize(It).length(),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()||(Dt[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),Dt[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),Dt[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),Dt[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),Dt[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),Dt[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),Dt[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),Dt[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(Dt)),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}Pt.prototype.isBox3=!0;const Dt=[new Lt,new Lt,new Lt,new Lt,new Lt,new Lt,new Lt,new Lt],It=new Lt,Nt=new Pt,Bt=new Lt,zt=new Lt,Ft=new Lt,Ot=new Lt,Ht=new Lt,Gt=new Lt,Ut=new Lt,kt=new Lt,Vt=new Lt,Wt=new Lt;function jt(t,e,n,i,r){for(let s=0,a=t.length-3;s<=a;s+=3){Wt.fromArray(t,s);const a=r.x*Math.abs(Wt.x)+r.y*Math.abs(Wt.y)+r.z*Math.abs(Wt.z),o=e.dot(Wt),l=n.dot(Wt),c=i.dot(Wt);if(Math.max(-Math.max(o,l,c),Math.min(o,l,c))>a)return!1}return!0}const qt=new Pt,Xt=new Lt,Yt=new Lt,Zt=new Lt;class Jt{constructor(t=new Lt,e=-1){this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const n=this.center;void 0!==e?n.copy(e):qt.setFromPoints(t).getCenter(n);let i=0;for(let e=0,r=t.length;ethis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return void 0===t&&(console.warn("THREE.Sphere: .getBoundingBox() target is now required"),t=new Pt),this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){Zt.subVectors(t,this.center);const e=Zt.lengthSq();if(e>this.radius*this.radius){const t=Math.sqrt(e),n=.5*(t-this.radius);this.center.add(Zt.multiplyScalar(n/t)),this.radius+=n}return this}union(t){return Yt.subVectors(t.center,this.center).normalize().multiplyScalar(t.radius),this.expandByPoint(Xt.copy(t.center).add(Yt)),this.expandByPoint(Xt.copy(t.center).sub(Yt)),this}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return(new this.constructor).copy(this)}}const Qt=new Lt,Kt=new Lt,$t=new Lt,te=new Lt,ee=new Lt,ne=new Lt,ie=new Lt;class re{constructor(t=new Lt,e=new Lt(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return void 0===e&&(console.warn("THREE.Ray: .at() target is now required"),e=new Lt),e.copy(this.direction).multiplyScalar(t).add(this.origin)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,Qt)),this}closestPointToPoint(t,e){void 0===e&&(console.warn("THREE.Ray: .closestPointToPoint() target is now required"),e=new Lt),e.subVectors(t,this.origin);const n=e.dot(this.direction);return n<0?e.copy(this.origin):e.copy(this.direction).multiplyScalar(n).add(this.origin)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=Qt.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(Qt.copy(this.direction).multiplyScalar(e).add(this.origin),Qt.distanceToSquared(t))}distanceSqToSegment(t,e,n,i){Kt.copy(t).add(e).multiplyScalar(.5),$t.copy(e).sub(t).normalize(),te.copy(this.origin).sub(Kt);const r=.5*t.distanceTo(e),s=-this.direction.dot($t),a=te.dot(this.direction),o=-te.dot($t),l=te.lengthSq(),c=Math.abs(1-s*s);let h,u,d,p;if(c>0)if(h=s*o-a,u=s*a-o,p=r*c,h>=0)if(u>=-p)if(u<=p){const t=1/c;h*=t,u*=t,d=h*(h+s*u+2*a)+u*(s*h+u+2*o)+l}else u=r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;else u=-r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;else u<=-p?(h=Math.max(0,-(-s*r+a)),u=h>0?-r:Math.min(Math.max(-r,-o),r),d=-h*h+u*(u+2*o)+l):u<=p?(h=0,u=Math.min(Math.max(-r,-o),r),d=u*(u+2*o)+l):(h=Math.max(0,-(s*r+a)),u=h>0?r:Math.min(Math.max(-r,-o),r),d=-h*h+u*(u+2*o)+l);else u=s>0?-r:r,h=Math.max(0,-(s*u+a)),d=-h*h+u*(u+2*o)+l;return n&&n.copy(this.direction).multiplyScalar(h).add(this.origin),i&&i.copy($t).multiplyScalar(u).add(Kt),d}intersectSphere(t,e){Qt.subVectors(t.center,this.origin);const n=Qt.dot(this.direction),i=Qt.dot(Qt)-n*n,r=t.radius*t.radius;if(i>r)return null;const s=Math.sqrt(r-i),a=n-s,o=n+s;return a<0&&o<0?null:a<0?this.at(o,e):this.at(a,e)}intersectsSphere(t){return this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(0===e)return 0===t.distanceToPoint(this.origin)?0:null;const n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null}intersectPlane(t,e){const n=this.distanceToPlane(t);return null===n?null:this.at(n,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);if(0===e)return!0;return t.normal.dot(this.direction)*e<0}intersectBox(t,e){let n,i,r,s,a,o;const l=1/this.direction.x,c=1/this.direction.y,h=1/this.direction.z,u=this.origin;return l>=0?(n=(t.min.x-u.x)*l,i=(t.max.x-u.x)*l):(n=(t.max.x-u.x)*l,i=(t.min.x-u.x)*l),c>=0?(r=(t.min.y-u.y)*c,s=(t.max.y-u.y)*c):(r=(t.max.y-u.y)*c,s=(t.min.y-u.y)*c),n>s||r>i?null:((r>n||n!=n)&&(n=r),(s=0?(a=(t.min.z-u.z)*h,o=(t.max.z-u.z)*h):(a=(t.max.z-u.z)*h,o=(t.min.z-u.z)*h),n>o||a>i?null:((a>n||n!=n)&&(n=a),(o=0?n:i,e)))}intersectsBox(t){return null!==this.intersectBox(t,Qt)}intersectTriangle(t,e,n,i,r){ee.subVectors(e,t),ne.subVectors(n,t),ie.crossVectors(ee,ne);let s,a=this.direction.dot(ie);if(a>0){if(i)return null;s=1}else{if(!(a<0))return null;s=-1,a=-a}te.subVectors(this.origin,t);const o=s*this.direction.dot(ne.crossVectors(te,ne));if(o<0)return null;const l=s*this.direction.dot(ee.cross(te));if(l<0)return null;if(o+l>a)return null;const c=-s*te.dot(ie);return c<0?null:this.at(c/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return(new this.constructor).copy(this)}}class se{constructor(){this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],arguments.length>0&&console.error("THREE.Matrix4: the constructor no longer reads arguments. use .set() instead.")}set(t,e,n,i,r,s,a,o,l,c,h,u,d,p,m,f){const g=this.elements;return g[0]=t,g[4]=e,g[8]=n,g[12]=i,g[1]=r,g[5]=s,g[9]=a,g[13]=o,g[2]=l,g[6]=c,g[10]=h,g[14]=u,g[3]=d,g[7]=p,g[11]=m,g[15]=f,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return(new se).fromArray(this.elements)}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],e[9]=n[9],e[10]=n[10],e[11]=n[11],e[12]=n[12],e[13]=n[13],e[14]=n[14],e[15]=n[15],this}copyPosition(t){const e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,n){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,n=t.elements,i=1/ae.setFromMatrixColumn(t,0).length(),r=1/ae.setFromMatrixColumn(t,1).length(),s=1/ae.setFromMatrixColumn(t,2).length();return e[0]=n[0]*i,e[1]=n[1]*i,e[2]=n[2]*i,e[3]=0,e[4]=n[4]*r,e[5]=n[5]*r,e[6]=n[6]*r,e[7]=0,e[8]=n[8]*s,e[9]=n[9]*s,e[10]=n[10]*s,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){t&&t.isEuler||console.error("THREE.Matrix4: .makeRotationFromEuler() now expects a Euler rotation rather than a Vector3 and order.");const e=this.elements,n=t.x,i=t.y,r=t.z,s=Math.cos(n),a=Math.sin(n),o=Math.cos(i),l=Math.sin(i),c=Math.cos(r),h=Math.sin(r);if("XYZ"===t.order){const t=s*c,n=s*h,i=a*c,r=a*h;e[0]=o*c,e[4]=-o*h,e[8]=l,e[1]=n+i*l,e[5]=t-r*l,e[9]=-a*o,e[2]=r-t*l,e[6]=i+n*l,e[10]=s*o}else if("YXZ"===t.order){const t=o*c,n=o*h,i=l*c,r=l*h;e[0]=t+r*a,e[4]=i*a-n,e[8]=s*l,e[1]=s*h,e[5]=s*c,e[9]=-a,e[2]=n*a-i,e[6]=r+t*a,e[10]=s*o}else if("ZXY"===t.order){const t=o*c,n=o*h,i=l*c,r=l*h;e[0]=t-r*a,e[4]=-s*h,e[8]=i+n*a,e[1]=n+i*a,e[5]=s*c,e[9]=r-t*a,e[2]=-s*l,e[6]=a,e[10]=s*o}else if("ZYX"===t.order){const t=s*c,n=s*h,i=a*c,r=a*h;e[0]=o*c,e[4]=i*l-n,e[8]=t*l+r,e[1]=o*h,e[5]=r*l+t,e[9]=n*l-i,e[2]=-l,e[6]=a*o,e[10]=s*o}else if("YZX"===t.order){const t=s*o,n=s*l,i=a*o,r=a*l;e[0]=o*c,e[4]=r-t*h,e[8]=i*h+n,e[1]=h,e[5]=s*c,e[9]=-a*c,e[2]=-l*c,e[6]=n*h+i,e[10]=t-r*h}else if("XZY"===t.order){const t=s*o,n=s*l,i=a*o,r=a*l;e[0]=o*c,e[4]=-h,e[8]=l*c,e[1]=t*h+r,e[5]=s*c,e[9]=n*h-i,e[2]=i*h-n,e[6]=a*c,e[10]=r*h+t}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(le,t,ce)}lookAt(t,e,n){const i=this.elements;return de.subVectors(t,e),0===de.lengthSq()&&(de.z=1),de.normalize(),he.crossVectors(n,de),0===he.lengthSq()&&(1===Math.abs(n.z)?de.x+=1e-4:de.z+=1e-4,de.normalize(),he.crossVectors(n,de)),he.normalize(),ue.crossVectors(de,he),i[0]=he.x,i[4]=ue.x,i[8]=de.x,i[1]=he.y,i[5]=ue.y,i[9]=de.y,i[2]=he.z,i[6]=ue.z,i[10]=de.z,this}multiply(t,e){return void 0!==e?(console.warn("THREE.Matrix4: .multiply() now only accepts one argument. Use .multiplyMatrices( a, b ) instead."),this.multiplyMatrices(t,e)):this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,i=e.elements,r=this.elements,s=n[0],a=n[4],o=n[8],l=n[12],c=n[1],h=n[5],u=n[9],d=n[13],p=n[2],m=n[6],f=n[10],g=n[14],v=n[3],y=n[7],x=n[11],_=n[15],w=i[0],b=i[4],M=i[8],S=i[12],T=i[1],E=i[5],A=i[9],L=i[13],R=i[2],C=i[6],P=i[10],D=i[14],I=i[3],N=i[7],B=i[11],z=i[15];return r[0]=s*w+a*T+o*R+l*I,r[4]=s*b+a*E+o*C+l*N,r[8]=s*M+a*A+o*P+l*B,r[12]=s*S+a*L+o*D+l*z,r[1]=c*w+h*T+u*R+d*I,r[5]=c*b+h*E+u*C+d*N,r[9]=c*M+h*A+u*P+d*B,r[13]=c*S+h*L+u*D+d*z,r[2]=p*w+m*T+f*R+g*I,r[6]=p*b+m*E+f*C+g*N,r[10]=p*M+m*A+f*P+g*B,r[14]=p*S+m*L+f*D+g*z,r[3]=v*w+y*T+x*R+_*I,r[7]=v*b+y*E+x*C+_*N,r[11]=v*M+y*A+x*P+_*B,r[15]=v*S+y*L+x*D+_*z,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[4],i=t[8],r=t[12],s=t[1],a=t[5],o=t[9],l=t[13],c=t[2],h=t[6],u=t[10],d=t[14];return t[3]*(+r*o*h-i*l*h-r*a*u+n*l*u+i*a*d-n*o*d)+t[7]*(+e*o*d-e*l*u+r*s*u-i*s*d+i*l*c-r*o*c)+t[11]*(+e*l*h-e*a*d-r*s*h+n*s*d+r*a*c-n*l*c)+t[15]*(-i*a*c-e*o*h+e*a*u+i*s*h-n*s*u+n*o*c)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,n){const i=this.elements;return t.isVector3?(i[12]=t.x,i[13]=t.y,i[14]=t.z):(i[12]=t,i[13]=e,i[14]=n),this}invert(){const t=this.elements,e=t[0],n=t[1],i=t[2],r=t[3],s=t[4],a=t[5],o=t[6],l=t[7],c=t[8],h=t[9],u=t[10],d=t[11],p=t[12],m=t[13],f=t[14],g=t[15],v=h*f*l-m*u*l+m*o*d-a*f*d-h*o*g+a*u*g,y=p*u*l-c*f*l-p*o*d+s*f*d+c*o*g-s*u*g,x=c*m*l-p*h*l+p*a*d-s*m*d-c*a*g+s*h*g,_=p*h*o-c*m*o-p*a*u+s*m*u+c*a*f-s*h*f,w=e*v+n*y+i*x+r*_;if(0===w)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const b=1/w;return t[0]=v*b,t[1]=(m*u*r-h*f*r-m*i*d+n*f*d+h*i*g-n*u*g)*b,t[2]=(a*f*r-m*o*r+m*i*l-n*f*l-a*i*g+n*o*g)*b,t[3]=(h*o*r-a*u*r-h*i*l+n*u*l+a*i*d-n*o*d)*b,t[4]=y*b,t[5]=(c*f*r-p*u*r+p*i*d-e*f*d-c*i*g+e*u*g)*b,t[6]=(p*o*r-s*f*r-p*i*l+e*f*l+s*i*g-e*o*g)*b,t[7]=(s*u*r-c*o*r+c*i*l-e*u*l-s*i*d+e*o*d)*b,t[8]=x*b,t[9]=(p*h*r-c*m*r-p*n*d+e*m*d+c*n*g-e*h*g)*b,t[10]=(s*m*r-p*a*r+p*n*l-e*m*l-s*n*g+e*a*g)*b,t[11]=(c*a*r-s*h*r-c*n*l+e*h*l+s*n*d-e*a*d)*b,t[12]=_*b,t[13]=(c*m*i-p*h*i+p*n*u-e*m*u-c*n*f+e*h*f)*b,t[14]=(p*a*i-s*m*i-p*n*o+e*m*o+s*n*f-e*a*f)*b,t[15]=(s*h*i-c*a*i+c*n*o-e*h*o-s*n*u+e*a*u)*b,this}scale(t){const e=this.elements,n=t.x,i=t.y,r=t.z;return e[0]*=n,e[4]*=i,e[8]*=r,e[1]*=n,e[5]*=i,e[9]*=r,e[2]*=n,e[6]*=i,e[10]*=r,e[3]*=n,e[7]*=i,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],i=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,n,i))}makeTranslation(t,e,n){return this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const n=Math.cos(e),i=Math.sin(e),r=1-n,s=t.x,a=t.y,o=t.z,l=r*s,c=r*a;return this.set(l*s+n,l*a-i*o,l*o+i*a,0,l*a+i*o,c*a+n,c*o-i*s,0,l*o-i*a,c*o+i*s,r*o*o+n,0,0,0,0,1),this}makeScale(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this}makeShear(t,e,n){return this.set(1,e,n,0,t,1,n,0,t,e,1,0,0,0,0,1),this}compose(t,e,n){const i=this.elements,r=e._x,s=e._y,a=e._z,o=e._w,l=r+r,c=s+s,h=a+a,u=r*l,d=r*c,p=r*h,m=s*c,f=s*h,g=a*h,v=o*l,y=o*c,x=o*h,_=n.x,w=n.y,b=n.z;return i[0]=(1-(m+g))*_,i[1]=(d+x)*_,i[2]=(p-y)*_,i[3]=0,i[4]=(d-x)*w,i[5]=(1-(u+g))*w,i[6]=(f+v)*w,i[7]=0,i[8]=(p+y)*b,i[9]=(f-v)*b,i[10]=(1-(u+m))*b,i[11]=0,i[12]=t.x,i[13]=t.y,i[14]=t.z,i[15]=1,this}decompose(t,e,n){const i=this.elements;let r=ae.set(i[0],i[1],i[2]).length();const s=ae.set(i[4],i[5],i[6]).length(),a=ae.set(i[8],i[9],i[10]).length();this.determinant()<0&&(r=-r),t.x=i[12],t.y=i[13],t.z=i[14],oe.copy(this);const o=1/r,l=1/s,c=1/a;return oe.elements[0]*=o,oe.elements[1]*=o,oe.elements[2]*=o,oe.elements[4]*=l,oe.elements[5]*=l,oe.elements[6]*=l,oe.elements[8]*=c,oe.elements[9]*=c,oe.elements[10]*=c,e.setFromRotationMatrix(oe),n.x=r,n.y=s,n.z=a,this}makePerspective(t,e,n,i,r,s){void 0===s&&console.warn("THREE.Matrix4: .makePerspective() has been redefined and has a new signature. Please check the docs.");const a=this.elements,o=2*r/(e-t),l=2*r/(n-i),c=(e+t)/(e-t),h=(n+i)/(n-i),u=-(s+r)/(s-r),d=-2*s*r/(s-r);return a[0]=o,a[4]=0,a[8]=c,a[12]=0,a[1]=0,a[5]=l,a[9]=h,a[13]=0,a[2]=0,a[6]=0,a[10]=u,a[14]=d,a[3]=0,a[7]=0,a[11]=-1,a[15]=0,this}makeOrthographic(t,e,n,i,r,s){const a=this.elements,o=1/(e-t),l=1/(n-i),c=1/(s-r),h=(e+t)*o,u=(n+i)*l,d=(s+r)*c;return a[0]=2*o,a[4]=0,a[8]=0,a[12]=-h,a[1]=0,a[5]=2*l,a[9]=0,a[13]=-u,a[2]=0,a[6]=0,a[10]=-2*c,a[14]=-d,a[3]=0,a[7]=0,a[11]=0,a[15]=1,this}equals(t){const e=this.elements,n=t.elements;for(let t=0;t<16;t++)if(e[t]!==n[t])return!1;return!0}fromArray(t,e=0){for(let n=0;n<16;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t}}se.prototype.isMatrix4=!0;const ae=new Lt,oe=new se,le=new Lt(0,0,0),ce=new Lt(1,1,1),he=new Lt,ue=new Lt,de=new Lt,pe=new se,me=new At;class fe{constructor(t=0,e=0,n=0,i=fe.DefaultOrder){this._x=t,this._y=e,this._z=n,this._order=i}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,n,i){return this._x=t,this._y=e,this._z=n,this._order=i||this._order,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e,n){const i=t.elements,r=i[0],s=i[4],a=i[8],o=i[1],l=i[5],c=i[9],h=i[2],u=i[6],d=i[10];switch(e=e||this._order){case"XYZ":this._y=Math.asin(ht(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(-c,d),this._z=Math.atan2(-s,r)):(this._x=Math.atan2(u,l),this._z=0);break;case"YXZ":this._x=Math.asin(-ht(c,-1,1)),Math.abs(c)<.9999999?(this._y=Math.atan2(a,d),this._z=Math.atan2(o,l)):(this._y=Math.atan2(-h,r),this._z=0);break;case"ZXY":this._x=Math.asin(ht(u,-1,1)),Math.abs(u)<.9999999?(this._y=Math.atan2(-h,d),this._z=Math.atan2(-s,l)):(this._y=0,this._z=Math.atan2(o,r));break;case"ZYX":this._y=Math.asin(-ht(h,-1,1)),Math.abs(h)<.9999999?(this._x=Math.atan2(u,d),this._z=Math.atan2(o,r)):(this._x=0,this._z=Math.atan2(-s,l));break;case"YZX":this._z=Math.asin(ht(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-c,l),this._y=Math.atan2(-h,r)):(this._x=0,this._y=Math.atan2(a,d));break;case"XZY":this._z=Math.asin(-ht(s,-1,1)),Math.abs(s)<.9999999?(this._x=Math.atan2(u,l),this._y=Math.atan2(a,r)):(this._x=Math.atan2(-c,d),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,!1!==n&&this._onChangeCallback(),this}setFromQuaternion(t,e,n){return pe.makeRotationFromQuaternion(t),this.setFromRotationMatrix(pe,e,n)}setFromVector3(t,e){return this.set(t.x,t.y,t.z,e||this._order)}reorder(t){return me.setFromEuler(this),this.setFromQuaternion(me,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],void 0!==t[3]&&(this._order=t[3]),this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t}toVector3(t){return t?t.set(this._x,this._y,this._z):new Lt(this._x,this._y,this._z)}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}}fe.prototype.isEuler=!0,fe.DefaultOrder="XYZ",fe.RotationOrders=["XYZ","YZX","ZXY","XZY","YXZ","ZYX"];class ge{constructor(){this.mask=1}set(t){this.mask=1<1){for(let t=0;t1){for(let t=0;t0){i.children=[];for(let e=0;e0){i.animations=[];for(let e=0;e0&&(n.geometries=e),i.length>0&&(n.materials=i),r.length>0&&(n.textures=r),a.length>0&&(n.images=a),o.length>0&&(n.shapes=o),l.length>0&&(n.skeletons=l),c.length>0&&(n.animations=c)}return n.object=i,n;function s(t){const e=[];for(const n in t){const i=t[n];delete i.metadata,e.push(i)}return e}}clone(t){return(new this.constructor).copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.userData=JSON.parse(JSON.stringify(t.userData)),!0===e)for(let e=0;e1?null:e.copy(n).multiplyScalar(r).add(t.start)}intersectsLine(t){const e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return e<0&&n>0||n<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return void 0===t&&(console.warn("THREE.Plane: .coplanarPoint() target is now required"),t=new Lt),t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const n=e||Ie.getNormalMatrix(t),i=this.coplanarPoint(Pe).applyMatrix4(t),r=this.normal.applyMatrix3(n).normalize();return this.constant=-i.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return(new this.constructor).copy(this)}}Ne.prototype.isPlane=!0;const Be=new Lt,ze=new Lt,Fe=new Lt,Oe=new Lt,He=new Lt,Ge=new Lt,Ue=new Lt,ke=new Lt,Ve=new Lt,We=new Lt;class je{constructor(t=new Lt,e=new Lt,n=new Lt){this.a=t,this.b=e,this.c=n}static getNormal(t,e,n,i){void 0===i&&(console.warn("THREE.Triangle: .getNormal() target is now required"),i=new Lt),i.subVectors(n,e),Be.subVectors(t,e),i.cross(Be);const r=i.lengthSq();return r>0?i.multiplyScalar(1/Math.sqrt(r)):i.set(0,0,0)}static getBarycoord(t,e,n,i,r){Be.subVectors(i,e),ze.subVectors(n,e),Fe.subVectors(t,e);const s=Be.dot(Be),a=Be.dot(ze),o=Be.dot(Fe),l=ze.dot(ze),c=ze.dot(Fe),h=s*l-a*a;if(void 0===r&&(console.warn("THREE.Triangle: .getBarycoord() target is now required"),r=new Lt),0===h)return r.set(-2,-1,-1);const u=1/h,d=(l*o-a*c)*u,p=(s*c-a*o)*u;return r.set(1-d-p,p,d)}static containsPoint(t,e,n,i){return this.getBarycoord(t,e,n,i,Oe),Oe.x>=0&&Oe.y>=0&&Oe.x+Oe.y<=1}static getUV(t,e,n,i,r,s,a,o){return this.getBarycoord(t,e,n,i,Oe),o.set(0,0),o.addScaledVector(r,Oe.x),o.addScaledVector(s,Oe.y),o.addScaledVector(a,Oe.z),o}static isFrontFacing(t,e,n,i){return Be.subVectors(n,e),ze.subVectors(t,e),Be.cross(ze).dot(i)<0}set(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this}setFromPointsAndIndices(t,e,n,i){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[i]),this}clone(){return(new this.constructor).copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return Be.subVectors(this.c,this.b),ze.subVectors(this.a,this.b),.5*Be.cross(ze).length()}getMidpoint(t){return void 0===t&&(console.warn("THREE.Triangle: .getMidpoint() target is now required"),t=new Lt),t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return je.getNormal(this.a,this.b,this.c,t)}getPlane(t){return void 0===t&&(console.warn("THREE.Triangle: .getPlane() target is now required"),t=new Ne),t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return je.getBarycoord(t,this.a,this.b,this.c,e)}getUV(t,e,n,i,r){return je.getUV(t,this.a,this.b,this.c,e,n,i,r)}containsPoint(t){return je.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return je.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){void 0===e&&(console.warn("THREE.Triangle: .closestPointToPoint() target is now required"),e=new Lt);const n=this.a,i=this.b,r=this.c;let s,a;He.subVectors(i,n),Ge.subVectors(r,n),ke.subVectors(t,n);const o=He.dot(ke),l=Ge.dot(ke);if(o<=0&&l<=0)return e.copy(n);Ve.subVectors(t,i);const c=He.dot(Ve),h=Ge.dot(Ve);if(c>=0&&h<=c)return e.copy(i);const u=o*h-c*l;if(u<=0&&o>=0&&c<=0)return s=o/(o-c),e.copy(n).addScaledVector(He,s);We.subVectors(t,r);const d=He.dot(We),p=Ge.dot(We);if(p>=0&&d<=p)return e.copy(r);const m=d*l-o*p;if(m<=0&&l>=0&&p<=0)return a=l/(l-p),e.copy(n).addScaledVector(Ge,a);const f=c*p-d*h;if(f<=0&&h-c>=0&&d-p>=0)return Ue.subVectors(r,i),a=(h-c)/(h-c+(d-p)),e.copy(i).addScaledVector(Ue,a);const g=1/(f+m+u);return s=m*g,a=u*g,e.copy(n).addScaledVector(He,s).addScaledVector(Ge,a)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}let qe=0;function Xe(){Object.defineProperty(this,"id",{value:qe++}),this.uuid=ct(),this.name="",this.type="Material",this.fog=!0,this.blending=1,this.side=0,this.vertexColors=!1,this.opacity=1,this.transparent=!1,this.blendSrc=204,this.blendDst=205,this.blendEquation=n,this.blendSrcAlpha=null,this.blendDstAlpha=null,this.blendEquationAlpha=null,this.depthFunc=3,this.depthTest=!0,this.depthWrite=!0,this.stencilWriteMask=255,this.stencilFunc=519,this.stencilRef=0,this.stencilFuncMask=255,this.stencilFail=tt,this.stencilZFail=tt,this.stencilZPass=tt,this.stencilWrite=!1,this.clippingPlanes=null,this.clipIntersection=!1,this.clipShadows=!1,this.shadowSide=null,this.colorWrite=!0,this.precision=null,this.polygonOffset=!1,this.polygonOffsetFactor=0,this.polygonOffsetUnits=0,this.dithering=!1,this.alphaTest=0,this.alphaToCoverage=!1,this.premultipliedAlpha=!1,this.visible=!0,this.toneMapped=!0,this.userData={},this.version=0}Xe.prototype=Object.assign(Object.create(rt.prototype),{constructor:Xe,isMaterial:!0,onBuild:function(){},onBeforeCompile:function(){},customProgramCacheKey:function(){return this.onBeforeCompile.toString()},setValues:function(t){if(void 0!==t)for(const e in t){const n=t[e];if(void 0===n){console.warn("THREE.Material: '"+e+"' parameter is undefined.");continue}if("shading"===e){console.warn("THREE."+this.type+": .shading has been removed. Use the boolean .flatShading instead."),this.flatShading=1===n;continue}const i=this[e];void 0!==i?i&&i.isColor?i.set(n):i&&i.isVector3&&n&&n.isVector3?i.copy(n):this[e]=n:console.warn("THREE."+this.type+": '"+e+"' is not a property of this material.")}},toJSON:function(t){const e=void 0===t||"string"==typeof t;e&&(t={textures:{},images:{}});const n={metadata:{version:4.5,type:"Material",generator:"Material.toJSON"}};function i(t){const e=[];for(const n in t){const i=t[n];delete i.metadata,e.push(i)}return e}if(n.uuid=this.uuid,n.type=this.type,""!==this.name&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),void 0!==this.roughness&&(n.roughness=this.roughness),void 0!==this.metalness&&(n.metalness=this.metalness),this.sheen&&this.sheen.isColor&&(n.sheen=this.sheen.getHex()),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity&&1!==this.emissiveIntensity&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),void 0!==this.shininess&&(n.shininess=this.shininess),void 0!==this.clearcoat&&(n.clearcoat=this.clearcoat),void 0!==this.clearcoatRoughness&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(t).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(t).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(t).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(t).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(t).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(t).uuid,void 0!==this.combine&&(n.combine=this.combine)),void 0!==this.envMapIntensity&&(n.envMapIntensity=this.envMapIntensity),void 0!==this.reflectivity&&(n.reflectivity=this.reflectivity),void 0!==this.refractionRatio&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(t).uuid),void 0!==this.size&&(n.size=this.size),null!==this.shadowSide&&(n.shadowSide=this.shadowSide),void 0!==this.sizeAttenuation&&(n.sizeAttenuation=this.sizeAttenuation),1!==this.blending&&(n.blending=this.blending),0!==this.side&&(n.side=this.side),this.vertexColors&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),!0===this.transparent&&(n.transparent=this.transparent),n.depthFunc=this.depthFunc,n.depthTest=this.depthTest,n.depthWrite=this.depthWrite,n.colorWrite=this.colorWrite,n.stencilWrite=this.stencilWrite,n.stencilWriteMask=this.stencilWriteMask,n.stencilFunc=this.stencilFunc,n.stencilRef=this.stencilRef,n.stencilFuncMask=this.stencilFuncMask,n.stencilFail=this.stencilFail,n.stencilZFail=this.stencilZFail,n.stencilZPass=this.stencilZPass,this.rotation&&0!==this.rotation&&(n.rotation=this.rotation),!0===this.polygonOffset&&(n.polygonOffset=!0),0!==this.polygonOffsetFactor&&(n.polygonOffsetFactor=this.polygonOffsetFactor),0!==this.polygonOffsetUnits&&(n.polygonOffsetUnits=this.polygonOffsetUnits),this.linewidth&&1!==this.linewidth&&(n.linewidth=this.linewidth),void 0!==this.dashSize&&(n.dashSize=this.dashSize),void 0!==this.gapSize&&(n.gapSize=this.gapSize),void 0!==this.scale&&(n.scale=this.scale),!0===this.dithering&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),!0===this.alphaToCoverage&&(n.alphaToCoverage=this.alphaToCoverage),!0===this.premultipliedAlpha&&(n.premultipliedAlpha=this.premultipliedAlpha),!0===this.wireframe&&(n.wireframe=this.wireframe),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),"round"!==this.wireframeLinecap&&(n.wireframeLinecap=this.wireframeLinecap),"round"!==this.wireframeLinejoin&&(n.wireframeLinejoin=this.wireframeLinejoin),!0===this.morphTargets&&(n.morphTargets=!0),!0===this.morphNormals&&(n.morphNormals=!0),!0===this.skinning&&(n.skinning=!0),!0===this.flatShading&&(n.flatShading=this.flatShading),!1===this.visible&&(n.visible=!1),!1===this.toneMapped&&(n.toneMapped=!1),"{}"!==JSON.stringify(this.userData)&&(n.userData=this.userData),e){const e=i(t.textures),r=i(t.images);e.length>0&&(n.textures=e),r.length>0&&(n.images=r)}return n},clone:function(){return(new this.constructor).copy(this)},copy:function(t){this.name=t.name,this.fog=t.fog,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let n=null;if(null!==e){const t=e.length;n=new Array(t);for(let i=0;i!==t;++i)n[i]=e[i].clone()}return this.clippingPlanes=n,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this},dispose:function(){this.dispatchEvent({type:"dispose"})}}),Object.defineProperty(Xe.prototype,"needsUpdate",{set:function(t){!0===t&&this.version++}});const Ye={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Ze={h:0,s:0,l:0},Je={h:0,s:0,l:0};function Qe(t,e,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?t+6*(e-t)*n:n<.5?e:n<2/3?t+6*(e-t)*(2/3-n):t}function Ke(t){return t<.04045?.0773993808*t:Math.pow(.9478672986*t+.0521327014,2.4)}function $e(t){return t<.0031308?12.92*t:1.055*Math.pow(t,.41666)-.055}class tn{constructor(t,e,n){return void 0===e&&void 0===n?this.set(t):this.setRGB(t,e,n)}set(t){return t&&t.isColor?this.copy(t):"number"==typeof t?this.setHex(t):"string"==typeof t&&this.setStyle(t),this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,this}setRGB(t,e,n){return this.r=t,this.g=e,this.b=n,this}setHSL(t,e,n){if(t=ut(t,1),e=ht(e,0,1),n=ht(n,0,1),0===e)this.r=this.g=this.b=n;else{const i=n<=.5?n*(1+e):n+e-n*e,r=2*n-i;this.r=Qe(r,i,t+1/3),this.g=Qe(r,i,t),this.b=Qe(r,i,t-1/3)}return this}setStyle(t){function e(e){void 0!==e&&parseFloat(e)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let n;if(n=/^((?:rgb|hsl)a?)\(([^\)]*)\)/.exec(t)){let t;const i=n[1],r=n[2];switch(i){case"rgb":case"rgba":if(t=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(r))return this.r=Math.min(255,parseInt(t[1],10))/255,this.g=Math.min(255,parseInt(t[2],10))/255,this.b=Math.min(255,parseInt(t[3],10))/255,e(t[4]),this;if(t=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(r))return this.r=Math.min(100,parseInt(t[1],10))/100,this.g=Math.min(100,parseInt(t[2],10))/100,this.b=Math.min(100,parseInt(t[3],10))/100,e(t[4]),this;break;case"hsl":case"hsla":if(t=/^\s*(\d*\.?\d+)\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(r)){const n=parseFloat(t[1])/360,i=parseInt(t[2],10)/100,r=parseInt(t[3],10)/100;return e(t[4]),this.setHSL(n,i,r)}}}else if(n=/^\#([A-Fa-f\d]+)$/.exec(t)){const t=n[1],e=t.length;if(3===e)return this.r=parseInt(t.charAt(0)+t.charAt(0),16)/255,this.g=parseInt(t.charAt(1)+t.charAt(1),16)/255,this.b=parseInt(t.charAt(2)+t.charAt(2),16)/255,this;if(6===e)return this.r=parseInt(t.charAt(0)+t.charAt(1),16)/255,this.g=parseInt(t.charAt(2)+t.charAt(3),16)/255,this.b=parseInt(t.charAt(4)+t.charAt(5),16)/255,this}return t&&t.length>0?this.setColorName(t):this}setColorName(t){const e=Ye[t.toLowerCase()];return void 0!==e?this.setHex(e):console.warn("THREE.Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copyGammaToLinear(t,e=2){return this.r=Math.pow(t.r,e),this.g=Math.pow(t.g,e),this.b=Math.pow(t.b,e),this}copyLinearToGamma(t,e=2){const n=e>0?1/e:1;return this.r=Math.pow(t.r,n),this.g=Math.pow(t.g,n),this.b=Math.pow(t.b,n),this}convertGammaToLinear(t){return this.copyGammaToLinear(this,t),this}convertLinearToGamma(t){return this.copyLinearToGamma(this,t),this}copySRGBToLinear(t){return this.r=Ke(t.r),this.g=Ke(t.g),this.b=Ke(t.b),this}copyLinearToSRGB(t){return this.r=$e(t.r),this.g=$e(t.g),this.b=$e(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(){return 255*this.r<<16^255*this.g<<8^255*this.b<<0}getHexString(){return("000000"+this.getHex().toString(16)).slice(-6)}getHSL(t){void 0===t&&(console.warn("THREE.Color: .getHSL() target is now required"),t={h:0,s:0,l:0});const e=this.r,n=this.g,i=this.b,r=Math.max(e,n,i),s=Math.min(e,n,i);let a,o;const l=(s+r)/2;if(s===r)a=0,o=0;else{const t=r-s;switch(o=l<=.5?t/(r+s):t/(2-r-s),r){case e:a=(n-i)/t+(ne&&(e=t[n]);return e}const vn={Int8Array:Int8Array,Uint8Array:Uint8Array,Uint8ClampedArray:Uint8ClampedArray,Int16Array:Int16Array,Uint16Array:Uint16Array,Int32Array:Int32Array,Uint32Array:Uint32Array,Float32Array:Float32Array,Float64Array:Float64Array};function yn(t,e){return new vn[t](e)}let xn=0;const _n=new se,wn=new Ce,bn=new Lt,Mn=new Pt,Sn=new Pt,Tn=new Lt;class En extends rt{constructor(){super(),Object.defineProperty(this,"id",{value:xn++}),this.uuid=ct(),this.name="",this.type="BufferGeometry",this.index=null,this.attributes={},this.morphAttributes={},this.morphTargetsRelative=!1,this.groups=[],this.boundingBox=null,this.boundingSphere=null,this.drawRange={start:0,count:1/0},this.userData={}}getIndex(){return this.index}setIndex(t){return Array.isArray(t)?this.index=new(gn(t)>65535?dn:hn)(t,1):this.index=t,this}getAttribute(t){return this.attributes[t]}setAttribute(t,e){return this.attributes[t]=e,this}deleteAttribute(t){return delete this.attributes[t],this}hasAttribute(t){return void 0!==this.attributes[t]}addGroup(t,e,n=0){this.groups.push({start:t,count:e,materialIndex:n})}clearGroups(){this.groups=[]}setDrawRange(t,e){this.drawRange.start=t,this.drawRange.count=e}applyMatrix4(t){const e=this.attributes.position;void 0!==e&&(e.applyMatrix4(t),e.needsUpdate=!0);const n=this.attributes.normal;if(void 0!==n){const e=(new yt).getNormalMatrix(t);n.applyNormalMatrix(e),n.needsUpdate=!0}const i=this.attributes.tangent;return void 0!==i&&(i.transformDirection(t),i.needsUpdate=!0),null!==this.boundingBox&&this.computeBoundingBox(),null!==this.boundingSphere&&this.computeBoundingSphere(),this}rotateX(t){return _n.makeRotationX(t),this.applyMatrix4(_n),this}rotateY(t){return _n.makeRotationY(t),this.applyMatrix4(_n),this}rotateZ(t){return _n.makeRotationZ(t),this.applyMatrix4(_n),this}translate(t,e,n){return _n.makeTranslation(t,e,n),this.applyMatrix4(_n),this}scale(t,e,n){return _n.makeScale(t,e,n),this.applyMatrix4(_n),this}lookAt(t){return wn.lookAt(t),wn.updateMatrix(),this.applyMatrix4(wn.matrix),this}center(){return this.computeBoundingBox(),this.boundingBox.getCenter(bn).negate(),this.translate(bn.x,bn.y,bn.z),this}setFromPoints(t){const e=[];for(let n=0,i=t.length;n0&&(t.userData=this.userData),void 0!==this.parameters){const e=this.parameters;for(const n in e)void 0!==e[n]&&(t[n]=e[n]);return t}t.data={attributes:{}};const e=this.index;null!==e&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const n=this.attributes;for(const e in n){const i=n[e];t.data.attributes[e]=i.toJSON(t.data)}const i={};let r=!1;for(const e in this.morphAttributes){const n=this.morphAttributes[e],s=[];for(let e=0,i=n.length;e0&&(i[e]=s,r=!0)}r&&(t.data.morphAttributes=i,t.data.morphTargetsRelative=this.morphTargetsRelative);const s=this.groups;s.length>0&&(t.data.groups=JSON.parse(JSON.stringify(s)));const a=this.boundingSphere;return null!==a&&(t.data.boundingSphere={center:a.center.toArray(),radius:a.radius}),t}clone(){return(new En).copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const n=t.index;null!==n&&this.setIndex(n.clone(e));const i=t.attributes;for(const t in i){const n=i[t];this.setAttribute(t,n.clone(e))}const r=t.morphAttributes;for(const t in r){const n=[],i=r[t];for(let t=0,r=i.length;t0){const t=e[n[0]];if(void 0!==t){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,n=t.length;e0&&console.error("THREE.Mesh.updateMorphTargets() no longer supports THREE.Geometry. Use THREE.BufferGeometry instead.")}}raycast(t,e){const n=this.geometry,i=this.material,r=this.matrixWorld;if(void 0===i)return;if(null===n.boundingSphere&&n.computeBoundingSphere(),Rn.copy(n.boundingSphere),Rn.applyMatrix4(r),!1===t.ray.intersectsSphere(Rn))return;if(An.copy(r).invert(),Ln.copy(t.ray).applyMatrix4(An),null!==n.boundingBox&&!1===Ln.intersectsBox(n.boundingBox))return;let s;if(n.isBufferGeometry){const r=n.index,a=n.attributes.position,o=n.morphAttributes.position,l=n.morphTargetsRelative,c=n.attributes.uv,h=n.attributes.uv2,u=n.groups,d=n.drawRange;if(null!==r)if(Array.isArray(i))for(let n=0,p=u.length;nn.far?null:{distance:c,point:Vn.clone(),object:t}}(t,e,n,i,Cn,Pn,Dn,kn);if(p){o&&(Hn.fromBufferAttribute(o,c),Gn.fromBufferAttribute(o,h),Un.fromBufferAttribute(o,u),p.uv=je.getUV(kn,Cn,Pn,Dn,Hn,Gn,Un,new vt)),l&&(Hn.fromBufferAttribute(l,c),Gn.fromBufferAttribute(l,h),Un.fromBufferAttribute(l,u),p.uv2=je.getUV(kn,Cn,Pn,Dn,Hn,Gn,Un,new vt));const t={a:c,b:h,c:u,normal:new Lt,materialIndex:0};je.getNormal(Cn,Pn,Dn,t.normal),p.face=t}return p}Wn.prototype.isMesh=!0;class qn extends En{constructor(t=1,e=1,n=1,i=1,r=1,s=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:i,heightSegments:r,depthSegments:s};const a=this;i=Math.floor(i),r=Math.floor(r),s=Math.floor(s);const o=[],l=[],c=[],h=[];let u=0,d=0;function p(t,e,n,i,r,s,p,m,f,g,v){const y=s/f,x=p/g,_=s/2,w=p/2,b=m/2,M=f+1,S=g+1;let T=0,E=0;const A=new Lt;for(let s=0;s0?1:-1,c.push(A.x,A.y,A.z),h.push(o/f),h.push(1-s/g),T+=1}}for(let t=0;t0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader;const n={};for(const t in this.extensions)!0===this.extensions[t]&&(n[t]=!0);return Object.keys(n).length>0&&(e.extensions=n),e}}Jn.prototype.isShaderMaterial=!0;class Qn extends Ce{constructor(){super(),this.type="Camera",this.matrixWorldInverse=new se,this.projectionMatrix=new se,this.projectionMatrixInverse=new se}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this}getWorldDirection(t){void 0===t&&(console.warn("THREE.Camera: .getWorldDirection() target is now required"),t=new Lt),this.updateWorldMatrix(!0,!1);const e=this.matrixWorld.elements;return t.set(-e[8],-e[9],-e[10]).normalize()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return(new this.constructor).copy(this)}}Qn.prototype.isCamera=!0;class Kn extends Qn{constructor(t=50,e=1,n=.1,i=2e3){super(),this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=n,this.far=i,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=null===t.view?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=2*lt*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(.5*ot*this.fov);return.5*this.getFilmHeight()/t}getEffectiveFOV(){return 2*lt*Math.atan(Math.tan(.5*ot*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}setViewOffset(t,e,n,i,r,s){this.aspect=t/e,null===this.view&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=i,this.view.width=r,this.view.height=s,this.updateProjectionMatrix()}clearViewOffset(){null!==this.view&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(.5*ot*this.fov)/this.zoom,n=2*e,i=this.aspect*n,r=-.5*i;const s=this.view;if(null!==this.view&&this.view.enabled){const t=s.fullWidth,a=s.fullHeight;r+=s.offsetX*i/t,e-=s.offsetY*n/a,i*=s.width/t,n*=s.height/a}const a=this.filmOffset;0!==a&&(r+=t*a/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+i,e,e-n,t,this.far),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,null!==this.view&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}Kn.prototype.isPerspectiveCamera=!0;const $n=90;class ti extends Ce{constructor(t,e,n){if(super(),this.type="CubeCamera",!0!==n.isWebGLCubeRenderTarget)return void console.error("THREE.CubeCamera: The constructor now expects an instance of WebGLCubeRenderTarget as third parameter.");this.renderTarget=n;const i=new Kn($n,1,t,e);i.layers=this.layers,i.up.set(0,-1,0),i.lookAt(new Lt(1,0,0)),this.add(i);const r=new Kn($n,1,t,e);r.layers=this.layers,r.up.set(0,-1,0),r.lookAt(new Lt(-1,0,0)),this.add(r);const s=new Kn($n,1,t,e);s.layers=this.layers,s.up.set(0,0,1),s.lookAt(new Lt(0,1,0)),this.add(s);const a=new Kn($n,1,t,e);a.layers=this.layers,a.up.set(0,0,-1),a.lookAt(new Lt(0,-1,0)),this.add(a);const o=new Kn($n,1,t,e);o.layers=this.layers,o.up.set(0,-1,0),o.lookAt(new Lt(0,0,1)),this.add(o);const l=new Kn($n,1,t,e);l.layers=this.layers,l.up.set(0,-1,0),l.lookAt(new Lt(0,0,-1)),this.add(l)}update(t,e){null===this.parent&&this.updateMatrixWorld();const n=this.renderTarget,[i,r,s,a,o,l]=this.children,c=t.xr.enabled,h=t.getRenderTarget();t.xr.enabled=!1;const u=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,t.setRenderTarget(n,0),t.render(e,i),t.setRenderTarget(n,1),t.render(e,r),t.setRenderTarget(n,2),t.render(e,s),t.setRenderTarget(n,3),t.render(e,a),t.setRenderTarget(n,4),t.render(e,o),n.texture.generateMipmaps=u,t.setRenderTarget(n,5),t.render(e,l),t.setRenderTarget(h),t.xr.enabled=c}}class ei extends bt{constructor(t,e,n,i,s,a,o,l,c,h){super(t=void 0!==t?t:[],e=void 0!==e?e:r,n,i,s,a,o=void 0!==o?o:T,l,c,h),this._needsFlipEnvMap=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}ei.prototype.isCubeTexture=!0;class ni extends Tt{constructor(t,e,n){Number.isInteger(e)&&(console.warn("THREE.WebGLCubeRenderTarget: constructor signature is now WebGLCubeRenderTarget( size, options )"),e=n),super(t,t,e),e=e||{},this.texture=new ei(void 0,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.encoding),this.texture.generateMipmaps=void 0!==e.generateMipmaps&&e.generateMipmaps,this.texture.minFilter=void 0!==e.minFilter?e.minFilter:g,this.texture._needsFlipEnvMap=!1}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.format=E,this.texture.encoding=e.encoding,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:"\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\tvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\n\t\t\t\t\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n\n\t\t\t\t}\n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvWorldDirection = transformDirection( position, modelMatrix );\n\n\t\t\t\t\t#include \n\t\t\t\t\t#include \n\n\t\t\t\t}\n\t\t\t",fragmentShader:"\n\n\t\t\t\tuniform sampler2D tEquirect;\n\n\t\t\t\tvarying vec3 vWorldDirection;\n\n\t\t\t\t#include \n\n\t\t\t\tvoid main() {\n\n\t\t\t\t\tvec3 direction = normalize( vWorldDirection );\n\n\t\t\t\t\tvec2 sampleUV = equirectUv( direction );\n\n\t\t\t\t\tgl_FragColor = texture2D( tEquirect, sampleUV );\n\n\t\t\t\t}\n\t\t\t"},i=new qn(5,5,5),r=new Jn({name:"CubemapFromEquirect",uniforms:Xn(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:1,blending:0});r.uniforms.tEquirect.value=e;const s=new Wn(i,r),a=e.minFilter;e.minFilter===y&&(e.minFilter=g);return new ti(1,10,this).update(t,s),e.minFilter=a,s.geometry.dispose(),s.material.dispose(),this}clear(t,e,n,i){const r=t.getRenderTarget();for(let r=0;r<6;r++)t.setRenderTarget(this,r),t.clear(e,n,i);t.setRenderTarget(r)}}ni.prototype.isWebGLCubeRenderTarget=!0;class ii extends bt{constructor(t,e,n,i,r,s,a,o,l,c,h,u){super(null,s,a,o,l,c,i,r,h,u),this.image={data:t||null,width:e||1,height:n||1},this.magFilter=void 0!==l?l:p,this.minFilter=void 0!==c?c:p,this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1,this.needsUpdate=!0}}ii.prototype.isDataTexture=!0;const ri=new Jt,si=new Lt;class ai{constructor(t=new Ne,e=new Ne,n=new Ne,i=new Ne,r=new Ne,s=new Ne){this.planes=[t,e,n,i,r,s]}set(t,e,n,i,r,s){const a=this.planes;return a[0].copy(t),a[1].copy(e),a[2].copy(n),a[3].copy(i),a[4].copy(r),a[5].copy(s),this}copy(t){const e=this.planes;for(let n=0;n<6;n++)e[n].copy(t.planes[n]);return this}setFromProjectionMatrix(t){const e=this.planes,n=t.elements,i=n[0],r=n[1],s=n[2],a=n[3],o=n[4],l=n[5],c=n[6],h=n[7],u=n[8],d=n[9],p=n[10],m=n[11],f=n[12],g=n[13],v=n[14],y=n[15];return e[0].setComponents(a-i,h-o,m-u,y-f).normalize(),e[1].setComponents(a+i,h+o,m+u,y+f).normalize(),e[2].setComponents(a+r,h+l,m+d,y+g).normalize(),e[3].setComponents(a-r,h-l,m-d,y-g).normalize(),e[4].setComponents(a-s,h-c,m-p,y-v).normalize(),e[5].setComponents(a+s,h+c,m+p,y+v).normalize(),this}intersectsObject(t){const e=t.geometry;return null===e.boundingSphere&&e.computeBoundingSphere(),ri.copy(e.boundingSphere).applyMatrix4(t.matrixWorld),this.intersectsSphere(ri)}intersectsSprite(t){return ri.center.set(0,0,0),ri.radius=.7071067811865476,ri.applyMatrix4(t.matrixWorld),this.intersectsSphere(ri)}intersectsSphere(t){const e=this.planes,n=t.center,i=-t.radius;for(let t=0;t<6;t++){if(e[t].distanceToPoint(n)0?t.max.x:t.min.x,si.y=i.normal.y>0?t.max.y:t.min.y,si.z=i.normal.z>0?t.max.z:t.min.z,i.distanceToPoint(si)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let n=0;n<6;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0}clone(){return(new this.constructor).copy(this)}}function oi(){let t=null,e=!1,n=null,i=null;function r(e,s){n(e,s),i=t.requestAnimationFrame(r)}return{start:function(){!0!==e&&null!==n&&(i=t.requestAnimationFrame(r),e=!0)},stop:function(){t.cancelAnimationFrame(i),e=!1},setAnimationLoop:function(t){n=t},setContext:function(e){t=e}}}function li(t,e){const n=e.isWebGL2,i=new WeakMap;return{get:function(t){return t.isInterleavedBufferAttribute&&(t=t.data),i.get(t)},remove:function(e){e.isInterleavedBufferAttribute&&(e=e.data);const n=i.get(e);n&&(t.deleteBuffer(n.buffer),i.delete(e))},update:function(e,r){if(e.isGLBufferAttribute){const t=i.get(e);return void((!t||t.version 0.0 ) {\n\t\tdistanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) );\n\t}\n\treturn distanceFalloff;\n#else\n\tif( cutoffDistance > 0.0 && decayExponent > 0.0 ) {\n\t\treturn pow( saturate( -lightDistance / cutoffDistance + 1.0 ), decayExponent );\n\t}\n\treturn 1.0;\n#endif\n}\nvec3 BRDF_Diffuse_Lambert( const in vec3 diffuseColor ) {\n\treturn RECIPROCAL_PI * diffuseColor;\n}\nvec3 F_Schlick( const in vec3 specularColor, const in float dotLH ) {\n\tfloat fresnel = exp2( ( -5.55473 * dotLH - 6.98316 ) * dotLH );\n\treturn ( 1.0 - specularColor ) * fresnel + specularColor;\n}\nvec3 F_Schlick_RoughnessDependent( const in vec3 F0, const in float dotNV, const in float roughness ) {\n\tfloat fresnel = exp2( ( -5.55473 * dotNV - 6.98316 ) * dotNV );\n\tvec3 Fr = max( vec3( 1.0 - roughness ), F0 ) - F0;\n\treturn Fr * fresnel + F0;\n}\nfloat G_GGX_Smith( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gl = dotNL + sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\tfloat gv = dotNV + sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\treturn 1.0 / ( gl * gv );\n}\nfloat G_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {\n\tfloat a2 = pow2( alpha );\n\tfloat gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) );\n\tfloat gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) );\n\treturn 0.5 / max( gv + gl, EPSILON );\n}\nfloat D_GGX( const in float alpha, const in float dotNH ) {\n\tfloat a2 = pow2( alpha );\n\tfloat denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0;\n\treturn RECIPROCAL_PI * a2 / pow2( denom );\n}\nvec3 BRDF_Specular_GGX( const in IncidentLight incidentLight, const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float roughness ) {\n\tfloat alpha = pow2( roughness );\n\tvec3 halfDir = normalize( incidentLight.direction + viewDir );\n\tfloat dotNL = saturate( dot( normal, incidentLight.direction ) );\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tfloat dotNH = saturate( dot( normal, halfDir ) );\n\tfloat dotLH = saturate( dot( incidentLight.direction, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, dotLH );\n\tfloat G = G_GGX_SmithCorrelated( alpha, dotNL, dotNV );\n\tfloat D = D_GGX( alpha, dotNH );\n\treturn F * ( G * D );\n}\nvec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) {\n\tconst float LUT_SIZE = 64.0;\n\tconst float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;\n\tconst float LUT_BIAS = 0.5 / LUT_SIZE;\n\tfloat dotNV = saturate( dot( N, V ) );\n\tvec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) );\n\tuv = uv * LUT_SCALE + LUT_BIAS;\n\treturn uv;\n}\nfloat LTC_ClippedSphereFormFactor( const in vec3 f ) {\n\tfloat l = length( f );\n\treturn max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 );\n}\nvec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) {\n\tfloat x = dot( v1, v2 );\n\tfloat y = abs( x );\n\tfloat a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y;\n\tfloat b = 3.4175940 + ( 4.1616724 + y ) * y;\n\tfloat v = a / b;\n\tfloat theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v;\n\treturn cross( v1, v2 ) * theta_sintheta;\n}\nvec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) {\n\tvec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ];\n\tvec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ];\n\tvec3 lightNormal = cross( v1, v2 );\n\tif( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 );\n\tvec3 T1, T2;\n\tT1 = normalize( V - N * dot( V, N ) );\n\tT2 = - cross( N, T1 );\n\tmat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) );\n\tvec3 coords[ 4 ];\n\tcoords[ 0 ] = mat * ( rectCoords[ 0 ] - P );\n\tcoords[ 1 ] = mat * ( rectCoords[ 1 ] - P );\n\tcoords[ 2 ] = mat * ( rectCoords[ 2 ] - P );\n\tcoords[ 3 ] = mat * ( rectCoords[ 3 ] - P );\n\tcoords[ 0 ] = normalize( coords[ 0 ] );\n\tcoords[ 1 ] = normalize( coords[ 1 ] );\n\tcoords[ 2 ] = normalize( coords[ 2 ] );\n\tcoords[ 3 ] = normalize( coords[ 3 ] );\n\tvec3 vectorFormFactor = vec3( 0.0 );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] );\n\tvectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] );\n\tfloat result = LTC_ClippedSphereFormFactor( vectorFormFactor );\n\treturn vec3( result );\n}\nvec3 BRDF_Specular_GGX_Environment( const in vec3 viewDir, const in vec3 normal, const in vec3 specularColor, const in float roughness ) {\n\tfloat dotNV = saturate( dot( normal, viewDir ) );\n\tvec2 brdf = integrateSpecularBRDF( dotNV, roughness );\n\treturn specularColor * brdf.x + brdf.y;\n}\nvoid BRDF_Specular_Multiscattering_Environment( const in GeometricContext geometry, const in vec3 specularColor, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {\n\tfloat dotNV = saturate( dot( geometry.normal, geometry.viewDir ) );\n\tvec3 F = F_Schlick_RoughnessDependent( specularColor, dotNV, roughness );\n\tvec2 brdf = integrateSpecularBRDF( dotNV, roughness );\n\tvec3 FssEss = F * brdf.x + brdf.y;\n\tfloat Ess = brdf.x + brdf.y;\n\tfloat Ems = 1.0 - Ess;\n\tvec3 Favg = specularColor + ( 1.0 - specularColor ) * 0.047619;\tvec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );\n\tsingleScatter += FssEss;\n\tmultiScatter += Fms * Ems;\n}\nfloat G_BlinnPhong_Implicit( ) {\n\treturn 0.25;\n}\nfloat D_BlinnPhong( const in float shininess, const in float dotNH ) {\n\treturn RECIPROCAL_PI * ( shininess * 0.5 + 1.0 ) * pow( dotNH, shininess );\n}\nvec3 BRDF_Specular_BlinnPhong( const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float shininess ) {\n\tvec3 halfDir = normalize( incidentLight.direction + geometry.viewDir );\n\tfloat dotNH = saturate( dot( geometry.normal, halfDir ) );\n\tfloat dotLH = saturate( dot( incidentLight.direction, halfDir ) );\n\tvec3 F = F_Schlick( specularColor, dotLH );\n\tfloat G = G_BlinnPhong_Implicit( );\n\tfloat D = D_BlinnPhong( shininess, dotNH );\n\treturn F * ( G * D );\n}\nfloat GGXRoughnessToBlinnExponent( const in float ggxRoughness ) {\n\treturn ( 2.0 / pow2( ggxRoughness + 0.0001 ) - 2.0 );\n}\nfloat BlinnExponentToGGXRoughness( const in float blinnExponent ) {\n\treturn sqrt( 2.0 / ( blinnExponent + 2.0 ) );\n}\n#if defined( USE_SHEEN )\nfloat D_Charlie(float roughness, float NoH) {\n\tfloat invAlpha = 1.0 / roughness;\n\tfloat cos2h = NoH * NoH;\n\tfloat sin2h = max(1.0 - cos2h, 0.0078125);\treturn (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI);\n}\nfloat V_Neubelt(float NoV, float NoL) {\n\treturn saturate(1.0 / (4.0 * (NoL + NoV - NoL * NoV)));\n}\nvec3 BRDF_Specular_Sheen( const in float roughness, const in vec3 L, const in GeometricContext geometry, vec3 specularColor ) {\n\tvec3 N = geometry.normal;\n\tvec3 V = geometry.viewDir;\n\tvec3 H = normalize( V + L );\n\tfloat dotNH = saturate( dot( N, H ) );\n\treturn specularColor * D_Charlie( roughness, dotNH ) * V_Neubelt( dot(N, V), dot(N, L) );\n}\n#endif",bumpmap_pars_fragment:"#ifdef USE_BUMPMAP\n\tuniform sampler2D bumpMap;\n\tuniform float bumpScale;\n\tvec2 dHdxy_fwd() {\n\t\tvec2 dSTdx = dFdx( vUv );\n\t\tvec2 dSTdy = dFdy( vUv );\n\t\tfloat Hll = bumpScale * texture2D( bumpMap, vUv ).x;\n\t\tfloat dBx = bumpScale * texture2D( bumpMap, vUv + dSTdx ).x - Hll;\n\t\tfloat dBy = bumpScale * texture2D( bumpMap, vUv + dSTdy ).x - Hll;\n\t\treturn vec2( dBx, dBy );\n\t}\n\tvec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {\n\t\tvec3 vSigmaX = vec3( dFdx( surf_pos.x ), dFdx( surf_pos.y ), dFdx( surf_pos.z ) );\n\t\tvec3 vSigmaY = vec3( dFdy( surf_pos.x ), dFdy( surf_pos.y ), dFdy( surf_pos.z ) );\n\t\tvec3 vN = surf_norm;\n\t\tvec3 R1 = cross( vSigmaY, vN );\n\t\tvec3 R2 = cross( vN, vSigmaX );\n\t\tfloat fDet = dot( vSigmaX, R1 ) * faceDirection;\n\t\tvec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n\t\treturn normalize( abs( fDet ) * surf_norm - vGrad );\n\t}\n#endif",clipping_planes_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvec4 plane;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) {\n\t\tplane = clippingPlanes[ i ];\n\t\tif ( dot( vClipPosition, plane.xyz ) > plane.w ) discard;\n\t}\n\t#pragma unroll_loop_end\n\t#if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES\n\t\tbool clipped = true;\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) {\n\t\t\tplane = clippingPlanes[ i ];\n\t\t\tclipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped;\n\t\t}\n\t\t#pragma unroll_loop_end\n\t\tif ( clipped ) discard;\n\t#endif\n#endif",clipping_planes_pars_fragment:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n\tuniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ];\n#endif",clipping_planes_pars_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvarying vec3 vClipPosition;\n#endif",clipping_planes_vertex:"#if NUM_CLIPPING_PLANES > 0\n\tvClipPosition = - mvPosition.xyz;\n#endif",color_fragment:"#if defined( USE_COLOR_ALPHA )\n\tdiffuseColor *= vColor;\n#elif defined( USE_COLOR )\n\tdiffuseColor.rgb *= vColor;\n#endif",color_pars_fragment:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR )\n\tvarying vec3 vColor;\n#endif",color_pars_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvarying vec4 vColor;\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvarying vec3 vColor;\n#endif",color_vertex:"#if defined( USE_COLOR_ALPHA )\n\tvColor = vec4( 1.0 );\n#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR )\n\tvColor = vec3( 1.0 );\n#endif\n#ifdef USE_COLOR\n\tvColor *= color;\n#endif\n#ifdef USE_INSTANCING_COLOR\n\tvColor.xyz *= instanceColor.xyz;\n#endif",common:"#define PI 3.141592653589793\n#define PI2 6.283185307179586\n#define PI_HALF 1.5707963267948966\n#define RECIPROCAL_PI 0.3183098861837907\n#define RECIPROCAL_PI2 0.15915494309189535\n#define EPSILON 1e-6\n#ifndef saturate\n#define saturate(a) clamp( a, 0.0, 1.0 )\n#endif\n#define whiteComplement(a) ( 1.0 - saturate( a ) )\nfloat pow2( const in float x ) { return x*x; }\nfloat pow3( const in float x ) { return x*x*x; }\nfloat pow4( const in float x ) { float x2 = x*x; return x2*x2; }\nfloat average( const in vec3 color ) { return dot( color, vec3( 0.3333 ) ); }\nhighp float rand( const in vec2 uv ) {\n\tconst highp float a = 12.9898, b = 78.233, c = 43758.5453;\n\thighp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );\n\treturn fract(sin(sn) * c);\n}\n#ifdef HIGH_PRECISION\n\tfloat precisionSafeLength( vec3 v ) { return length( v ); }\n#else\n\tfloat max3( vec3 v ) { return max( max( v.x, v.y ), v.z ); }\n\tfloat precisionSafeLength( vec3 v ) {\n\t\tfloat maxComponent = max3( abs( v ) );\n\t\treturn length( v / maxComponent ) * maxComponent;\n\t}\n#endif\nstruct IncidentLight {\n\tvec3 color;\n\tvec3 direction;\n\tbool visible;\n};\nstruct ReflectedLight {\n\tvec3 directDiffuse;\n\tvec3 directSpecular;\n\tvec3 indirectDiffuse;\n\tvec3 indirectSpecular;\n};\nstruct GeometricContext {\n\tvec3 position;\n\tvec3 normal;\n\tvec3 viewDir;\n#ifdef CLEARCOAT\n\tvec3 clearcoatNormal;\n#endif\n};\nvec3 transformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );\n}\nvec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {\n\treturn normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );\n}\nvec3 projectOnPlane(in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\tfloat distance = dot( planeNormal, point - pointOnPlane );\n\treturn - distance * planeNormal + point;\n}\nfloat sideOfPlane( in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\treturn sign( dot( point - pointOnPlane, planeNormal ) );\n}\nvec3 linePlaneIntersect( in vec3 pointOnLine, in vec3 lineDirection, in vec3 pointOnPlane, in vec3 planeNormal ) {\n\treturn lineDirection * ( dot( planeNormal, pointOnPlane - pointOnLine ) / dot( planeNormal, lineDirection ) ) + pointOnLine;\n}\nmat3 transposeMat3( const in mat3 m ) {\n\tmat3 tmp;\n\ttmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x );\n\ttmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y );\n\ttmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z );\n\treturn tmp;\n}\nfloat linearToRelativeLuminance( const in vec3 color ) {\n\tvec3 weights = vec3( 0.2126, 0.7152, 0.0722 );\n\treturn dot( weights, color.rgb );\n}\nbool isPerspectiveMatrix( mat4 m ) {\n\treturn m[ 2 ][ 3 ] == - 1.0;\n}\nvec2 equirectUv( in vec3 dir ) {\n\tfloat u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5;\n\tfloat v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5;\n\treturn vec2( u, v );\n}",cube_uv_reflection_fragment:"#ifdef ENVMAP_TYPE_CUBE_UV\n\t#define cubeUV_maxMipLevel 8.0\n\t#define cubeUV_minMipLevel 4.0\n\t#define cubeUV_maxTileSize 256.0\n\t#define cubeUV_minTileSize 16.0\n\tfloat getFace( vec3 direction ) {\n\t\tvec3 absDirection = abs( direction );\n\t\tfloat face = - 1.0;\n\t\tif ( absDirection.x > absDirection.z ) {\n\t\t\tif ( absDirection.x > absDirection.y )\n\t\t\t\tface = direction.x > 0.0 ? 0.0 : 3.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t} else {\n\t\t\tif ( absDirection.z > absDirection.y )\n\t\t\t\tface = direction.z > 0.0 ? 2.0 : 5.0;\n\t\t\telse\n\t\t\t\tface = direction.y > 0.0 ? 1.0 : 4.0;\n\t\t}\n\t\treturn face;\n\t}\n\tvec2 getUV( vec3 direction, float face ) {\n\t\tvec2 uv;\n\t\tif ( face == 0.0 ) {\n\t\t\tuv = vec2( direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 1.0 ) {\n\t\t\tuv = vec2( - direction.x, - direction.z ) / abs( direction.y );\n\t\t} else if ( face == 2.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.y ) / abs( direction.z );\n\t\t} else if ( face == 3.0 ) {\n\t\t\tuv = vec2( - direction.z, direction.y ) / abs( direction.x );\n\t\t} else if ( face == 4.0 ) {\n\t\t\tuv = vec2( - direction.x, direction.z ) / abs( direction.y );\n\t\t} else {\n\t\t\tuv = vec2( direction.x, direction.y ) / abs( direction.z );\n\t\t}\n\t\treturn 0.5 * ( uv + 1.0 );\n\t}\n\tvec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) {\n\t\tfloat face = getFace( direction );\n\t\tfloat filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 );\n\t\tmipInt = max( mipInt, cubeUV_minMipLevel );\n\t\tfloat faceSize = exp2( mipInt );\n\t\tfloat texelSize = 1.0 / ( 3.0 * cubeUV_maxTileSize );\n\t\tvec2 uv = getUV( direction, face ) * ( faceSize - 1.0 );\n\t\tvec2 f = fract( uv );\n\t\tuv += 0.5 - f;\n\t\tif ( face > 2.0 ) {\n\t\t\tuv.y += faceSize;\n\t\t\tface -= 3.0;\n\t\t}\n\t\tuv.x += face * faceSize;\n\t\tif ( mipInt < cubeUV_maxMipLevel ) {\n\t\t\tuv.y += 2.0 * cubeUV_maxTileSize;\n\t\t}\n\t\tuv.y += filterInt * 2.0 * cubeUV_minTileSize;\n\t\tuv.x += 3.0 * max( 0.0, cubeUV_maxTileSize - 2.0 * faceSize );\n\t\tuv *= texelSize;\n\t\tvec3 tl = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tuv.x += texelSize;\n\t\tvec3 tr = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tuv.y += texelSize;\n\t\tvec3 br = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tuv.x -= texelSize;\n\t\tvec3 bl = envMapTexelToLinear( texture2D( envMap, uv ) ).rgb;\n\t\tvec3 tm = mix( tl, tr, f.x );\n\t\tvec3 bm = mix( bl, br, f.x );\n\t\treturn mix( tm, bm, f.y );\n\t}\n\t#define r0 1.0\n\t#define v0 0.339\n\t#define m0 - 2.0\n\t#define r1 0.8\n\t#define v1 0.276\n\t#define m1 - 1.0\n\t#define r4 0.4\n\t#define v4 0.046\n\t#define m4 2.0\n\t#define r5 0.305\n\t#define v5 0.016\n\t#define m5 3.0\n\t#define r6 0.21\n\t#define v6 0.0038\n\t#define m6 4.0\n\tfloat roughnessToMip( float roughness ) {\n\t\tfloat mip = 0.0;\n\t\tif ( roughness >= r1 ) {\n\t\t\tmip = ( r0 - roughness ) * ( m1 - m0 ) / ( r0 - r1 ) + m0;\n\t\t} else if ( roughness >= r4 ) {\n\t\t\tmip = ( r1 - roughness ) * ( m4 - m1 ) / ( r1 - r4 ) + m1;\n\t\t} else if ( roughness >= r5 ) {\n\t\t\tmip = ( r4 - roughness ) * ( m5 - m4 ) / ( r4 - r5 ) + m4;\n\t\t} else if ( roughness >= r6 ) {\n\t\t\tmip = ( r5 - roughness ) * ( m6 - m5 ) / ( r5 - r6 ) + m5;\n\t\t} else {\n\t\t\tmip = - 2.0 * log2( 1.16 * roughness );\t\t}\n\t\treturn mip;\n\t}\n\tvec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) {\n\t\tfloat mip = clamp( roughnessToMip( roughness ), m0, cubeUV_maxMipLevel );\n\t\tfloat mipF = fract( mip );\n\t\tfloat mipInt = floor( mip );\n\t\tvec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt );\n\t\tif ( mipF == 0.0 ) {\n\t\t\treturn vec4( color0, 1.0 );\n\t\t} else {\n\t\t\tvec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 );\n\t\t\treturn vec4( mix( color0, color1, mipF ), 1.0 );\n\t\t}\n\t}\n#endif",defaultnormal_vertex:"vec3 transformedNormal = objectNormal;\n#ifdef USE_INSTANCING\n\tmat3 m = mat3( instanceMatrix );\n\ttransformedNormal /= vec3( dot( m[ 0 ], m[ 0 ] ), dot( m[ 1 ], m[ 1 ] ), dot( m[ 2 ], m[ 2 ] ) );\n\ttransformedNormal = m * transformedNormal;\n#endif\ntransformedNormal = normalMatrix * transformedNormal;\n#ifdef FLIP_SIDED\n\ttransformedNormal = - transformedNormal;\n#endif\n#ifdef USE_TANGENT\n\tvec3 transformedTangent = ( modelViewMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#ifdef FLIP_SIDED\n\t\ttransformedTangent = - transformedTangent;\n\t#endif\n#endif",displacementmap_pars_vertex:"#ifdef USE_DISPLACEMENTMAP\n\tuniform sampler2D displacementMap;\n\tuniform float displacementScale;\n\tuniform float displacementBias;\n#endif",displacementmap_vertex:"#ifdef USE_DISPLACEMENTMAP\n\ttransformed += normalize( objectNormal ) * ( texture2D( displacementMap, vUv ).x * displacementScale + displacementBias );\n#endif",emissivemap_fragment:"#ifdef USE_EMISSIVEMAP\n\tvec4 emissiveColor = texture2D( emissiveMap, vUv );\n\temissiveColor.rgb = emissiveMapTexelToLinear( emissiveColor ).rgb;\n\ttotalEmissiveRadiance *= emissiveColor.rgb;\n#endif",emissivemap_pars_fragment:"#ifdef USE_EMISSIVEMAP\n\tuniform sampler2D emissiveMap;\n#endif",encodings_fragment:"gl_FragColor = linearToOutputTexel( gl_FragColor );",encodings_pars_fragment:"\nvec4 LinearToLinear( in vec4 value ) {\n\treturn value;\n}\nvec4 GammaToLinear( in vec4 value, in float gammaFactor ) {\n\treturn vec4( pow( value.rgb, vec3( gammaFactor ) ), value.a );\n}\nvec4 LinearToGamma( in vec4 value, in float gammaFactor ) {\n\treturn vec4( pow( value.rgb, vec3( 1.0 / gammaFactor ) ), value.a );\n}\nvec4 sRGBToLinear( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a );\n}\nvec4 LinearTosRGB( in vec4 value ) {\n\treturn vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a );\n}\nvec4 RGBEToLinear( in vec4 value ) {\n\treturn vec4( value.rgb * exp2( value.a * 255.0 - 128.0 ), 1.0 );\n}\nvec4 LinearToRGBE( in vec4 value ) {\n\tfloat maxComponent = max( max( value.r, value.g ), value.b );\n\tfloat fExp = clamp( ceil( log2( maxComponent ) ), -128.0, 127.0 );\n\treturn vec4( value.rgb / exp2( fExp ), ( fExp + 128.0 ) / 255.0 );\n}\nvec4 RGBMToLinear( in vec4 value, in float maxRange ) {\n\treturn vec4( value.rgb * value.a * maxRange, 1.0 );\n}\nvec4 LinearToRGBM( in vec4 value, in float maxRange ) {\n\tfloat maxRGB = max( value.r, max( value.g, value.b ) );\n\tfloat M = clamp( maxRGB / maxRange, 0.0, 1.0 );\n\tM = ceil( M * 255.0 ) / 255.0;\n\treturn vec4( value.rgb / ( M * maxRange ), M );\n}\nvec4 RGBDToLinear( in vec4 value, in float maxRange ) {\n\treturn vec4( value.rgb * ( ( maxRange / 255.0 ) / value.a ), 1.0 );\n}\nvec4 LinearToRGBD( in vec4 value, in float maxRange ) {\n\tfloat maxRGB = max( value.r, max( value.g, value.b ) );\n\tfloat D = max( maxRange / maxRGB, 1.0 );\n\tD = clamp( floor( D ) / 255.0, 0.0, 1.0 );\n\treturn vec4( value.rgb * ( D * ( 255.0 / maxRange ) ), D );\n}\nconst mat3 cLogLuvM = mat3( 0.2209, 0.3390, 0.4184, 0.1138, 0.6780, 0.7319, 0.0102, 0.1130, 0.2969 );\nvec4 LinearToLogLuv( in vec4 value ) {\n\tvec3 Xp_Y_XYZp = cLogLuvM * value.rgb;\n\tXp_Y_XYZp = max( Xp_Y_XYZp, vec3( 1e-6, 1e-6, 1e-6 ) );\n\tvec4 vResult;\n\tvResult.xy = Xp_Y_XYZp.xy / Xp_Y_XYZp.z;\n\tfloat Le = 2.0 * log2(Xp_Y_XYZp.y) + 127.0;\n\tvResult.w = fract( Le );\n\tvResult.z = ( Le - ( floor( vResult.w * 255.0 ) ) / 255.0 ) / 255.0;\n\treturn vResult;\n}\nconst mat3 cLogLuvInverseM = mat3( 6.0014, -2.7008, -1.7996, -1.3320, 3.1029, -5.7721, 0.3008, -1.0882, 5.6268 );\nvec4 LogLuvToLinear( in vec4 value ) {\n\tfloat Le = value.z * 255.0 + value.w;\n\tvec3 Xp_Y_XYZp;\n\tXp_Y_XYZp.y = exp2( ( Le - 127.0 ) / 2.0 );\n\tXp_Y_XYZp.z = Xp_Y_XYZp.y / value.y;\n\tXp_Y_XYZp.x = value.x * Xp_Y_XYZp.z;\n\tvec3 vRGB = cLogLuvInverseM * Xp_Y_XYZp.rgb;\n\treturn vec4( max( vRGB, 0.0 ), 1.0 );\n}",envmap_fragment:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvec3 cameraToFrag;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToFrag = normalize( vWorldPosition - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( cameraToFrag, worldNormal );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio );\n\t\t#endif\n\t#else\n\t\tvec3 reflectVec = vReflect;\n\t#endif\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tvec4 envColor = textureCube( envMap, vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\tvec4 envColor = textureCubeUV( envMap, reflectVec, 0.0 );\n\t#else\n\t\tvec4 envColor = vec4( 0.0 );\n\t#endif\n\t#ifndef ENVMAP_TYPE_CUBE_UV\n\t\tenvColor = envMapTexelToLinear( envColor );\n\t#endif\n\t#ifdef ENVMAP_BLENDING_MULTIPLY\n\t\toutgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_MIX )\n\t\toutgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\t#elif defined( ENVMAP_BLENDING_ADD )\n\t\toutgoingLight += envColor.xyz * specularStrength * reflectivity;\n\t#endif\n#endif",envmap_common_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float envMapIntensity;\n\tuniform float flipEnvMap;\n\tuniform int maxMipLevel;\n\t#ifdef ENVMAP_TYPE_CUBE\n\t\tuniform samplerCube envMap;\n\t#else\n\t\tuniform sampler2D envMap;\n\t#endif\n\t\n#endif",envmap_pars_fragment:"#ifdef USE_ENVMAP\n\tuniform float reflectivity;\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\tvarying vec3 vWorldPosition;\n\t\tuniform float refractionRatio;\n\t#else\n\t\tvarying vec3 vReflect;\n\t#endif\n#endif",envmap_pars_vertex:"#ifdef USE_ENVMAP\n\t#if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) ||defined( PHONG )\n\t\t#define ENV_WORLDPOS\n\t#endif\n\t#ifdef ENV_WORLDPOS\n\t\t\n\t\tvarying vec3 vWorldPosition;\n\t#else\n\t\tvarying vec3 vReflect;\n\t\tuniform float refractionRatio;\n\t#endif\n#endif",envmap_physical_pars_fragment:"#if defined( USE_ENVMAP )\n\t#ifdef ENVMAP_MODE_REFRACTION\n\t\tuniform float refractionRatio;\n\t#endif\n\tvec3 getLightProbeIndirectIrradiance( const in GeometricContext geometry, const in int maxMIPLevel ) {\n\t\tvec3 worldNormal = inverseTransformDirection( geometry.normal, viewMatrix );\n\t\t#ifdef ENVMAP_TYPE_CUBE\n\t\t\tvec3 queryVec = vec3( flipEnvMap * worldNormal.x, worldNormal.yz );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = textureCubeLodEXT( envMap, queryVec, float( maxMIPLevel ) );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = textureCube( envMap, queryVec, float( maxMIPLevel ) );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, worldNormal, 1.0 );\n\t\t#else\n\t\t\tvec4 envMapColor = vec4( 0.0 );\n\t\t#endif\n\t\treturn PI * envMapColor.rgb * envMapIntensity;\n\t}\n\tfloat getSpecularMIPLevel( const in float roughness, const in int maxMIPLevel ) {\n\t\tfloat maxMIPLevelScalar = float( maxMIPLevel );\n\t\tfloat sigma = PI * roughness * roughness / ( 1.0 + roughness );\n\t\tfloat desiredMIPLevel = maxMIPLevelScalar + log2( sigma );\n\t\treturn clamp( desiredMIPLevel, 0.0, maxMIPLevelScalar );\n\t}\n\tvec3 getLightProbeIndirectRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in int maxMIPLevel ) {\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvec3 reflectVec = reflect( -viewDir, normal );\n\t\t\treflectVec = normalize( mix( reflectVec, normal, roughness * roughness) );\n\t\t#else\n\t\t\tvec3 reflectVec = refract( -viewDir, normal, refractionRatio );\n\t\t#endif\n\t\treflectVec = inverseTransformDirection( reflectVec, viewMatrix );\n\t\tfloat specularMIPLevel = getSpecularMIPLevel( roughness, maxMIPLevel );\n\t\t#ifdef ENVMAP_TYPE_CUBE\n\t\t\tvec3 queryReflectVec = vec3( flipEnvMap * reflectVec.x, reflectVec.yz );\n\t\t\t#ifdef TEXTURE_LOD_EXT\n\t\t\t\tvec4 envMapColor = textureCubeLodEXT( envMap, queryReflectVec, specularMIPLevel );\n\t\t\t#else\n\t\t\t\tvec4 envMapColor = textureCube( envMap, queryReflectVec, specularMIPLevel );\n\t\t\t#endif\n\t\t\tenvMapColor.rgb = envMapTexelToLinear( envMapColor ).rgb;\n\t\t#elif defined( ENVMAP_TYPE_CUBE_UV )\n\t\t\tvec4 envMapColor = textureCubeUV( envMap, reflectVec, roughness );\n\t\t#endif\n\t\treturn envMapColor.rgb * envMapIntensity;\n\t}\n#endif",envmap_vertex:"#ifdef USE_ENVMAP\n\t#ifdef ENV_WORLDPOS\n\t\tvWorldPosition = worldPosition.xyz;\n\t#else\n\t\tvec3 cameraToVertex;\n\t\tif ( isOrthographic ) {\n\t\t\tcameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) );\n\t\t} else {\n\t\t\tcameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\t\t}\n\t\tvec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\t#ifdef ENVMAP_MODE_REFLECTION\n\t\t\tvReflect = reflect( cameraToVertex, worldNormal );\n\t\t#else\n\t\t\tvReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\t\t#endif\n\t#endif\n#endif",fog_vertex:"#ifdef USE_FOG\n\tfogDepth = - mvPosition.z;\n#endif",fog_pars_vertex:"#ifdef USE_FOG\n\tvarying float fogDepth;\n#endif",fog_fragment:"#ifdef USE_FOG\n\t#ifdef FOG_EXP2\n\t\tfloat fogFactor = 1.0 - exp( - fogDensity * fogDensity * fogDepth * fogDepth );\n\t#else\n\t\tfloat fogFactor = smoothstep( fogNear, fogFar, fogDepth );\n\t#endif\n\tgl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor );\n#endif",fog_pars_fragment:"#ifdef USE_FOG\n\tuniform vec3 fogColor;\n\tvarying float fogDepth;\n\t#ifdef FOG_EXP2\n\t\tuniform float fogDensity;\n\t#else\n\t\tuniform float fogNear;\n\t\tuniform float fogFar;\n\t#endif\n#endif",gradientmap_pars_fragment:"#ifdef USE_GRADIENTMAP\n\tuniform sampler2D gradientMap;\n#endif\nvec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) {\n\tfloat dotNL = dot( normal, lightDirection );\n\tvec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 );\n\t#ifdef USE_GRADIENTMAP\n\t\treturn texture2D( gradientMap, coord ).rgb;\n\t#else\n\t\treturn ( coord.x < 0.7 ) ? vec3( 0.7 ) : vec3( 1.0 );\n\t#endif\n}",lightmap_fragment:"#ifdef USE_LIGHTMAP\n\tvec4 lightMapTexel= texture2D( lightMap, vUv2 );\n\treflectedLight.indirectDiffuse += PI * lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;\n#endif",lightmap_pars_fragment:"#ifdef USE_LIGHTMAP\n\tuniform sampler2D lightMap;\n\tuniform float lightMapIntensity;\n#endif",lights_lambert_vertex:"vec3 diffuse = vec3( 1.0 );\nGeometricContext geometry;\ngeometry.position = mvPosition.xyz;\ngeometry.normal = normalize( transformedNormal );\ngeometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( -mvPosition.xyz );\nGeometricContext backGeometry;\nbackGeometry.position = geometry.position;\nbackGeometry.normal = -geometry.normal;\nbackGeometry.viewDir = geometry.viewDir;\nvLightFront = vec3( 0.0 );\nvIndirectFront = vec3( 0.0 );\n#ifdef DOUBLE_SIDED\n\tvLightBack = vec3( 0.0 );\n\tvIndirectBack = vec3( 0.0 );\n#endif\nIncidentLight directLight;\nfloat dotNL;\nvec3 directLightColor_Diffuse;\nvIndirectFront += getAmbientLightIrradiance( ambientLightColor );\nvIndirectFront += getLightProbeIrradiance( lightProbe, geometry );\n#ifdef DOUBLE_SIDED\n\tvIndirectBack += getAmbientLightIrradiance( ambientLightColor );\n\tvIndirectBack += getLightProbeIrradiance( lightProbe, backGeometry );\n#endif\n#if NUM_POINT_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tgetPointDirectLightIrradiance( pointLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tgetSpotDirectLightIrradiance( spotLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if NUM_DIR_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tgetDirectionalDirectLightIrradiance( directionalLights[ i ], geometry, directLight );\n\t\tdotNL = dot( geometry.normal, directLight.direction );\n\t\tdirectLightColor_Diffuse = PI * directLight.color;\n\t\tvLightFront += saturate( dotNL ) * directLightColor_Diffuse;\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvLightBack += saturate( -dotNL ) * directLightColor_Diffuse;\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\tvIndirectFront += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );\n\t\t#ifdef DOUBLE_SIDED\n\t\t\tvIndirectBack += getHemisphereLightIrradiance( hemisphereLights[ i ], backGeometry );\n\t\t#endif\n\t}\n\t#pragma unroll_loop_end\n#endif",lights_pars_begin:"uniform bool receiveShadow;\nuniform vec3 ambientLightColor;\nuniform vec3 lightProbe[ 9 ];\nvec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) {\n\tfloat x = normal.x, y = normal.y, z = normal.z;\n\tvec3 result = shCoefficients[ 0 ] * 0.886227;\n\tresult += shCoefficients[ 1 ] * 2.0 * 0.511664 * y;\n\tresult += shCoefficients[ 2 ] * 2.0 * 0.511664 * z;\n\tresult += shCoefficients[ 3 ] * 2.0 * 0.511664 * x;\n\tresult += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y;\n\tresult += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z;\n\tresult += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 );\n\tresult += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z;\n\tresult += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y );\n\treturn result;\n}\nvec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in GeometricContext geometry ) {\n\tvec3 worldNormal = inverseTransformDirection( geometry.normal, viewMatrix );\n\tvec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe );\n\treturn irradiance;\n}\nvec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) {\n\tvec3 irradiance = ambientLightColor;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treturn irradiance;\n}\n#if NUM_DIR_LIGHTS > 0\n\tstruct DirectionalLight {\n\t\tvec3 direction;\n\t\tvec3 color;\n\t};\n\tuniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];\n\tvoid getDirectionalDirectLightIrradiance( const in DirectionalLight directionalLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tdirectLight.color = directionalLight.color;\n\t\tdirectLight.direction = directionalLight.direction;\n\t\tdirectLight.visible = true;\n\t}\n#endif\n#if NUM_POINT_LIGHTS > 0\n\tstruct PointLight {\n\t\tvec3 position;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t};\n\tuniform PointLight pointLights[ NUM_POINT_LIGHTS ];\n\tvoid getPointDirectLightIrradiance( const in PointLight pointLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tvec3 lVector = pointLight.position - geometry.position;\n\t\tdirectLight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tdirectLight.color = pointLight.color;\n\t\tdirectLight.color *= punctualLightIntensityToIrradianceFactor( lightDistance, pointLight.distance, pointLight.decay );\n\t\tdirectLight.visible = ( directLight.color != vec3( 0.0 ) );\n\t}\n#endif\n#if NUM_SPOT_LIGHTS > 0\n\tstruct SpotLight {\n\t\tvec3 position;\n\t\tvec3 direction;\n\t\tvec3 color;\n\t\tfloat distance;\n\t\tfloat decay;\n\t\tfloat coneCos;\n\t\tfloat penumbraCos;\n\t};\n\tuniform SpotLight spotLights[ NUM_SPOT_LIGHTS ];\n\tvoid getSpotDirectLightIrradiance( const in SpotLight spotLight, const in GeometricContext geometry, out IncidentLight directLight ) {\n\t\tvec3 lVector = spotLight.position - geometry.position;\n\t\tdirectLight.direction = normalize( lVector );\n\t\tfloat lightDistance = length( lVector );\n\t\tfloat angleCos = dot( directLight.direction, spotLight.direction );\n\t\tif ( angleCos > spotLight.coneCos ) {\n\t\t\tfloat spotEffect = smoothstep( spotLight.coneCos, spotLight.penumbraCos, angleCos );\n\t\t\tdirectLight.color = spotLight.color;\n\t\t\tdirectLight.color *= spotEffect * punctualLightIntensityToIrradianceFactor( lightDistance, spotLight.distance, spotLight.decay );\n\t\t\tdirectLight.visible = true;\n\t\t} else {\n\t\t\tdirectLight.color = vec3( 0.0 );\n\t\t\tdirectLight.visible = false;\n\t\t}\n\t}\n#endif\n#if NUM_RECT_AREA_LIGHTS > 0\n\tstruct RectAreaLight {\n\t\tvec3 color;\n\t\tvec3 position;\n\t\tvec3 halfWidth;\n\t\tvec3 halfHeight;\n\t};\n\tuniform sampler2D ltc_1;\tuniform sampler2D ltc_2;\n\tuniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ];\n#endif\n#if NUM_HEMI_LIGHTS > 0\n\tstruct HemisphereLight {\n\t\tvec3 direction;\n\t\tvec3 skyColor;\n\t\tvec3 groundColor;\n\t};\n\tuniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ];\n\tvec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in GeometricContext geometry ) {\n\t\tfloat dotNL = dot( geometry.normal, hemiLight.direction );\n\t\tfloat hemiDiffuseWeight = 0.5 * dotNL + 0.5;\n\t\tvec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tirradiance *= PI;\n\t\t#endif\n\t\treturn irradiance;\n\t}\n#endif",lights_toon_fragment:"ToonMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;",lights_toon_pars_fragment:"varying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\nstruct ToonMaterial {\n\tvec3 diffuseColor;\n};\nvoid RE_Direct_Toon( const in IncidentLight directLight, const in GeometricContext geometry, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\tvec3 irradiance = getGradientIrradiance( geometry.normal, directLight.direction ) * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in GeometricContext geometry, const in ToonMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_Toon\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Toon\n#define Material_LightProbeLOD( material )\t(0)",lights_phong_fragment:"BlinnPhongMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb;\nmaterial.specularColor = specular;\nmaterial.specularShininess = shininess;\nmaterial.specularStrength = specularStrength;",lights_phong_pars_fragment:"varying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\nstruct BlinnPhongMaterial {\n\tvec3 diffuseColor;\n\tvec3 specularColor;\n\tfloat specularShininess;\n\tfloat specularStrength;\n};\nvoid RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\treflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n\treflectedLight.directSpecular += irradiance * BRDF_Specular_BlinnPhong( directLight, geometry, material.specularColor, material.specularShininess ) * material.specularStrength;\n}\nvoid RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\n#define RE_Direct\t\t\t\tRE_Direct_BlinnPhong\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_BlinnPhong\n#define Material_LightProbeLOD( material )\t(0)",lights_physical_fragment:"PhysicalMaterial material;\nmaterial.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor );\nvec3 dxy = max( abs( dFdx( geometryNormal ) ), abs( dFdy( geometryNormal ) ) );\nfloat geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z );\nmaterial.specularRoughness = max( roughnessFactor, 0.0525 );material.specularRoughness += geometryRoughness;\nmaterial.specularRoughness = min( material.specularRoughness, 1.0 );\n#ifdef REFLECTIVITY\n\tmaterial.specularColor = mix( vec3( MAXIMUM_SPECULAR_COEFFICIENT * pow2( reflectivity ) ), diffuseColor.rgb, metalnessFactor );\n#else\n\tmaterial.specularColor = mix( vec3( DEFAULT_SPECULAR_COEFFICIENT ), diffuseColor.rgb, metalnessFactor );\n#endif\n#ifdef CLEARCOAT\n\tmaterial.clearcoat = clearcoat;\n\tmaterial.clearcoatRoughness = clearcoatRoughness;\n\t#ifdef USE_CLEARCOATMAP\n\t\tmaterial.clearcoat *= texture2D( clearcoatMap, vUv ).x;\n\t#endif\n\t#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\t\tmaterial.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vUv ).y;\n\t#endif\n\tmaterial.clearcoat = saturate( material.clearcoat );\tmaterial.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 );\n\tmaterial.clearcoatRoughness += geometryRoughness;\n\tmaterial.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 );\n#endif\n#ifdef USE_SHEEN\n\tmaterial.sheenColor = sheen;\n#endif",lights_physical_pars_fragment:"struct PhysicalMaterial {\n\tvec3 diffuseColor;\n\tfloat specularRoughness;\n\tvec3 specularColor;\n#ifdef CLEARCOAT\n\tfloat clearcoat;\n\tfloat clearcoatRoughness;\n#endif\n#ifdef USE_SHEEN\n\tvec3 sheenColor;\n#endif\n};\n#define MAXIMUM_SPECULAR_COEFFICIENT 0.16\n#define DEFAULT_SPECULAR_COEFFICIENT 0.04\nfloat clearcoatDHRApprox( const in float roughness, const in float dotNL ) {\n\treturn DEFAULT_SPECULAR_COEFFICIENT + ( 1.0 - DEFAULT_SPECULAR_COEFFICIENT ) * ( pow( 1.0 - dotNL, 5.0 ) * pow( 1.0 - roughness, 2.0 ) );\n}\n#if NUM_RECT_AREA_LIGHTS > 0\n\tvoid RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\t\tvec3 normal = geometry.normal;\n\t\tvec3 viewDir = geometry.viewDir;\n\t\tvec3 position = geometry.position;\n\t\tvec3 lightPos = rectAreaLight.position;\n\t\tvec3 halfWidth = rectAreaLight.halfWidth;\n\t\tvec3 halfHeight = rectAreaLight.halfHeight;\n\t\tvec3 lightColor = rectAreaLight.color;\n\t\tfloat roughness = material.specularRoughness;\n\t\tvec3 rectCoords[ 4 ];\n\t\trectCoords[ 0 ] = lightPos + halfWidth - halfHeight;\t\trectCoords[ 1 ] = lightPos - halfWidth - halfHeight;\n\t\trectCoords[ 2 ] = lightPos - halfWidth + halfHeight;\n\t\trectCoords[ 3 ] = lightPos + halfWidth + halfHeight;\n\t\tvec2 uv = LTC_Uv( normal, viewDir, roughness );\n\t\tvec4 t1 = texture2D( ltc_1, uv );\n\t\tvec4 t2 = texture2D( ltc_2, uv );\n\t\tmat3 mInv = mat3(\n\t\t\tvec3( t1.x, 0, t1.y ),\n\t\t\tvec3(\t\t0, 1,\t\t0 ),\n\t\t\tvec3( t1.z, 0, t1.w )\n\t\t);\n\t\tvec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y );\n\t\treflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords );\n\t\treflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords );\n\t}\n#endif\nvoid RE_Direct_Physical( const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\tfloat dotNL = saturate( dot( geometry.normal, directLight.direction ) );\n\tvec3 irradiance = dotNL * directLight.color;\n\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\tirradiance *= PI;\n\t#endif\n\t#ifdef CLEARCOAT\n\t\tfloat ccDotNL = saturate( dot( geometry.clearcoatNormal, directLight.direction ) );\n\t\tvec3 ccIrradiance = ccDotNL * directLight.color;\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tccIrradiance *= PI;\n\t\t#endif\n\t\tfloat clearcoatDHR = material.clearcoat * clearcoatDHRApprox( material.clearcoatRoughness, ccDotNL );\n\t\treflectedLight.directSpecular += ccIrradiance * material.clearcoat * BRDF_Specular_GGX( directLight, geometry.viewDir, geometry.clearcoatNormal, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearcoatRoughness );\n\t#else\n\t\tfloat clearcoatDHR = 0.0;\n\t#endif\n\t#ifdef USE_SHEEN\n\t\treflectedLight.directSpecular += ( 1.0 - clearcoatDHR ) * irradiance * BRDF_Specular_Sheen(\n\t\t\tmaterial.specularRoughness,\n\t\t\tdirectLight.direction,\n\t\t\tgeometry,\n\t\t\tmaterial.sheenColor\n\t\t);\n\t#else\n\t\treflectedLight.directSpecular += ( 1.0 - clearcoatDHR ) * irradiance * BRDF_Specular_GGX( directLight, geometry.viewDir, geometry.normal, material.specularColor, material.specularRoughness);\n\t#endif\n\treflectedLight.directDiffuse += ( 1.0 - clearcoatDHR ) * irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) {\n\treflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert( material.diffuseColor );\n}\nvoid RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {\n\t#ifdef CLEARCOAT\n\t\tfloat ccDotNV = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) );\n\t\treflectedLight.indirectSpecular += clearcoatRadiance * material.clearcoat * BRDF_Specular_GGX_Environment( geometry.viewDir, geometry.clearcoatNormal, vec3( DEFAULT_SPECULAR_COEFFICIENT ), material.clearcoatRoughness );\n\t\tfloat ccDotNL = ccDotNV;\n\t\tfloat clearcoatDHR = material.clearcoat * clearcoatDHRApprox( material.clearcoatRoughness, ccDotNL );\n\t#else\n\t\tfloat clearcoatDHR = 0.0;\n\t#endif\n\tfloat clearcoatInv = 1.0 - clearcoatDHR;\n\tvec3 singleScattering = vec3( 0.0 );\n\tvec3 multiScattering = vec3( 0.0 );\n\tvec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;\n\tBRDF_Specular_Multiscattering_Environment( geometry, material.specularColor, material.specularRoughness, singleScattering, multiScattering );\n\tvec3 diffuse = material.diffuseColor * ( 1.0 - ( singleScattering + multiScattering ) );\n\treflectedLight.indirectSpecular += clearcoatInv * radiance * singleScattering;\n\treflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;\n\treflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance;\n}\n#define RE_Direct\t\t\t\tRE_Direct_Physical\n#define RE_Direct_RectArea\t\tRE_Direct_RectArea_Physical\n#define RE_IndirectDiffuse\t\tRE_IndirectDiffuse_Physical\n#define RE_IndirectSpecular\t\tRE_IndirectSpecular_Physical\nfloat computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) {\n\treturn saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion );\n}",lights_fragment_begin:"\nGeometricContext geometry;\ngeometry.position = - vViewPosition;\ngeometry.normal = normal;\ngeometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition );\n#ifdef CLEARCOAT\n\tgeometry.clearcoatNormal = clearcoatNormal;\n#endif\nIncidentLight directLight;\n#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )\n\tPointLight pointLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {\n\t\tpointLight = pointLights[ i ];\n\t\tgetPointDirectLightIrradiance( pointLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS )\n\t\tpointLightShadow = pointLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct )\n\tSpotLight spotLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {\n\t\tspotLight = spotLights[ i ];\n\t\tgetSpotDirectLightIrradiance( spotLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS )\n\t\tspotLightShadow = spotLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct )\n\tDirectionalLight directionalLight;\n\t#if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLightShadow;\n\t#endif\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {\n\t\tdirectionalLight = directionalLights[ i ];\n\t\tgetDirectionalDirectLightIrradiance( directionalLight, geometry, directLight );\n\t\t#if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS )\n\t\tdirectionalLightShadow = directionalLightShadows[ i ];\n\t\tdirectLight.color *= all( bvec2( directLight.visible, receiveShadow ) ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t\t#endif\n\t\tRE_Direct( directLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea )\n\tRectAreaLight rectAreaLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) {\n\t\trectAreaLight = rectAreaLights[ i ];\n\t\tRE_Direct_RectArea( rectAreaLight, geometry, material, reflectedLight );\n\t}\n\t#pragma unroll_loop_end\n#endif\n#if defined( RE_IndirectDiffuse )\n\tvec3 iblIrradiance = vec3( 0.0 );\n\tvec3 irradiance = getAmbientLightIrradiance( ambientLightColor );\n\tirradiance += getLightProbeIrradiance( lightProbe, geometry );\n\t#if ( NUM_HEMI_LIGHTS > 0 )\n\t\t#pragma unroll_loop_start\n\t\tfor ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {\n\t\t\tirradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );\n\t\t}\n\t\t#pragma unroll_loop_end\n\t#endif\n#endif\n#if defined( RE_IndirectSpecular )\n\tvec3 radiance = vec3( 0.0 );\n\tvec3 clearcoatRadiance = vec3( 0.0 );\n#endif",lights_fragment_maps:"#if defined( RE_IndirectDiffuse )\n\t#ifdef USE_LIGHTMAP\n\t\tvec4 lightMapTexel= texture2D( lightMap, vUv2 );\n\t\tvec3 lightMapIrradiance = lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;\n\t\t#ifndef PHYSICALLY_CORRECT_LIGHTS\n\t\t\tlightMapIrradiance *= PI;\n\t\t#endif\n\t\tirradiance += lightMapIrradiance;\n\t#endif\n\t#if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV )\n\t\tiblIrradiance += getLightProbeIndirectIrradiance( geometry, maxMipLevel );\n\t#endif\n#endif\n#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular )\n\tradiance += getLightProbeIndirectRadiance( geometry.viewDir, geometry.normal, material.specularRoughness, maxMipLevel );\n\t#ifdef CLEARCOAT\n\t\tclearcoatRadiance += getLightProbeIndirectRadiance( geometry.viewDir, geometry.clearcoatNormal, material.clearcoatRoughness, maxMipLevel );\n\t#endif\n#endif",lights_fragment_end:"#if defined( RE_IndirectDiffuse )\n\tRE_IndirectDiffuse( irradiance, geometry, material, reflectedLight );\n#endif\n#if defined( RE_IndirectSpecular )\n\tRE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometry, material, reflectedLight );\n#endif",logdepthbuf_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tgl_FragDepthEXT = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5;\n#endif",logdepthbuf_pars_fragment:"#if defined( USE_LOGDEPTHBUF ) && defined( USE_LOGDEPTHBUF_EXT )\n\tuniform float logDepthBufFC;\n\tvarying float vFragDepth;\n\tvarying float vIsPerspective;\n#endif",logdepthbuf_pars_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvarying float vFragDepth;\n\t\tvarying float vIsPerspective;\n\t#else\n\t\tuniform float logDepthBufFC;\n\t#endif\n#endif",logdepthbuf_vertex:"#ifdef USE_LOGDEPTHBUF\n\t#ifdef USE_LOGDEPTHBUF_EXT\n\t\tvFragDepth = 1.0 + gl_Position.w;\n\t\tvIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) );\n\t#else\n\t\tif ( isPerspectiveMatrix( projectionMatrix ) ) {\n\t\t\tgl_Position.z = log2( max( EPSILON, gl_Position.w + 1.0 ) ) * logDepthBufFC - 1.0;\n\t\t\tgl_Position.z *= gl_Position.w;\n\t\t}\n\t#endif\n#endif",map_fragment:"#ifdef USE_MAP\n\tvec4 texelColor = texture2D( map, vUv );\n\ttexelColor = mapTexelToLinear( texelColor );\n\tdiffuseColor *= texelColor;\n#endif",map_pars_fragment:"#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif",map_particle_fragment:"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\tvec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy;\n#endif\n#ifdef USE_MAP\n\tvec4 mapTexel = texture2D( map, uv );\n\tdiffuseColor *= mapTexelToLinear( mapTexel );\n#endif\n#ifdef USE_ALPHAMAP\n\tdiffuseColor.a *= texture2D( alphaMap, uv ).g;\n#endif",map_particle_pars_fragment:"#if defined( USE_MAP ) || defined( USE_ALPHAMAP )\n\tuniform mat3 uvTransform;\n#endif\n#ifdef USE_MAP\n\tuniform sampler2D map;\n#endif\n#ifdef USE_ALPHAMAP\n\tuniform sampler2D alphaMap;\n#endif",metalnessmap_fragment:"float metalnessFactor = metalness;\n#ifdef USE_METALNESSMAP\n\tvec4 texelMetalness = texture2D( metalnessMap, vUv );\n\tmetalnessFactor *= texelMetalness.b;\n#endif",metalnessmap_pars_fragment:"#ifdef USE_METALNESSMAP\n\tuniform sampler2D metalnessMap;\n#endif",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\n\tobjectNormal *= morphTargetBaseInfluence;\n\tobjectNormal += morphNormal0 * morphTargetInfluences[ 0 ];\n\tobjectNormal += morphNormal1 * morphTargetInfluences[ 1 ];\n\tobjectNormal += morphNormal2 * morphTargetInfluences[ 2 ];\n\tobjectNormal += morphNormal3 * morphTargetInfluences[ 3 ];\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n\tuniform float morphTargetBaseInfluence;\n\t#ifndef USE_MORPHNORMALS\n\t\tuniform float morphTargetInfluences[ 8 ];\n\t#else\n\t\tuniform float morphTargetInfluences[ 4 ];\n\t#endif\n#endif",morphtarget_vertex:"#ifdef USE_MORPHTARGETS\n\ttransformed *= morphTargetBaseInfluence;\n\ttransformed += morphTarget0 * morphTargetInfluences[ 0 ];\n\ttransformed += morphTarget1 * morphTargetInfluences[ 1 ];\n\ttransformed += morphTarget2 * morphTargetInfluences[ 2 ];\n\ttransformed += morphTarget3 * morphTargetInfluences[ 3 ];\n\t#ifndef USE_MORPHNORMALS\n\t\ttransformed += morphTarget4 * morphTargetInfluences[ 4 ];\n\t\ttransformed += morphTarget5 * morphTargetInfluences[ 5 ];\n\t\ttransformed += morphTarget6 * morphTargetInfluences[ 6 ];\n\t\ttransformed += morphTarget7 * morphTargetInfluences[ 7 ];\n\t#endif\n#endif",normal_fragment_begin:"float faceDirection = gl_FrontFacing ? 1.0 : - 1.0;\n#ifdef FLAT_SHADED\n\tvec3 fdx = vec3( dFdx( vViewPosition.x ), dFdx( vViewPosition.y ), dFdx( vViewPosition.z ) );\n\tvec3 fdy = vec3( dFdy( vViewPosition.x ), dFdy( vViewPosition.y ), dFdy( vViewPosition.z ) );\n\tvec3 normal = normalize( cross( fdx, fdy ) );\n#else\n\tvec3 normal = normalize( vNormal );\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\t#ifdef USE_TANGENT\n\t\tvec3 tangent = normalize( vTangent );\n\t\tvec3 bitangent = normalize( vBitangent );\n\t\t#ifdef DOUBLE_SIDED\n\t\t\ttangent = tangent * faceDirection;\n\t\t\tbitangent = bitangent * faceDirection;\n\t\t#endif\n\t\t#if defined( TANGENTSPACE_NORMALMAP ) || defined( USE_CLEARCOAT_NORMALMAP )\n\t\t\tmat3 vTBN = mat3( tangent, bitangent, normal );\n\t\t#endif\n\t#endif\n#endif\nvec3 geometryNormal = normal;",normal_fragment_maps:"#ifdef OBJECTSPACE_NORMALMAP\n\tnormal = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\t#ifdef FLIP_SIDED\n\t\tnormal = - normal;\n\t#endif\n\t#ifdef DOUBLE_SIDED\n\t\tnormal = normal * faceDirection;\n\t#endif\n\tnormal = normalize( normalMatrix * normal );\n#elif defined( TANGENTSPACE_NORMALMAP )\n\tvec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n\tmapN.xy *= normalScale;\n\t#ifdef USE_TANGENT\n\t\tnormal = normalize( vTBN * mapN );\n\t#else\n\t\tnormal = perturbNormal2Arb( -vViewPosition, normal, mapN, faceDirection );\n\t#endif\n#elif defined( USE_BUMPMAP )\n\tnormal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd(), faceDirection );\n#endif",normalmap_pars_fragment:"#ifdef USE_NORMALMAP\n\tuniform sampler2D normalMap;\n\tuniform vec2 normalScale;\n#endif\n#ifdef OBJECTSPACE_NORMALMAP\n\tuniform mat3 normalMatrix;\n#endif\n#if ! defined ( USE_TANGENT ) && ( defined ( TANGENTSPACE_NORMALMAP ) || defined ( USE_CLEARCOAT_NORMALMAP ) )\n\tvec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm, vec3 mapN, float faceDirection ) {\n\t\tvec3 q0 = vec3( dFdx( eye_pos.x ), dFdx( eye_pos.y ), dFdx( eye_pos.z ) );\n\t\tvec3 q1 = vec3( dFdy( eye_pos.x ), dFdy( eye_pos.y ), dFdy( eye_pos.z ) );\n\t\tvec2 st0 = dFdx( vUv.st );\n\t\tvec2 st1 = dFdy( vUv.st );\n\t\tvec3 N = surf_norm;\n\t\tvec3 q1perp = cross( q1, N );\n\t\tvec3 q0perp = cross( N, q0 );\n\t\tvec3 T = q1perp * st0.x + q0perp * st1.x;\n\t\tvec3 B = q1perp * st0.y + q0perp * st1.y;\n\t\tfloat det = max( dot( T, T ), dot( B, B ) );\n\t\tfloat scale = ( det == 0.0 ) ? 0.0 : faceDirection * inversesqrt( det );\n\t\treturn normalize( T * ( mapN.x * scale ) + B * ( mapN.y * scale ) + N * mapN.z );\n\t}\n#endif",clearcoat_normal_fragment_begin:"#ifdef CLEARCOAT\n\tvec3 clearcoatNormal = geometryNormal;\n#endif",clearcoat_normal_fragment_maps:"#ifdef USE_CLEARCOAT_NORMALMAP\n\tvec3 clearcoatMapN = texture2D( clearcoatNormalMap, vUv ).xyz * 2.0 - 1.0;\n\tclearcoatMapN.xy *= clearcoatNormalScale;\n\t#ifdef USE_TANGENT\n\t\tclearcoatNormal = normalize( vTBN * clearcoatMapN );\n\t#else\n\t\tclearcoatNormal = perturbNormal2Arb( - vViewPosition, clearcoatNormal, clearcoatMapN, faceDirection );\n\t#endif\n#endif",clearcoat_pars_fragment:"#ifdef USE_CLEARCOATMAP\n\tuniform sampler2D clearcoatMap;\n#endif\n#ifdef USE_CLEARCOAT_ROUGHNESSMAP\n\tuniform sampler2D clearcoatRoughnessMap;\n#endif\n#ifdef USE_CLEARCOAT_NORMALMAP\n\tuniform sampler2D clearcoatNormalMap;\n\tuniform vec2 clearcoatNormalScale;\n#endif",packing:"vec3 packNormalToRGB( const in vec3 normal ) {\n\treturn normalize( normal ) * 0.5 + 0.5;\n}\nvec3 unpackRGBToNormal( const in vec3 rgb ) {\n\treturn 2.0 * rgb.xyz - 1.0;\n}\nconst float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;\nconst vec3 PackFactors = vec3( 256. * 256. * 256., 256. * 256., 256. );\nconst vec4 UnpackFactors = UnpackDownscale / vec4( PackFactors, 1. );\nconst float ShiftRight8 = 1. / 256.;\nvec4 packDepthToRGBA( const in float v ) {\n\tvec4 r = vec4( fract( v * PackFactors ), v );\n\tr.yzw -= r.xyz * ShiftRight8;\treturn r * PackUpscale;\n}\nfloat unpackRGBAToDepth( const in vec4 v ) {\n\treturn dot( v, UnpackFactors );\n}\nvec4 pack2HalfToRGBA( vec2 v ) {\n\tvec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ));\n\treturn vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w);\n}\nvec2 unpackRGBATo2Half( vec4 v ) {\n\treturn vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) );\n}\nfloat viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn ( viewZ + near ) / ( near - far );\n}\nfloat orthographicDepthToViewZ( const in float linearClipZ, const in float near, const in float far ) {\n\treturn linearClipZ * ( near - far ) - near;\n}\nfloat viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) {\n\treturn (( near + viewZ ) * far ) / (( far - near ) * viewZ );\n}\nfloat perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) {\n\treturn ( near * far ) / ( ( far - near ) * invClipZ - far );\n}",premultiplied_alpha_fragment:"#ifdef PREMULTIPLIED_ALPHA\n\tgl_FragColor.rgb *= gl_FragColor.a;\n#endif",project_vertex:"vec4 mvPosition = vec4( transformed, 1.0 );\n#ifdef USE_INSTANCING\n\tmvPosition = instanceMatrix * mvPosition;\n#endif\nmvPosition = modelViewMatrix * mvPosition;\ngl_Position = projectionMatrix * mvPosition;",dithering_fragment:"#ifdef DITHERING\n\tgl_FragColor.rgb = dithering( gl_FragColor.rgb );\n#endif",dithering_pars_fragment:"#ifdef DITHERING\n\tvec3 dithering( vec3 color ) {\n\t\tfloat grid_position = rand( gl_FragCoord.xy );\n\t\tvec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 );\n\t\tdither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position );\n\t\treturn color + dither_shift_RGB;\n\t}\n#endif",roughnessmap_fragment:"float roughnessFactor = roughness;\n#ifdef USE_ROUGHNESSMAP\n\tvec4 texelRoughness = texture2D( roughnessMap, vUv );\n\troughnessFactor *= texelRoughness.g;\n#endif",roughnessmap_pars_fragment:"#ifdef USE_ROUGHNESSMAP\n\tuniform sampler2D roughnessMap;\n#endif",shadowmap_pars_fragment:"#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n\tfloat texture2DCompare( sampler2D depths, vec2 uv, float compare ) {\n\t\treturn step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) );\n\t}\n\tvec2 texture2DDistribution( sampler2D shadow, vec2 uv ) {\n\t\treturn unpackRGBATo2Half( texture2D( shadow, uv ) );\n\t}\n\tfloat VSMShadow (sampler2D shadow, vec2 uv, float compare ){\n\t\tfloat occlusion = 1.0;\n\t\tvec2 distribution = texture2DDistribution( shadow, uv );\n\t\tfloat hard_shadow = step( compare , distribution.x );\n\t\tif (hard_shadow != 1.0 ) {\n\t\t\tfloat distance = compare - distribution.x ;\n\t\t\tfloat variance = max( 0.00000, distribution.y * distribution.y );\n\t\t\tfloat softness_probability = variance / (variance + distance * distance );\t\t\tsoftness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 );\t\t\tocclusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 );\n\t\t}\n\t\treturn occlusion;\n\t}\n\tfloat getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord ) {\n\t\tfloat shadow = 1.0;\n\t\tshadowCoord.xyz /= shadowCoord.w;\n\t\tshadowCoord.z += shadowBias;\n\t\tbvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\n\t\tbool inFrustum = all( inFrustumVec );\n\t\tbvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n\t\tbool frustumTest = all( frustumTestVec );\n\t\tif ( frustumTest ) {\n\t\t#if defined( SHADOWMAP_TYPE_PCF )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx0 = - texelSize.x * shadowRadius;\n\t\t\tfloat dy0 = - texelSize.y * shadowRadius;\n\t\t\tfloat dx1 = + texelSize.x * shadowRadius;\n\t\t\tfloat dy1 = + texelSize.y * shadowRadius;\n\t\t\tfloat dx2 = dx0 / 2.0;\n\t\t\tfloat dy2 = dy0 / 2.0;\n\t\t\tfloat dx3 = dx1 / 2.0;\n\t\t\tfloat dy3 = dy1 / 2.0;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z )\n\t\t\t) * ( 1.0 / 17.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\t\t\tvec2 texelSize = vec2( 1.0 ) / shadowMapSize;\n\t\t\tfloat dx = texelSize.x;\n\t\t\tfloat dy = texelSize.y;\n\t\t\tvec2 uv = shadowCoord.xy;\n\t\t\tvec2 f = fract( uv * shadowMapSize + 0.5 );\n\t\t\tuv -= f * texelSize;\n\t\t\tshadow = (\n\t\t\t\ttexture2DCompare( shadowMap, uv, shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) +\n\t\t\t\ttexture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ),\n\t\t\t\t\t f.x ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), \n\t\t\t\t\t texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t f.y ) +\n\t\t\t\tmix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), \n\t\t\t\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ),\n\t\t\t\t\t\t\tf.x ),\n\t\t\t\t\t mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), \n\t\t\t\t\t\t\ttexture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ),\n\t\t\t\t\t\t\tf.x ),\n\t\t\t\t\t f.y )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#elif defined( SHADOWMAP_TYPE_VSM )\n\t\t\tshadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#else\n\t\t\tshadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z );\n\t\t#endif\n\t\t}\n\t\treturn shadow;\n\t}\n\tvec2 cubeToUV( vec3 v, float texelSizeY ) {\n\t\tvec3 absV = abs( v );\n\t\tfloat scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) );\n\t\tabsV *= scaleToCube;\n\t\tv *= scaleToCube * ( 1.0 - 2.0 * texelSizeY );\n\t\tvec2 planar = v.xy;\n\t\tfloat almostATexel = 1.5 * texelSizeY;\n\t\tfloat almostOne = 1.0 - almostATexel;\n\t\tif ( absV.z >= almostOne ) {\n\t\t\tif ( v.z > 0.0 )\n\t\t\t\tplanar.x = 4.0 - v.x;\n\t\t} else if ( absV.x >= almostOne ) {\n\t\t\tfloat signX = sign( v.x );\n\t\t\tplanar.x = v.z * signX + 2.0 * signX;\n\t\t} else if ( absV.y >= almostOne ) {\n\t\t\tfloat signY = sign( v.y );\n\t\t\tplanar.x = v.x + 2.0 * signY + 2.0;\n\t\t\tplanar.y = v.z * signY - 2.0;\n\t\t}\n\t\treturn vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 );\n\t}\n\tfloat getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) {\n\t\tvec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) );\n\t\tvec3 lightToPosition = shadowCoord.xyz;\n\t\tfloat dp = ( length( lightToPosition ) - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear );\t\tdp += shadowBias;\n\t\tvec3 bd3D = normalize( lightToPosition );\n\t\t#if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM )\n\t\t\tvec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y;\n\t\t\treturn (\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) +\n\t\t\t\ttexture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp )\n\t\t\t) * ( 1.0 / 9.0 );\n\t\t#else\n\t\t\treturn texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp );\n\t\t#endif\n\t}\n#endif",shadowmap_pars_vertex:"#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t\tuniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tvarying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ];\n\t\tstruct DirectionalLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 spotShadowMatrix[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vSpotShadowCoord[ NUM_SPOT_LIGHT_SHADOWS ];\n\t\tstruct SpotLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t};\n\t\tuniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ];\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t\tuniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tvarying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ];\n\t\tstruct PointLightShadow {\n\t\t\tfloat shadowBias;\n\t\t\tfloat shadowNormalBias;\n\t\t\tfloat shadowRadius;\n\t\t\tvec2 shadowMapSize;\n\t\t\tfloat shadowCameraNear;\n\t\t\tfloat shadowCameraFar;\n\t\t};\n\t\tuniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ];\n\t#endif\n#endif",shadowmap_vertex:"#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0 || NUM_SPOT_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0\n\t\tvec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix );\n\t\tvec4 shadowWorldPosition;\n\t#endif\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvSpotShadowCoord[ i ] = spotShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tshadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 );\n\t\tvPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n#endif",shadowmask_pars_fragment:"float getShadowMask() {\n\tfloat shadow = 1.0;\n\t#ifdef USE_SHADOWMAP\n\t#if NUM_DIR_LIGHT_SHADOWS > 0\n\tDirectionalLightShadow directionalLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) {\n\t\tdirectionalLight = directionalLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_SPOT_LIGHT_SHADOWS > 0\n\tSpotLightShadow spotLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) {\n\t\tspotLight = spotLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowBias, spotLight.shadowRadius, vSpotShadowCoord[ i ] ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#if NUM_POINT_LIGHT_SHADOWS > 0\n\tPointLightShadow pointLight;\n\t#pragma unroll_loop_start\n\tfor ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) {\n\t\tpointLight = pointLightShadows[ i ];\n\t\tshadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0;\n\t}\n\t#pragma unroll_loop_end\n\t#endif\n\t#endif\n\treturn shadow;\n}",skinbase_vertex:"#ifdef USE_SKINNING\n\tmat4 boneMatX = getBoneMatrix( skinIndex.x );\n\tmat4 boneMatY = getBoneMatrix( skinIndex.y );\n\tmat4 boneMatZ = getBoneMatrix( skinIndex.z );\n\tmat4 boneMatW = getBoneMatrix( skinIndex.w );\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n\tuniform mat4 bindMatrix;\n\tuniform mat4 bindMatrixInverse;\n\t#ifdef BONE_TEXTURE\n\t\tuniform highp sampler2D boneTexture;\n\t\tuniform int boneTextureSize;\n\t\tmat4 getBoneMatrix( const in float i ) {\n\t\t\tfloat j = i * 4.0;\n\t\t\tfloat x = mod( j, float( boneTextureSize ) );\n\t\t\tfloat y = floor( j / float( boneTextureSize ) );\n\t\t\tfloat dx = 1.0 / float( boneTextureSize );\n\t\t\tfloat dy = 1.0 / float( boneTextureSize );\n\t\t\ty = dy * ( y + 0.5 );\n\t\t\tvec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\n\t\t\tvec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\n\t\t\tvec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\n\t\t\tvec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\n\t\t\tmat4 bone = mat4( v1, v2, v3, v4 );\n\t\t\treturn bone;\n\t\t}\n\t#else\n\t\tuniform mat4 boneMatrices[ MAX_BONES ];\n\t\tmat4 getBoneMatrix( const in float i ) {\n\t\t\tmat4 bone = boneMatrices[ int(i) ];\n\t\t\treturn bone;\n\t\t}\n\t#endif\n#endif",skinning_vertex:"#ifdef USE_SKINNING\n\tvec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );\n\tvec4 skinned = vec4( 0.0 );\n\tskinned += boneMatX * skinVertex * skinWeight.x;\n\tskinned += boneMatY * skinVertex * skinWeight.y;\n\tskinned += boneMatZ * skinVertex * skinWeight.z;\n\tskinned += boneMatW * skinVertex * skinWeight.w;\n\ttransformed = ( bindMatrixInverse * skinned ).xyz;\n#endif",skinnormal_vertex:"#ifdef USE_SKINNING\n\tmat4 skinMatrix = mat4( 0.0 );\n\tskinMatrix += skinWeight.x * boneMatX;\n\tskinMatrix += skinWeight.y * boneMatY;\n\tskinMatrix += skinWeight.z * boneMatZ;\n\tskinMatrix += skinWeight.w * boneMatW;\n\tskinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\tobjectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz;\n\t#ifdef USE_TANGENT\n\t\tobjectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz;\n\t#endif\n#endif",specularmap_fragment:"float specularStrength;\n#ifdef USE_SPECULARMAP\n\tvec4 texelSpecular = texture2D( specularMap, vUv );\n\tspecularStrength = texelSpecular.r;\n#else\n\tspecularStrength = 1.0;\n#endif",specularmap_pars_fragment:"#ifdef USE_SPECULARMAP\n\tuniform sampler2D specularMap;\n#endif",tonemapping_fragment:"#if defined( TONE_MAPPING )\n\tgl_FragColor.rgb = toneMapping( gl_FragColor.rgb );\n#endif",tonemapping_pars_fragment:"#ifndef saturate\n#define saturate(a) clamp( a, 0.0, 1.0 )\n#endif\nuniform float toneMappingExposure;\nvec3 LinearToneMapping( vec3 color ) {\n\treturn toneMappingExposure * color;\n}\nvec3 ReinhardToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\treturn saturate( color / ( vec3( 1.0 ) + color ) );\n}\nvec3 OptimizedCineonToneMapping( vec3 color ) {\n\tcolor *= toneMappingExposure;\n\tcolor = max( vec3( 0.0 ), color - 0.004 );\n\treturn pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) );\n}\nvec3 RRTAndODTFit( vec3 v ) {\n\tvec3 a = v * ( v + 0.0245786 ) - 0.000090537;\n\tvec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081;\n\treturn a / b;\n}\nvec3 ACESFilmicToneMapping( vec3 color ) {\n\tconst mat3 ACESInputMat = mat3(\n\t\tvec3( 0.59719, 0.07600, 0.02840 ),\t\tvec3( 0.35458, 0.90834, 0.13383 ),\n\t\tvec3( 0.04823, 0.01566, 0.83777 )\n\t);\n\tconst mat3 ACESOutputMat = mat3(\n\t\tvec3(\t1.60475, -0.10208, -0.00327 ),\t\tvec3( -0.53108,\t1.10813, -0.07276 ),\n\t\tvec3( -0.07367, -0.00605,\t1.07602 )\n\t);\n\tcolor *= toneMappingExposure / 0.6;\n\tcolor = ACESInputMat * color;\n\tcolor = RRTAndODTFit( color );\n\tcolor = ACESOutputMat * color;\n\treturn saturate( color );\n}\nvec3 CustomToneMapping( vec3 color ) { return color; }",transmissionmap_fragment:"#ifdef USE_TRANSMISSIONMAP\n\ttotalTransmission *= texture2D( transmissionMap, vUv ).r;\n#endif",transmissionmap_pars_fragment:"#ifdef USE_TRANSMISSIONMAP\n\tuniform sampler2D transmissionMap;\n#endif",uv_pars_fragment:"#if ( defined( USE_UV ) && ! defined( UVS_VERTEX_ONLY ) )\n\tvarying vec2 vUv;\n#endif",uv_pars_vertex:"#ifdef USE_UV\n\t#ifdef UVS_VERTEX_ONLY\n\t\tvec2 vUv;\n\t#else\n\t\tvarying vec2 vUv;\n\t#endif\n\tuniform mat3 uvTransform;\n#endif",uv_vertex:"#ifdef USE_UV\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n#endif",uv2_pars_fragment:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvarying vec2 vUv2;\n#endif",uv2_pars_vertex:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tattribute vec2 uv2;\n\tvarying vec2 vUv2;\n\tuniform mat3 uv2Transform;\n#endif",uv2_vertex:"#if defined( USE_LIGHTMAP ) || defined( USE_AOMAP )\n\tvUv2 = ( uv2Transform * vec3( uv2, 1 ) ).xy;\n#endif",worldpos_vertex:"#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP )\n\tvec4 worldPosition = vec4( transformed, 1.0 );\n\t#ifdef USE_INSTANCING\n\t\tworldPosition = instanceMatrix * worldPosition;\n\t#endif\n\tworldPosition = modelMatrix * worldPosition;\n#endif",background_frag:"uniform sampler2D t2D;\nvarying vec2 vUv;\nvoid main() {\n\tvec4 texColor = texture2D( t2D, vUv );\n\tgl_FragColor = mapTexelToLinear( texColor );\n\t#include \n\t#include \n}",background_vert:"varying vec2 vUv;\nuniform mat3 uvTransform;\nvoid main() {\n\tvUv = ( uvTransform * vec3( uv, 1 ) ).xy;\n\tgl_Position = vec4( position.xy, 1.0, 1.0 );\n}",cube_frag:"#include \nuniform float opacity;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvec3 vReflect = vWorldDirection;\n\t#include \n\tgl_FragColor = envColor;\n\tgl_FragColor.a *= opacity;\n\t#include \n\t#include \n}",cube_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n\tgl_Position.z = gl_Position.w;\n}",depth_frag:"#if DEPTH_PACKING == 3200\n\tuniform float opacity;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( 1.0 );\n\t#if DEPTH_PACKING == 3200\n\t\tdiffuseColor.a = opacity;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\tfloat fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5;\n\t#if DEPTH_PACKING == 3200\n\t\tgl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );\n\t#elif DEPTH_PACKING == 3201\n\t\tgl_FragColor = packDepthToRGBA( fragCoordZ );\n\t#endif\n}",depth_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \nvarying vec2 vHighPrecisionZW;\nvoid main() {\n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvHighPrecisionZW = gl_Position.zw;\n}",distanceRGBA_frag:"#define DISTANCE\nuniform vec3 referencePosition;\nuniform float nearDistance;\nuniform float farDistance;\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main () {\n\t#include \n\tvec4 diffuseColor = vec4( 1.0 );\n\t#include \n\t#include \n\t#include \n\tfloat dist = length( vWorldPosition - referencePosition );\n\tdist = ( dist - nearDistance ) / ( farDistance - nearDistance );\n\tdist = saturate( dist );\n\tgl_FragColor = packDepthToRGBA( dist );\n}",distanceRGBA_vert:"#define DISTANCE\nvarying vec3 vWorldPosition;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#ifdef USE_DISPLACEMENTMAP\n\t\t#include \n\t\t#include \n\t\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvWorldPosition = worldPosition.xyz;\n}",equirect_frag:"uniform sampler2D tEquirect;\nvarying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvec3 direction = normalize( vWorldDirection );\n\tvec2 sampleUV = equirectUv( direction );\n\tvec4 texColor = texture2D( tEquirect, sampleUV );\n\tgl_FragColor = mapTexelToLinear( texColor );\n\t#include \n\t#include \n}",equirect_vert:"varying vec3 vWorldDirection;\n#include \nvoid main() {\n\tvWorldDirection = transformDirection( position, modelMatrix );\n\t#include \n\t#include \n}",linedashed_frag:"uniform vec3 diffuse;\nuniform float opacity;\nuniform float dashSize;\nuniform float totalSize;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tif ( mod( vLineDistance, totalSize ) > dashSize ) {\n\t\tdiscard;\n\t}\n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n}",linedashed_vert:"uniform float scale;\nattribute float lineDistance;\nvarying float vLineDistance;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tvLineDistance = scale * lineDistance;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\t#ifdef USE_LIGHTMAP\n\t\n\t\tvec4 lightMapTexel= texture2D( lightMap, vUv2 );\n\t\treflectedLight.indirectDiffuse += lightMapTexelToLinear( lightMapTexel ).rgb * lightMapIntensity;\n\t#else\n\t\treflectedLight.indirectDiffuse += vec3( 1.0 );\n\t#endif\n\t#include \n\treflectedLight.indirectDiffuse *= diffuseColor.rgb;\n\tvec3 outgoingLight = reflectedLight.indirectDiffuse;\n\t#include \n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshbasic_vert:"#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef USE_ENVMAP\n\t#include \n\t#include \n\t#include \n\t#include \n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_frag:"uniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\nvarying vec3 vLightFront;\nvarying vec3 vIndirectFront;\n#ifdef DOUBLE_SIDED\n\tvarying vec3 vLightBack;\n\tvarying vec3 vIndirectBack;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifdef DOUBLE_SIDED\n\t\treflectedLight.indirectDiffuse += ( gl_FrontFacing ) ? vIndirectFront : vIndirectBack;\n\t#else\n\t\treflectedLight.indirectDiffuse += vIndirectFront;\n\t#endif\n\t#include \n\treflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert( diffuseColor.rgb );\n\t#ifdef DOUBLE_SIDED\n\t\treflectedLight.directDiffuse = ( gl_FrontFacing ) ? vLightFront : vLightBack;\n\t#else\n\t\treflectedLight.directDiffuse = vLightFront;\n\t#endif\n\treflectedLight.directDiffuse *= BRDF_Diffuse_Lambert( diffuseColor.rgb ) * getShadowMask();\n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\t#include \n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshlambert_vert:"#define LAMBERT\nvarying vec3 vLightFront;\nvarying vec3 vIndirectFront;\n#ifdef DOUBLE_SIDED\n\tvarying vec3 vLightBack;\n\tvarying vec3 vIndirectBack;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshmatcap_frag:"#define MATCAP\nuniform vec3 diffuse;\nuniform float opacity;\nuniform sampler2D matcap;\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 viewDir = normalize( vViewPosition );\n\tvec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) );\n\tvec3 y = cross( viewDir, x );\n\tvec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5;\n\t#ifdef USE_MATCAP\n\t\tvec4 matcapColor = texture2D( matcap, uv );\n\t\tmatcapColor = matcapTexelToLinear( matcapColor );\n\t#else\n\t\tvec4 matcapColor = vec4( 1.0 );\n\t#endif\n\tvec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshmatcap_vert:"#define MATCAP\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#ifndef FLAT_SHADED\n\t\tvNormal = normalize( transformedNormal );\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n}",meshtoon_frag:"#define TOON\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshtoon_vert:"#define TOON\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}",meshphong_frag:"#define PHONG\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform vec3 specular;\nuniform float shininess;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#include \n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphong_vert:"#define PHONG\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphysical_frag:"#define STANDARD\n#ifdef PHYSICAL\n\t#define REFLECTIVITY\n\t#define CLEARCOAT\n\t#define TRANSMISSION\n#endif\nuniform vec3 diffuse;\nuniform vec3 emissive;\nuniform float roughness;\nuniform float metalness;\nuniform float opacity;\n#ifdef TRANSMISSION\n\tuniform float transmission;\n#endif\n#ifdef REFLECTIVITY\n\tuniform float reflectivity;\n#endif\n#ifdef CLEARCOAT\n\tuniform float clearcoat;\n\tuniform float clearcoatRoughness;\n#endif\n#ifdef USE_SHEEN\n\tuniform vec3 sheen;\n#endif\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\tReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );\n\tvec3 totalEmissiveRadiance = emissive;\n\t#ifdef TRANSMISSION\n\t\tfloat totalTransmission = transmission;\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;\n\t#ifdef TRANSMISSION\n\t\tdiffuseColor.a *= mix( saturate( 1. - totalTransmission + linearToRelativeLuminance( reflectedLight.directSpecular + reflectedLight.indirectSpecular ) ), 1.0, metalness );\n\t#endif\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",meshphysical_vert:"#define STANDARD\nvarying vec3 vViewPosition;\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\tvViewPosition = - mvPosition.xyz;\n\t#include \n\t#include \n\t#include \n}",normal_frag:"#define NORMAL\nuniform float opacity;\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_FragColor = vec4( packNormalToRGB( normal ), opacity );\n}",normal_vert:"#define NORMAL\n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvarying vec3 vViewPosition;\n#endif\n#ifndef FLAT_SHADED\n\tvarying vec3 vNormal;\n\t#ifdef USE_TANGENT\n\t\tvarying vec3 vTangent;\n\t\tvarying vec3 vBitangent;\n\t#endif\n#endif\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#ifndef FLAT_SHADED\n\tvNormal = normalize( transformedNormal );\n\t#ifdef USE_TANGENT\n\t\tvTangent = normalize( transformedTangent );\n\t\tvBitangent = normalize( cross( vNormal, vTangent ) * tangent.w );\n\t#endif\n#endif\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( TANGENTSPACE_NORMALMAP )\n\tvViewPosition = - mvPosition.xyz;\n#endif\n}",points_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n\t#include \n}",points_vert:"uniform float size;\nuniform float scale;\n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\tgl_PointSize = size;\n\t#ifdef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );\n\t#endif\n\t#include \n\t#include \n\t#include \n\t#include \n}",shadow_frag:"uniform vec3 color;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\tgl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) );\n\t#include \n\t#include \n\t#include \n}",shadow_vert:"#include \n#include \n#include \nvoid main() {\n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n\t#include \n}",sprite_frag:"uniform vec3 diffuse;\nuniform float opacity;\n#include \n#include \n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec3 outgoingLight = vec3( 0.0 );\n\tvec4 diffuseColor = vec4( diffuse, opacity );\n\t#include \n\t#include \n\t#include \n\t#include \n\toutgoingLight = diffuseColor.rgb;\n\tgl_FragColor = vec4( outgoingLight, diffuseColor.a );\n\t#include \n\t#include \n\t#include \n}",sprite_vert:"uniform float rotation;\nuniform vec2 center;\n#include \n#include \n#include \n#include \n#include \nvoid main() {\n\t#include \n\tvec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );\n\tvec2 scale;\n\tscale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );\n\tscale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );\n\t#ifndef USE_SIZEATTENUATION\n\t\tbool isPerspective = isPerspectiveMatrix( projectionMatrix );\n\t\tif ( isPerspective ) scale *= - mvPosition.z;\n\t#endif\n\tvec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale;\n\tvec2 rotatedPosition;\n\trotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;\n\trotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;\n\tmvPosition.xy += rotatedPosition;\n\tgl_Position = projectionMatrix * mvPosition;\n\t#include \n\t#include \n\t#include \n}"},ui={common:{diffuse:{value:new tn(15658734)},opacity:{value:1},map:{value:null},uvTransform:{value:new yt},uv2Transform:{value:new yt},alphaMap:{value:null}},specularmap:{specularMap:{value:null}},envmap:{envMap:{value:null},flipEnvMap:{value:-1},reflectivity:{value:1},refractionRatio:{value:.98},maxMipLevel:{value:0}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1}},emissivemap:{emissiveMap:{value:null}},bumpmap:{bumpMap:{value:null},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalScale:{value:new vt(1,1)}},displacementmap:{displacementMap:{value:null},displacementScale:{value:1},displacementBias:{value:0}},roughnessmap:{roughnessMap:{value:null}},metalnessmap:{metalnessMap:{value:null}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new tn(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotShadowMap:{value:[]},spotShadowMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new tn(15658734)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},uvTransform:{value:new yt}},sprite:{diffuse:{value:new tn(15658734)},opacity:{value:1},center:{value:new vt(.5,.5)},rotation:{value:0},map:{value:null},alphaMap:{value:null},uvTransform:{value:new yt}}},di={basic:{uniforms:Yn([ui.common,ui.specularmap,ui.envmap,ui.aomap,ui.lightmap,ui.fog]),vertexShader:hi.meshbasic_vert,fragmentShader:hi.meshbasic_frag},lambert:{uniforms:Yn([ui.common,ui.specularmap,ui.envmap,ui.aomap,ui.lightmap,ui.emissivemap,ui.fog,ui.lights,{emissive:{value:new tn(0)}}]),vertexShader:hi.meshlambert_vert,fragmentShader:hi.meshlambert_frag},phong:{uniforms:Yn([ui.common,ui.specularmap,ui.envmap,ui.aomap,ui.lightmap,ui.emissivemap,ui.bumpmap,ui.normalmap,ui.displacementmap,ui.fog,ui.lights,{emissive:{value:new tn(0)},specular:{value:new tn(1118481)},shininess:{value:30}}]),vertexShader:hi.meshphong_vert,fragmentShader:hi.meshphong_frag},standard:{uniforms:Yn([ui.common,ui.envmap,ui.aomap,ui.lightmap,ui.emissivemap,ui.bumpmap,ui.normalmap,ui.displacementmap,ui.roughnessmap,ui.metalnessmap,ui.fog,ui.lights,{emissive:{value:new tn(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:hi.meshphysical_vert,fragmentShader:hi.meshphysical_frag},toon:{uniforms:Yn([ui.common,ui.aomap,ui.lightmap,ui.emissivemap,ui.bumpmap,ui.normalmap,ui.displacementmap,ui.gradientmap,ui.fog,ui.lights,{emissive:{value:new tn(0)}}]),vertexShader:hi.meshtoon_vert,fragmentShader:hi.meshtoon_frag},matcap:{uniforms:Yn([ui.common,ui.bumpmap,ui.normalmap,ui.displacementmap,ui.fog,{matcap:{value:null}}]),vertexShader:hi.meshmatcap_vert,fragmentShader:hi.meshmatcap_frag},points:{uniforms:Yn([ui.points,ui.fog]),vertexShader:hi.points_vert,fragmentShader:hi.points_frag},dashed:{uniforms:Yn([ui.common,ui.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:hi.linedashed_vert,fragmentShader:hi.linedashed_frag},depth:{uniforms:Yn([ui.common,ui.displacementmap]),vertexShader:hi.depth_vert,fragmentShader:hi.depth_frag},normal:{uniforms:Yn([ui.common,ui.bumpmap,ui.normalmap,ui.displacementmap,{opacity:{value:1}}]),vertexShader:hi.normal_vert,fragmentShader:hi.normal_frag},sprite:{uniforms:Yn([ui.sprite,ui.fog]),vertexShader:hi.sprite_vert,fragmentShader:hi.sprite_frag},background:{uniforms:{uvTransform:{value:new yt},t2D:{value:null}},vertexShader:hi.background_vert,fragmentShader:hi.background_frag},cube:{uniforms:Yn([ui.envmap,{opacity:{value:1}}]),vertexShader:hi.cube_vert,fragmentShader:hi.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:hi.equirect_vert,fragmentShader:hi.equirect_frag},distanceRGBA:{uniforms:Yn([ui.common,ui.displacementmap,{referencePosition:{value:new Lt},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:hi.distanceRGBA_vert,fragmentShader:hi.distanceRGBA_frag},shadow:{uniforms:Yn([ui.lights,ui.fog,{color:{value:new tn(0)},opacity:{value:1}}]),vertexShader:hi.shadow_vert,fragmentShader:hi.shadow_frag}};function pi(t,e,n,i,r){const s=new tn(0);let a,o,c=0,h=null,u=0,d=null;function p(t,e){n.buffers.color.setClear(t.r,t.g,t.b,e,r)}return{getClearColor:function(){return s},setClearColor:function(t,e=1){s.set(t),c=e,p(s,c)},getClearAlpha:function(){return c},setClearAlpha:function(t){c=t,p(s,c)},render:function(n,r,m,f){let g=!0===r.isScene?r.background:null;g&&g.isTexture&&(g=e.get(g));const v=t.xr,y=v.getSession&&v.getSession();y&&"additive"===y.environmentBlendMode&&(g=null),null===g?p(s,c):g&&g.isColor&&(p(g,1),f=!0),(t.autoClear||f)&&t.clear(t.autoClearColor,t.autoClearDepth,t.autoClearStencil),g&&(g.isCubeTexture||g.mapping===l)?(void 0===o&&(o=new Wn(new qn(1,1,1),new Jn({name:"BackgroundCubeMaterial",uniforms:Xn(di.cube.uniforms),vertexShader:di.cube.vertexShader,fragmentShader:di.cube.fragmentShader,side:1,depthTest:!1,depthWrite:!1,fog:!1})),o.geometry.deleteAttribute("normal"),o.geometry.deleteAttribute("uv"),o.onBeforeRender=function(t,e,n){this.matrixWorld.copyPosition(n.matrixWorld)},Object.defineProperty(o.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),i.update(o)),o.material.uniforms.envMap.value=g,o.material.uniforms.flipEnvMap.value=g.isCubeTexture&&g._needsFlipEnvMap?-1:1,h===g&&u===g.version&&d===t.toneMapping||(o.material.needsUpdate=!0,h=g,u=g.version,d=t.toneMapping),n.unshift(o,o.geometry,o.material,0,0,null)):g&&g.isTexture&&(void 0===a&&(a=new Wn(new ci(2,2),new Jn({name:"BackgroundMaterial",uniforms:Xn(di.background.uniforms),vertexShader:di.background.vertexShader,fragmentShader:di.background.fragmentShader,side:0,depthTest:!1,depthWrite:!1,fog:!1})),a.geometry.deleteAttribute("normal"),Object.defineProperty(a.material,"map",{get:function(){return this.uniforms.t2D.value}}),i.update(a)),a.material.uniforms.t2D.value=g,!0===g.matrixAutoUpdate&&g.updateMatrix(),a.material.uniforms.uvTransform.value.copy(g.matrix),h===g&&u===g.version&&d===t.toneMapping||(a.material.needsUpdate=!0,h=g,u=g.version,d=t.toneMapping),n.unshift(a,a.geometry,a.material,0,0,null))}}}function mi(t,e,n,i){const r=t.getParameter(34921),s=i.isWebGL2?null:e.get("OES_vertex_array_object"),a=i.isWebGL2||null!==s,o={},l=d(null);let c=l;function h(e){return i.isWebGL2?t.bindVertexArray(e):s.bindVertexArrayOES(e)}function u(e){return i.isWebGL2?t.deleteVertexArray(e):s.deleteVertexArrayOES(e)}function d(t){const e=[],n=[],i=[];for(let t=0;t=0){const s=l[e];if(void 0!==s){const e=s.normalized,r=s.itemSize,a=n.get(s);if(void 0===a)continue;const l=a.buffer,c=a.type,h=a.bytesPerElement;if(s.isInterleavedBufferAttribute){const n=s.data,a=n.stride,u=s.offset;n&&n.isInstancedInterleavedBuffer?(f(i,n.meshPerAttribute),void 0===o._maxInstanceCount&&(o._maxInstanceCount=n.meshPerAttribute*n.count)):m(i),t.bindBuffer(34962,l),v(i,r,c,e,a*h,u*h)}else s.isInstancedBufferAttribute?(f(i,s.meshPerAttribute),void 0===o._maxInstanceCount&&(o._maxInstanceCount=s.meshPerAttribute*s.count)):m(i),t.bindBuffer(34962,l),v(i,r,c,e,0,0)}else if("instanceMatrix"===e){const e=n.get(r.instanceMatrix);if(void 0===e)continue;const s=e.buffer,a=e.type;f(i+0,1),f(i+1,1),f(i+2,1),f(i+3,1),t.bindBuffer(34962,s),t.vertexAttribPointer(i+0,4,a,!1,64,0),t.vertexAttribPointer(i+1,4,a,!1,64,16),t.vertexAttribPointer(i+2,4,a,!1,64,32),t.vertexAttribPointer(i+3,4,a,!1,64,48)}else if("instanceColor"===e){const e=n.get(r.instanceColor);if(void 0===e)continue;const s=e.buffer,a=e.type;f(i,1),t.bindBuffer(34962,s),t.vertexAttribPointer(i,3,a,!1,12,0)}else if(void 0!==h){const n=h[e];if(void 0!==n)switch(n.length){case 2:t.vertexAttrib2fv(i,n);break;case 3:t.vertexAttrib3fv(i,n);break;case 4:t.vertexAttrib4fv(i,n);break;default:t.vertexAttrib1fv(i,n)}}}}g()}(r,l,u,y),null!==x&&t.bindBuffer(34963,n.get(x).buffer))},reset:y,resetDefaultState:x,dispose:function(){y();for(const t in o){const e=o[t];for(const t in e){const n=e[t];for(const t in n)u(n[t].object),delete n[t];delete e[t]}delete o[t]}},releaseStatesOfGeometry:function(t){if(void 0===o[t.id])return;const e=o[t.id];for(const t in e){const n=e[t];for(const t in n)u(n[t].object),delete n[t];delete e[t]}delete o[t.id]},releaseStatesOfProgram:function(t){for(const e in o){const n=o[e];if(void 0===n[t.id])continue;const i=n[t.id];for(const t in i)u(i[t].object),delete i[t];delete n[t.id]}},initAttributes:p,enableAttribute:m,disableUnusedAttributes:g}}function fi(t,e,n,i){const r=i.isWebGL2;let s;this.setMode=function(t){s=t},this.render=function(e,i){t.drawArrays(s,e,i),n.update(i,s,1)},this.renderInstances=function(i,a,o){if(0===o)return;let l,c;if(r)l=t,c="drawArraysInstanced";else if(l=e.get("ANGLE_instanced_arrays"),c="drawArraysInstancedANGLE",null===l)return void console.error("THREE.WebGLBufferRenderer: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays.");l[c](s,i,a,o),n.update(a,s,o)}}function gi(t,e,n){let i;function r(e){if("highp"===e){if(t.getShaderPrecisionFormat(35633,36338).precision>0&&t.getShaderPrecisionFormat(35632,36338).precision>0)return"highp";e="mediump"}return"mediump"===e&&t.getShaderPrecisionFormat(35633,36337).precision>0&&t.getShaderPrecisionFormat(35632,36337).precision>0?"mediump":"lowp"}const s="undefined"!=typeof WebGL2RenderingContext&&t instanceof WebGL2RenderingContext||"undefined"!=typeof WebGL2ComputeRenderingContext&&t instanceof WebGL2ComputeRenderingContext;let a=void 0!==n.precision?n.precision:"highp";const o=r(a);o!==a&&(console.warn("THREE.WebGLRenderer:",a,"not supported, using",o,"instead."),a=o);const l=!0===n.logarithmicDepthBuffer,c=t.getParameter(34930),h=t.getParameter(35660),u=t.getParameter(3379),d=t.getParameter(34076),p=t.getParameter(34921),m=t.getParameter(36347),f=t.getParameter(36348),g=t.getParameter(36349),v=h>0,y=s||e.has("OES_texture_float");return{isWebGL2:s,getMaxAnisotropy:function(){if(void 0!==i)return i;if(!0===e.has("EXT_texture_filter_anisotropic")){const n=e.get("EXT_texture_filter_anisotropic");i=t.getParameter(n.MAX_TEXTURE_MAX_ANISOTROPY_EXT)}else i=0;return i},getMaxPrecision:r,precision:a,logarithmicDepthBuffer:l,maxTextures:c,maxVertexTextures:h,maxTextureSize:u,maxCubemapSize:d,maxAttributes:p,maxVertexUniforms:m,maxVaryings:f,maxFragmentUniforms:g,vertexTextures:v,floatFragmentTextures:y,floatVertexTextures:v&&y,maxSamples:s?t.getParameter(36183):0}}function vi(t){const e=this;let n=null,i=0,r=!1,s=!1;const a=new Ne,o=new yt,l={value:null,needsUpdate:!1};function c(){l.value!==n&&(l.value=n,l.needsUpdate=i>0),e.numPlanes=i,e.numIntersection=0}function h(t,n,i,r){const s=null!==t?t.length:0;let c=null;if(0!==s){if(c=l.value,!0!==r||null===c){const e=i+4*s,r=n.matrixWorldInverse;o.getNormalMatrix(r),(null===c||c.length0){const a=t.getRenderTarget(),o=new ni(s.height/2);return o.fromEquirectangularTexture(t,r),e.set(r,o),t.setRenderTarget(a),r.addEventListener("dispose",i),n(o.texture,r.mapping)}return null}}}return r},dispose:function(){e=new WeakMap}}}function xi(t){const e={};function n(n){if(void 0!==e[n])return e[n];let i;switch(n){case"WEBGL_depth_texture":i=t.getExtension("WEBGL_depth_texture")||t.getExtension("MOZ_WEBGL_depth_texture")||t.getExtension("WEBKIT_WEBGL_depth_texture");break;case"EXT_texture_filter_anisotropic":i=t.getExtension("EXT_texture_filter_anisotropic")||t.getExtension("MOZ_EXT_texture_filter_anisotropic")||t.getExtension("WEBKIT_EXT_texture_filter_anisotropic");break;case"WEBGL_compressed_texture_s3tc":i=t.getExtension("WEBGL_compressed_texture_s3tc")||t.getExtension("MOZ_WEBGL_compressed_texture_s3tc")||t.getExtension("WEBKIT_WEBGL_compressed_texture_s3tc");break;case"WEBGL_compressed_texture_pvrtc":i=t.getExtension("WEBGL_compressed_texture_pvrtc")||t.getExtension("WEBKIT_WEBGL_compressed_texture_pvrtc");break;default:i=t.getExtension(n)}return e[n]=i,i}return{has:function(t){return null!==n(t)},init:function(t){t.isWebGL2?n("EXT_color_buffer_float"):(n("WEBGL_depth_texture"),n("OES_texture_float"),n("OES_texture_half_float"),n("OES_texture_half_float_linear"),n("OES_standard_derivatives"),n("OES_element_index_uint"),n("OES_vertex_array_object"),n("ANGLE_instanced_arrays")),n("OES_texture_float_linear"),n("EXT_color_buffer_half_float")},get:function(t){const e=n(t);return null===e&&console.warn("THREE.WebGLRenderer: "+t+" extension not supported."),e}}}function _i(t,e,n,i){const r={},s=new WeakMap;function a(t){const o=t.target;null!==o.index&&e.remove(o.index);for(const t in o.attributes)e.remove(o.attributes[t]);o.removeEventListener("dispose",a),delete r[o.id];const l=s.get(o);l&&(e.remove(l),s.delete(o)),i.releaseStatesOfGeometry(o),!0===o.isInstancedBufferGeometry&&delete o._maxInstanceCount,n.memory.geometries--}function o(t){const n=[],i=t.index,r=t.attributes.position;let a=0;if(null!==i){const t=i.array;a=i.version;for(let e=0,i=t.length;e65535?dn:hn)(n,1);o.version=a;const l=s.get(t);l&&e.remove(l),s.set(t,o)}return{get:function(t,e){return!0===r[e.id]||(e.addEventListener("dispose",a),r[e.id]=!0,n.memory.geometries++),e},update:function(t){const n=t.attributes;for(const t in n)e.update(n[t],34962);const i=t.morphAttributes;for(const t in i){const n=i[t];for(let t=0,i=n.length;t0)return t;const r=e*n;let s=Ii[r];if(void 0===s&&(s=new Float32Array(r),Ii[r]=s),0!==e){i.toArray(s,0);for(let i=1,r=0;i!==e;++i)r+=n,t[i].toArray(s,r)}return s}function Hi(t,e){if(t.length!==e.length)return!1;for(let n=0,i=t.length;n/gm;function kr(t){return t.replace(Ur,Vr)}function Vr(t,e){const n=hi[e];if(void 0===n)throw new Error("Can not resolve #include <"+e+">");return kr(n)}const Wr=/#pragma unroll_loop[\s]+?for \( int i \= (\d+)\; i < (\d+)\; i \+\+ \) \{([\s\S]+?)(?=\})\}/g,jr=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function qr(t){return t.replace(jr,Yr).replace(Wr,Xr)}function Xr(t,e,n,i){return console.warn("WebGLProgram: #pragma unroll_loop shader syntax is deprecated. Please use #pragma unroll_loop_start syntax instead."),Yr(t,e,n,i)}function Yr(t,e,n,i){let r="";for(let t=parseInt(e);t0?t.gammaFactor:1,v=n.isWebGL2?"":function(t){return[t.extensionDerivatives||t.envMapCubeUV||t.bumpMap||t.tangentSpaceNormalMap||t.clearcoatNormalMap||t.flatShading||"physical"===t.shaderID?"#extension GL_OES_standard_derivatives : enable":"",(t.extensionFragDepth||t.logarithmicDepthBuffer)&&t.rendererExtensionFragDepth?"#extension GL_EXT_frag_depth : enable":"",t.extensionDrawBuffers&&t.rendererExtensionDrawBuffers?"#extension GL_EXT_draw_buffers : require":"",(t.extensionShaderTextureLOD||t.envMap)&&t.rendererExtensionShaderTextureLod?"#extension GL_EXT_shader_texture_lod : enable":""].filter(Or).join("\n")}(n),y=function(t){const e=[];for(const n in t){const i=t[n];!1!==i&&e.push("#define "+n+" "+i)}return e.join("\n")}(o),x=a.createProgram();let _,w,b=n.glslVersion?"#version "+n.glslVersion+"\n":"";n.isRawShaderMaterial?(_=[y].filter(Or).join("\n"),_.length>0&&(_+="\n"),w=[v,y].filter(Or).join("\n"),w.length>0&&(w+="\n")):(_=[Zr(n),"#define SHADER_NAME "+n.shaderName,y,n.instancing?"#define USE_INSTANCING":"",n.instancingColor?"#define USE_INSTANCING_COLOR":"",n.supportsVertexTextures?"#define VERTEX_TEXTURES":"","#define GAMMA_FACTOR "+g,"#define MAX_BONES "+n.maxBones,n.useFog&&n.fog?"#define USE_FOG":"",n.useFog&&n.fogExp2?"#define FOG_EXP2":"",n.map?"#define USE_MAP":"",n.envMap?"#define USE_ENVMAP":"",n.envMap?"#define "+m:"",n.lightMap?"#define USE_LIGHTMAP":"",n.aoMap?"#define USE_AOMAP":"",n.emissiveMap?"#define USE_EMISSIVEMAP":"",n.bumpMap?"#define USE_BUMPMAP":"",n.normalMap?"#define USE_NORMALMAP":"",n.normalMap&&n.objectSpaceNormalMap?"#define OBJECTSPACE_NORMALMAP":"",n.normalMap&&n.tangentSpaceNormalMap?"#define TANGENTSPACE_NORMALMAP":"",n.clearcoatMap?"#define USE_CLEARCOATMAP":"",n.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",n.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",n.displacementMap&&n.supportsVertexTextures?"#define USE_DISPLACEMENTMAP":"",n.specularMap?"#define USE_SPECULARMAP":"",n.roughnessMap?"#define USE_ROUGHNESSMAP":"",n.metalnessMap?"#define USE_METALNESSMAP":"",n.alphaMap?"#define USE_ALPHAMAP":"",n.transmissionMap?"#define USE_TRANSMISSIONMAP":"",n.vertexTangents?"#define USE_TANGENT":"",n.vertexColors?"#define USE_COLOR":"",n.vertexAlphas?"#define USE_COLOR_ALPHA":"",n.vertexUvs?"#define USE_UV":"",n.uvsVertexOnly?"#define UVS_VERTEX_ONLY":"",n.flatShading?"#define FLAT_SHADED":"",n.skinning?"#define USE_SKINNING":"",n.useVertexTexture?"#define BONE_TEXTURE":"",n.morphTargets?"#define USE_MORPHTARGETS":"",n.morphNormals&&!1===n.flatShading?"#define USE_MORPHNORMALS":"",n.doubleSided?"#define DOUBLE_SIDED":"",n.flipSided?"#define FLIP_SIDED":"",n.shadowMapEnabled?"#define USE_SHADOWMAP":"",n.shadowMapEnabled?"#define "+d:"",n.sizeAttenuation?"#define USE_SIZEATTENUATION":"",n.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",n.logarithmicDepthBuffer&&n.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING","\tattribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR","\tattribute vec3 instanceColor;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_TANGENT","\tattribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )","\tattribute vec4 color;","#elif defined( USE_COLOR )","\tattribute vec3 color;","#endif","#ifdef USE_MORPHTARGETS","\tattribute vec3 morphTarget0;","\tattribute vec3 morphTarget1;","\tattribute vec3 morphTarget2;","\tattribute vec3 morphTarget3;","\t#ifdef USE_MORPHNORMALS","\t\tattribute vec3 morphNormal0;","\t\tattribute vec3 morphNormal1;","\t\tattribute vec3 morphNormal2;","\t\tattribute vec3 morphNormal3;","\t#else","\t\tattribute vec3 morphTarget4;","\t\tattribute vec3 morphTarget5;","\t\tattribute vec3 morphTarget6;","\t\tattribute vec3 morphTarget7;","\t#endif","#endif","#ifdef USE_SKINNING","\tattribute vec4 skinIndex;","\tattribute vec4 skinWeight;","#endif","\n"].filter(Or).join("\n"),w=[v,Zr(n),"#define SHADER_NAME "+n.shaderName,y,n.alphaTest?"#define ALPHATEST "+n.alphaTest+(n.alphaTest%1?"":".0"):"","#define GAMMA_FACTOR "+g,n.useFog&&n.fog?"#define USE_FOG":"",n.useFog&&n.fogExp2?"#define FOG_EXP2":"",n.map?"#define USE_MAP":"",n.matcap?"#define USE_MATCAP":"",n.envMap?"#define USE_ENVMAP":"",n.envMap?"#define "+p:"",n.envMap?"#define "+m:"",n.envMap?"#define "+f:"",n.lightMap?"#define USE_LIGHTMAP":"",n.aoMap?"#define USE_AOMAP":"",n.emissiveMap?"#define USE_EMISSIVEMAP":"",n.bumpMap?"#define USE_BUMPMAP":"",n.normalMap?"#define USE_NORMALMAP":"",n.normalMap&&n.objectSpaceNormalMap?"#define OBJECTSPACE_NORMALMAP":"",n.normalMap&&n.tangentSpaceNormalMap?"#define TANGENTSPACE_NORMALMAP":"",n.clearcoatMap?"#define USE_CLEARCOATMAP":"",n.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",n.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",n.specularMap?"#define USE_SPECULARMAP":"",n.roughnessMap?"#define USE_ROUGHNESSMAP":"",n.metalnessMap?"#define USE_METALNESSMAP":"",n.alphaMap?"#define USE_ALPHAMAP":"",n.sheen?"#define USE_SHEEN":"",n.transmissionMap?"#define USE_TRANSMISSIONMAP":"",n.vertexTangents?"#define USE_TANGENT":"",n.vertexColors||n.instancingColor?"#define USE_COLOR":"",n.vertexAlphas?"#define USE_COLOR_ALPHA":"",n.vertexUvs?"#define USE_UV":"",n.uvsVertexOnly?"#define UVS_VERTEX_ONLY":"",n.gradientMap?"#define USE_GRADIENTMAP":"",n.flatShading?"#define FLAT_SHADED":"",n.doubleSided?"#define DOUBLE_SIDED":"",n.flipSided?"#define FLIP_SIDED":"",n.shadowMapEnabled?"#define USE_SHADOWMAP":"",n.shadowMapEnabled?"#define "+d:"",n.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",n.physicallyCorrectLights?"#define PHYSICALLY_CORRECT_LIGHTS":"",n.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",n.logarithmicDepthBuffer&&n.rendererExtensionFragDepth?"#define USE_LOGDEPTHBUF_EXT":"",(n.extensionShaderTextureLOD||n.envMap)&&n.rendererExtensionShaderTextureLod?"#define TEXTURE_LOD_EXT":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",0!==n.toneMapping?"#define TONE_MAPPING":"",0!==n.toneMapping?hi.tonemapping_pars_fragment:"",0!==n.toneMapping?Fr("toneMapping",n.toneMapping):"",n.dithering?"#define DITHERING":"",hi.encodings_pars_fragment,n.map?Br("mapTexelToLinear",n.mapEncoding):"",n.matcap?Br("matcapTexelToLinear",n.matcapEncoding):"",n.envMap?Br("envMapTexelToLinear",n.envMapEncoding):"",n.emissiveMap?Br("emissiveMapTexelToLinear",n.emissiveMapEncoding):"",n.lightMap?Br("lightMapTexelToLinear",n.lightMapEncoding):"",zr("linearToOutputTexel",n.outputEncoding),n.depthPacking?"#define DEPTH_PACKING "+n.depthPacking:"","\n"].filter(Or).join("\n")),h=kr(h),h=Hr(h,n),h=Gr(h,n),u=kr(u),u=Hr(u,n),u=Gr(u,n),h=qr(h),u=qr(u),n.isWebGL2&&!0!==n.isRawShaderMaterial&&(b="#version 300 es\n",_=["#define attribute in","#define varying out","#define texture2D texture"].join("\n")+"\n"+_,w=["#define varying in",n.glslVersion===it?"":"out highp vec4 pc_fragColor;",n.glslVersion===it?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join("\n")+"\n"+w);const M=b+w+u,S=Pr(a,35633,b+_+h),T=Pr(a,35632,M);if(a.attachShader(x,S),a.attachShader(x,T),void 0!==n.index0AttributeName?a.bindAttribLocation(x,0,n.index0AttributeName):!0===n.morphTargets&&a.bindAttribLocation(x,0,"position"),a.linkProgram(x),t.debug.checkShaderErrors){const t=a.getProgramInfoLog(x).trim(),e=a.getShaderInfoLog(S).trim(),n=a.getShaderInfoLog(T).trim();let i=!0,r=!0;if(!1===a.getProgramParameter(x,35714)){i=!1;const e=Nr(a,S,"vertex"),n=Nr(a,T,"fragment");console.error("THREE.WebGLProgram: shader error: ",a.getError(),"35715",a.getProgramParameter(x,35715),"gl.getProgramInfoLog",t,e,n)}else""!==t?console.warn("THREE.WebGLProgram: gl.getProgramInfoLog()",t):""!==e&&""!==n||(r=!1);r&&(this.diagnostics={runnable:i,programLog:t,vertexShader:{log:e,prefix:_},fragmentShader:{log:n,prefix:w}})}let E,A;return a.deleteShader(S),a.deleteShader(T),this.getUniforms=function(){return void 0===E&&(E=new Cr(a,x)),E},this.getAttributes=function(){return void 0===A&&(A=function(t,e){const n={},i=t.getProgramParameter(e,35721);for(let r=0;r0,maxBones:S,useVertexTexture:u,morphTargets:r.morphTargets,morphNormals:r.morphNormals,numDirLights:a.directional.length,numPointLights:a.point.length,numSpotLights:a.spot.length,numRectAreaLights:a.rectArea.length,numHemiLights:a.hemi.length,numDirLightShadows:a.directionalShadowMap.length,numPointLightShadows:a.pointShadowMap.length,numSpotLightShadows:a.spotShadowMap.length,numClippingPlanes:s.numPlanes,numClipIntersection:s.numIntersection,dithering:r.dithering,shadowMapEnabled:t.shadowMap.enabled&&g.length>0,shadowMapType:t.shadowMap.type,toneMapping:r.toneMapped?t.toneMapping:0,physicallyCorrectLights:t.physicallyCorrectLights,premultipliedAlpha:r.premultipliedAlpha,alphaTest:r.alphaTest,doubleSided:2===r.side,flipSided:1===r.side,depthPacking:void 0!==r.depthPacking&&r.depthPacking,index0AttributeName:r.index0AttributeName,extensionDerivatives:r.extensions&&r.extensions.derivatives,extensionFragDepth:r.extensions&&r.extensions.fragDepth,extensionDrawBuffers:r.extensions&&r.extensions.drawBuffers,extensionShaderTextureLOD:r.extensions&&r.extensions.shaderTextureLOD,rendererExtensionFragDepth:o||n.has("EXT_frag_depth"),rendererExtensionDrawBuffers:o||n.has("WEBGL_draw_buffers"),rendererExtensionShaderTextureLod:o||n.has("EXT_shader_texture_lod"),customProgramCacheKey:r.customProgramCacheKey()}},getProgramCacheKey:function(e){const n=[];if(e.shaderID?n.push(e.shaderID):(n.push(e.fragmentShader),n.push(e.vertexShader)),void 0!==e.defines)for(const t in e.defines)n.push(t),n.push(e.defines[t]);if(!1===e.isRawShaderMaterial){for(let t=0;t1&&i.sort(t||$r),r.length>1&&r.sort(e||ts)}}}function ns(t){let e=new WeakMap;return{get:function(n,i){let r;return!1===e.has(n)?(r=new es(t),e.set(n,[r])):i>=e.get(n).length?(r=new es(t),e.get(n).push(r)):r=e.get(n)[i],r},dispose:function(){e=new WeakMap}}}function is(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let n;switch(e.type){case"DirectionalLight":n={direction:new Lt,color:new tn};break;case"SpotLight":n={position:new Lt,direction:new Lt,color:new tn,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":n={position:new Lt,color:new tn,distance:0,decay:0};break;case"HemisphereLight":n={direction:new Lt,skyColor:new tn,groundColor:new tn};break;case"RectAreaLight":n={color:new tn,position:new Lt,halfWidth:new Lt,halfHeight:new Lt}}return t[e.id]=n,n}}}let rs=0;function ss(t,e){return(e.castShadow?1:0)-(t.castShadow?1:0)}function as(t,e){const n=new is,i=function(){const t={};return{get:function(e){if(void 0!==t[e.id])return t[e.id];let n;switch(e.type){case"DirectionalLight":case"SpotLight":n={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new vt};break;case"PointLight":n={shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new vt,shadowCameraNear:1,shadowCameraFar:1e3}}return t[e.id]=n,n}}}(),r={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotShadow:[],spotShadowMap:[],spotShadowMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[]};for(let t=0;t<9;t++)r.probe.push(new Lt);const s=new Lt,a=new se,o=new se;return{setup:function(s){let a=0,o=0,l=0;for(let t=0;t<9;t++)r.probe[t].set(0,0,0);let c=0,h=0,u=0,d=0,p=0,m=0,f=0,g=0;s.sort(ss);for(let t=0,e=s.length;t0&&(e.isWebGL2||!0===t.has("OES_texture_float_linear")?(r.rectAreaLTC1=ui.LTC_FLOAT_1,r.rectAreaLTC2=ui.LTC_FLOAT_2):!0===t.has("OES_texture_half_float_linear")?(r.rectAreaLTC1=ui.LTC_HALF_1,r.rectAreaLTC2=ui.LTC_HALF_2):console.error("THREE.WebGLRenderer: Unable to use RectAreaLight. Missing WebGL extensions.")),r.ambient[0]=a,r.ambient[1]=o,r.ambient[2]=l;const v=r.hash;v.directionalLength===c&&v.pointLength===h&&v.spotLength===u&&v.rectAreaLength===d&&v.hemiLength===p&&v.numDirectionalShadows===m&&v.numPointShadows===f&&v.numSpotShadows===g||(r.directional.length=c,r.spot.length=u,r.rectArea.length=d,r.point.length=h,r.hemi.length=p,r.directionalShadow.length=m,r.directionalShadowMap.length=m,r.pointShadow.length=f,r.pointShadowMap.length=f,r.spotShadow.length=g,r.spotShadowMap.length=g,r.directionalShadowMatrix.length=m,r.pointShadowMatrix.length=f,r.spotShadowMatrix.length=g,v.directionalLength=c,v.pointLength=h,v.spotLength=u,v.rectAreaLength=d,v.hemiLength=p,v.numDirectionalShadows=m,v.numPointShadows=f,v.numSpotShadows=g,r.version=rs++)},setupView:function(t,e){let n=0,i=0,l=0,c=0,h=0;const u=e.matrixWorldInverse;for(let e=0,d=t.length;e=n.get(i).length?(s=new os(t,e),n.get(i).push(s)):s=n.get(i)[r],s},dispose:function(){n=new WeakMap}}}class cs extends Xe{constructor(t){super(),this.type="MeshDepthMaterial",this.depthPacking=3200,this.skinning=!1,this.morphTargets=!1,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.wireframe=!1,this.wireframeLinewidth=1,this.fog=!1,this.setValues(t)}copy(t){return super.copy(t),this.depthPacking=t.depthPacking,this.skinning=t.skinning,this.morphTargets=t.morphTargets,this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this}}cs.prototype.isMeshDepthMaterial=!0;class hs extends Xe{constructor(t){super(),this.type="MeshDistanceMaterial",this.referencePosition=new Lt,this.nearDistance=1,this.farDistance=1e3,this.skinning=!1,this.morphTargets=!1,this.map=null,this.alphaMap=null,this.displacementMap=null,this.displacementScale=1,this.displacementBias=0,this.fog=!1,this.setValues(t)}copy(t){return super.copy(t),this.referencePosition.copy(t.referencePosition),this.nearDistance=t.nearDistance,this.farDistance=t.farDistance,this.skinning=t.skinning,this.morphTargets=t.morphTargets,this.map=t.map,this.alphaMap=t.alphaMap,this.displacementMap=t.displacementMap,this.displacementScale=t.displacementScale,this.displacementBias=t.displacementBias,this}}hs.prototype.isMeshDistanceMaterial=!0;function us(t,e,n){let i=new ai;const r=new vt,s=new vt,a=new St,o=[],l=[],c={},h=n.maxTextureSize,u={0:1,1:0,2:2},d=new Jn({defines:{SAMPLE_RATE:2/8,HALF_SAMPLE_RATE:1/8},uniforms:{shadow_pass:{value:null},resolution:{value:new vt},radius:{value:4}},vertexShader:"void main() {\n\tgl_Position = vec4( position, 1.0 );\n}",fragmentShader:"uniform sampler2D shadow_pass;\nuniform vec2 resolution;\nuniform float radius;\n#include \nvoid main() {\n\tfloat mean = 0.0;\n\tfloat squared_mean = 0.0;\n\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy ) / resolution ) );\n\tfor ( float i = -1.0; i < 1.0 ; i += SAMPLE_RATE) {\n\t\t#ifdef HORIZONTAL_PASS\n\t\t\tvec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( i, 0.0 ) * radius ) / resolution ) );\n\t\t\tmean += distribution.x;\n\t\t\tsquared_mean += distribution.y * distribution.y + distribution.x * distribution.x;\n\t\t#else\n\t\t\tfloat depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, i ) * radius ) / resolution ) );\n\t\t\tmean += depth;\n\t\t\tsquared_mean += depth * depth;\n\t\t#endif\n\t}\n\tmean = mean * HALF_SAMPLE_RATE;\n\tsquared_mean = squared_mean * HALF_SAMPLE_RATE;\n\tfloat std_dev = sqrt( squared_mean - mean * mean );\n\tgl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) );\n}"}),m=d.clone();m.defines.HORIZONTAL_PASS=1;const f=new En;f.setAttribute("position",new sn(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const v=new Wn(f,d),y=this;function x(n,i){const r=e.update(v);d.uniforms.shadow_pass.value=n.map.texture,d.uniforms.resolution.value=n.mapSize,d.uniforms.radius.value=n.radius,t.setRenderTarget(n.mapPass),t.clear(),t.renderBufferDirect(i,null,r,d,v,null),m.uniforms.shadow_pass.value=n.mapPass.texture,m.uniforms.resolution.value=n.mapSize,m.uniforms.radius.value=n.radius,t.setRenderTarget(n.map),t.clear(),t.renderBufferDirect(i,null,r,m,v,null)}function _(t,e,n){const i=t<<0|e<<1|n<<2;let r=o[i];return void 0===r&&(r=new cs({depthPacking:3201,morphTargets:t,skinning:e}),o[i]=r),r}function w(t,e,n){const i=t<<0|e<<1|n<<2;let r=l[i];return void 0===r&&(r=new hs({morphTargets:t,skinning:e}),l[i]=r),r}function b(e,n,i,r,s,a,o){let l=null,h=_,d=e.customDepthMaterial;if(!0===r.isPointLight&&(h=w,d=e.customDistanceMaterial),void 0===d){let t=!1;!0===i.morphTargets&&(t=n.morphAttributes&&n.morphAttributes.position&&n.morphAttributes.position.length>0);let r=!1;!0===e.isSkinnedMesh&&(!0===i.skinning?r=!0:console.warn("THREE.WebGLShadowMap: THREE.SkinnedMesh with material.skinning set to false:",e));l=h(t,r,!0===e.isInstancedMesh)}else l=d;if(t.localClippingEnabled&&!0===i.clipShadows&&0!==i.clippingPlanes.length){const t=l.uuid,e=i.uuid;let n=c[t];void 0===n&&(n={},c[t]=n);let r=n[e];void 0===r&&(r=l.clone(),n[e]=r),l=r}return l.visible=i.visible,l.wireframe=i.wireframe,l.side=3===o?null!==i.shadowSide?i.shadowSide:i.side:null!==i.shadowSide?i.shadowSide:u[i.side],l.clipShadows=i.clipShadows,l.clippingPlanes=i.clippingPlanes,l.clipIntersection=i.clipIntersection,l.wireframeLinewidth=i.wireframeLinewidth,l.linewidth=i.linewidth,!0===r.isPointLight&&!0===l.isMeshDistanceMaterial&&(l.referencePosition.setFromMatrixPosition(r.matrixWorld),l.nearDistance=s,l.farDistance=a),l}function M(n,r,s,a,o){if(!1===n.visible)return;if(n.layers.test(r.layers)&&(n.isMesh||n.isLine||n.isPoints)&&(n.castShadow||n.receiveShadow&&3===o)&&(!n.frustumCulled||i.intersectsObject(n))){n.modelViewMatrix.multiplyMatrices(s.matrixWorldInverse,n.matrixWorld);const i=e.update(n),r=n.material;if(Array.isArray(r)){const e=i.groups;for(let l=0,c=e.length;lh||r.y>h)&&(r.x>h&&(s.x=Math.floor(h/m.x),r.x=s.x*m.x,u.mapSize.x=s.x),r.y>h&&(s.y=Math.floor(h/m.y),r.y=s.y*m.y,u.mapSize.y=s.y)),null===u.map&&!u.isPointLightShadow&&3===this.type){const t={minFilter:g,magFilter:g,format:E};u.map=new Tt(r.x,r.y,t),u.map.texture.name=c.name+".shadowMap",u.mapPass=new Tt(r.x,r.y,t),u.camera.updateProjectionMatrix()}if(null===u.map){const t={minFilter:p,magFilter:p,format:E};u.map=new Tt(r.x,r.y,t),u.map.texture.name=c.name+".shadowMap",u.camera.updateProjectionMatrix()}t.setRenderTarget(u.map),t.clear();const f=u.getViewportCount();for(let t=0;t=1):-1!==R.indexOf("OpenGL ES")&&(L=parseFloat(/^OpenGL ES (\d)/.exec(R)[1]),A=L>=2);let C=null,P={};const D=new St(0,0,t.canvas.width,t.canvas.height),I=new St(0,0,t.canvas.width,t.canvas.height);function N(e,n,i){const r=new Uint8Array(4),s=t.createTexture();t.bindTexture(e,s),t.texParameteri(e,10241,9728),t.texParameteri(e,10240,9728);for(let e=0;ei||t.height>i)&&(r=i/Math.max(t.width,t.height)),r<1||!0===e){if("undefined"!=typeof HTMLImageElement&&t instanceof HTMLImageElement||"undefined"!=typeof HTMLCanvasElement&&t instanceof HTMLCanvasElement||"undefined"!=typeof ImageBitmap&&t instanceof ImageBitmap){const i=e?ft:Math.floor,s=i(r*t.width),a=i(r*t.height);void 0===P&&(P=I(s,a));const o=n?I(s,a):P;o.width=s,o.height=a;return o.getContext("2d").drawImage(t,0,0,s,a),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+t.width+"x"+t.height+") to ("+s+"x"+a+")."),o}return"data"in t&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+t.width+"x"+t.height+")."),t}return t}function B(t){return pt(t.width)&&pt(t.height)}function z(t,e){return t.generateMipmaps&&e&&t.minFilter!==p&&t.minFilter!==g}function F(e,n,r,s){t.generateMipmap(e);i.get(n).__maxMipLevel=Math.log2(Math.max(r,s))}function O(n,i,r){if(!1===o)return i;if(null!==n){if(void 0!==t[n])return t[n];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+n+"'")}let s=i;return 6403===i&&(5126===r&&(s=33326),5131===r&&(s=33325),5121===r&&(s=33321)),6407===i&&(5126===r&&(s=34837),5131===r&&(s=34843),5121===r&&(s=32849)),6408===i&&(5126===r&&(s=34836),5131===r&&(s=34842),5121===r&&(s=32856)),33325!==s&&33326!==s&&34842!==s&&34836!==s||e.get("EXT_color_buffer_float"),s}function H(t){return t===p||t===m||t===f?9728:9729}function G(e){const n=e.target;n.removeEventListener("dispose",G),function(e){const n=i.get(e);if(void 0===n.__webglInit)return;t.deleteTexture(n.__webglTexture),i.remove(e)}(n),n.isVideoTexture&&C.delete(n),a.memory.textures--}function U(e){const n=e.target;n.removeEventListener("dispose",U),function(e){const n=e.texture,r=i.get(e),s=i.get(n);if(!e)return;void 0!==s.__webglTexture&&t.deleteTexture(s.__webglTexture);e.depthTexture&&e.depthTexture.dispose();if(e.isWebGLCubeRenderTarget)for(let e=0;e<6;e++)t.deleteFramebuffer(r.__webglFramebuffer[e]),r.__webglDepthbuffer&&t.deleteRenderbuffer(r.__webglDepthbuffer[e]);else t.deleteFramebuffer(r.__webglFramebuffer),r.__webglDepthbuffer&&t.deleteRenderbuffer(r.__webglDepthbuffer),r.__webglMultisampledFramebuffer&&t.deleteFramebuffer(r.__webglMultisampledFramebuffer),r.__webglColorRenderbuffer&&t.deleteRenderbuffer(r.__webglColorRenderbuffer),r.__webglDepthRenderbuffer&&t.deleteRenderbuffer(r.__webglDepthRenderbuffer);i.remove(n),i.remove(e)}(n),a.memory.textures--}let k=0;function V(t,e){const r=i.get(t);if(t.isVideoTexture&&function(t){const e=a.render.frame;C.get(t)!==e&&(C.set(t,e),t.update())}(t),t.version>0&&r.__version!==t.version){const n=t.image;if(void 0===n)console.warn("THREE.WebGLRenderer: Texture marked for update but image is undefined");else{if(!1!==n.complete)return void Z(r,t,e);console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete")}}n.activeTexture(33984+e),n.bindTexture(3553,r.__webglTexture)}function W(e,r){const a=i.get(e);e.version>0&&a.__version!==e.version?function(e,i,r){if(6!==i.image.length)return;Y(e,i),n.activeTexture(33984+r),n.bindTexture(34067,e.__webglTexture),t.pixelStorei(37440,i.flipY),t.pixelStorei(37441,i.premultiplyAlpha),t.pixelStorei(3317,i.unpackAlignment),t.pixelStorei(37443,0);const a=i&&(i.isCompressedTexture||i.image[0].isCompressedTexture),l=i.image[0]&&i.image[0].isDataTexture,h=[];for(let t=0;t<6;t++)h[t]=a||l?l?i.image[t].image:i.image[t]:N(i.image[t],!1,!0,c);const u=h[0],d=B(u)||o,p=s.convert(i.format),m=s.convert(i.type),f=O(i.internalFormat,p,m);let g;if(X(34067,i,d),a){for(let t=0;t<6;t++){g=h[t].mipmaps;for(let e=0;e1||i.get(s).__currentAnisotropy)&&(t.texParameterf(n,a.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(s.anisotropy,r.getMaxAnisotropy())),i.get(s).__currentAnisotropy=s.anisotropy)}}function Y(e,n){void 0===e.__webglInit&&(e.__webglInit=!0,n.addEventListener("dispose",G),e.__webglTexture=t.createTexture(),a.memory.textures++)}function Z(e,i,r){let a=3553;i.isDataTexture2DArray&&(a=35866),i.isDataTexture3D&&(a=32879),Y(e,i),n.activeTexture(33984+r),n.bindTexture(a,e.__webglTexture),t.pixelStorei(37440,i.flipY),t.pixelStorei(37441,i.premultiplyAlpha),t.pixelStorei(3317,i.unpackAlignment),t.pixelStorei(37443,0);const l=function(t){return!o&&(t.wrapS!==u||t.wrapT!==u||t.minFilter!==p&&t.minFilter!==g)}(i)&&!1===B(i.image),c=N(i.image,l,!1,x),h=B(c)||o,d=s.convert(i.format);let m,f=s.convert(i.type),v=O(i.internalFormat,d,f);X(a,i,h);const y=i.mipmaps;if(i.isDepthTexture)v=6402,o?v=i.type===b?36012:i.type===w?33190:i.type===S?35056:33189:i.type===b&&console.error("WebGLRenderer: Floating point depth texture requires WebGL2."),i.format===A&&6402===v&&i.type!==_&&i.type!==w&&(console.warn("THREE.WebGLRenderer: Use UnsignedShortType or UnsignedIntType for DepthFormat DepthTexture."),i.type=_,f=s.convert(i.type)),i.format===L&&6402===v&&(v=34041,i.type!==S&&(console.warn("THREE.WebGLRenderer: Use UnsignedInt248Type for DepthStencilFormat DepthTexture."),i.type=S,f=s.convert(i.type))),n.texImage2D(3553,0,v,c.width,c.height,0,d,f,null);else if(i.isDataTexture)if(y.length>0&&h){for(let t=0,e=y.length;t0&&h){for(let t=0,e=y.length;t=l&&console.warn("THREE.WebGLTextures: Trying to use "+t+" texture units while this GPU supports only "+l),k+=1,t},this.resetTextureUnits=function(){k=0},this.setTexture2D=V,this.setTexture2DArray=function(t,e){const r=i.get(t);t.version>0&&r.__version!==t.version?Z(r,t,e):(n.activeTexture(33984+e),n.bindTexture(35866,r.__webglTexture))},this.setTexture3D=function(t,e){const r=i.get(t);t.version>0&&r.__version!==t.version?Z(r,t,e):(n.activeTexture(33984+e),n.bindTexture(32879,r.__webglTexture))},this.setTextureCube=W,this.setupRenderTarget=function(e){const r=e.texture,l=i.get(e),c=i.get(r);e.addEventListener("dispose",U),c.__webglTexture=t.createTexture(),c.__version=r.version,a.memory.textures++;const h=!0===e.isWebGLCubeRenderTarget,u=!0===e.isWebGLMultisampleRenderTarget,d=r.isDataTexture3D||r.isDataTexture2DArray,p=B(e)||o;if(!o||r.format!==T||r.type!==b&&r.type!==M||(r.format=E,console.warn("THREE.WebGLRenderer: Rendering to textures with RGB format is not supported. Using RGBA format instead.")),h){l.__webglFramebuffer=[];for(let e=0;e<6;e++)l.__webglFramebuffer[e]=t.createFramebuffer()}else if(l.__webglFramebuffer=t.createFramebuffer(),u)if(o){l.__webglMultisampledFramebuffer=t.createFramebuffer(),l.__webglColorRenderbuffer=t.createRenderbuffer(),t.bindRenderbuffer(36161,l.__webglColorRenderbuffer);const i=s.convert(r.format),a=s.convert(r.type),o=O(r.internalFormat,i,a),c=$(e);t.renderbufferStorageMultisample(36161,c,o,e.width,e.height),n.bindFramebuffer(36160,l.__webglMultisampledFramebuffer),t.framebufferRenderbuffer(36160,36064,36161,l.__webglColorRenderbuffer),t.bindRenderbuffer(36161,null),e.depthBuffer&&(l.__webglDepthRenderbuffer=t.createRenderbuffer(),Q(l.__webglDepthRenderbuffer,e,!0)),n.bindFramebuffer(36160,null)}else console.warn("THREE.WebGLRenderer: WebGLMultisampleRenderTarget can only be used with WebGL2.");if(h){n.bindTexture(34067,c.__webglTexture),X(34067,r,p);for(let t=0;t<6;t++)J(l.__webglFramebuffer[t],e,36064,34069+t);z(r,p)&&F(34067,r,e.width,e.height),n.bindTexture(34067,null)}else{let t=3553;if(d)if(o){t=r.isDataTexture3D?32879:35866}else console.warn("THREE.DataTexture3D and THREE.DataTexture2DArray only supported with WebGL2.");n.bindTexture(t,c.__webglTexture),X(t,r,p),J(l.__webglFramebuffer,e,36064,t),z(r,p)&&F(3553,r,e.width,e.height),n.bindTexture(3553,null)}e.depthBuffer&&K(e)},this.updateRenderTargetMipmap=function(t){const e=t.texture;if(z(e,B(t)||o)){const r=t.isWebGLCubeRenderTarget?34067:3553,s=i.get(e).__webglTexture;n.bindTexture(r,s),F(r,e,t.width,t.height),n.bindTexture(r,null)}},this.updateMultisampleRenderTarget=function(e){if(e.isWebGLMultisampleRenderTarget)if(o){const r=e.width,s=e.height;let a=16384;e.depthBuffer&&(a|=256),e.stencilBuffer&&(a|=1024);const o=i.get(e);n.bindFramebuffer(36008,o.__webglMultisampledFramebuffer),n.bindFramebuffer(36009,o.__webglFramebuffer),t.blitFramebuffer(0,0,r,s,0,0,r,s,a,9728),n.bindFramebuffer(36008,null),n.bindFramebuffer(36009,o.__webglMultisampledFramebuffer)}else console.warn("THREE.WebGLRenderer: WebGLMultisampleRenderTarget can only be used with WebGL2.")},this.safeSetTexture2D=function(t,e){t&&t.isWebGLRenderTarget&&(!1===tt&&(console.warn("THREE.WebGLTextures.safeSetTexture2D: don't use render targets as textures. Use their .texture property instead."),tt=!0),t=t.texture),V(t,e)},this.safeSetTextureCube=function(t,e){t&&t.isWebGLCubeRenderTarget&&(!1===et&&(console.warn("THREE.WebGLTextures.safeSetTextureCube: don't use cube render targets as textures. Use their .texture property instead."),et=!0),t=t.texture),W(t,e)}}function ms(t,e,n){const i=n.isWebGL2;return{convert:function(t){let n;if(t===x)return 5121;if(1017===t)return 32819;if(1018===t)return 32820;if(1019===t)return 33635;if(1010===t)return 5120;if(1011===t)return 5122;if(t===_)return 5123;if(1013===t)return 5124;if(t===w)return 5125;if(t===b)return 5126;if(t===M)return i?5131:(n=e.get("OES_texture_half_float"),null!==n?n.HALF_FLOAT_OES:null);if(1021===t)return 6406;if(t===T)return 6407;if(t===E)return 6408;if(1024===t)return 6409;if(1025===t)return 6410;if(t===A)return 6402;if(t===L)return 34041;if(1028===t)return 6403;if(1029===t)return 36244;if(1030===t)return 33319;if(1031===t)return 33320;if(1032===t)return 36248;if(1033===t)return 36249;if(t===R||t===C||t===P||t===D){if(n=e.get("WEBGL_compressed_texture_s3tc"),null===n)return null;if(t===R)return n.COMPRESSED_RGB_S3TC_DXT1_EXT;if(t===C)return n.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(t===P)return n.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(t===D)return n.COMPRESSED_RGBA_S3TC_DXT5_EXT}if(t===I||t===N||t===B||t===z){if(n=e.get("WEBGL_compressed_texture_pvrtc"),null===n)return null;if(t===I)return n.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(t===N)return n.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(t===B)return n.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(t===z)return n.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}if(36196===t)return n=e.get("WEBGL_compressed_texture_etc1"),null!==n?n.COMPRESSED_RGB_ETC1_WEBGL:null;if((t===F||t===O)&&(n=e.get("WEBGL_compressed_texture_etc"),null!==n)){if(t===F)return n.COMPRESSED_RGB8_ETC2;if(t===O)return n.COMPRESSED_RGBA8_ETC2_EAC}return 37808===t||37809===t||37810===t||37811===t||37812===t||37813===t||37814===t||37815===t||37816===t||37817===t||37818===t||37819===t||37820===t||37821===t||37840===t||37841===t||37842===t||37843===t||37844===t||37845===t||37846===t||37847===t||37848===t||37849===t||37850===t||37851===t||37852===t||37853===t?(n=e.get("WEBGL_compressed_texture_astc"),null!==n?t:null):36492===t?(n=e.get("EXT_texture_compression_bptc"),null!==n?t:null):t===S?i?34042:(n=e.get("WEBGL_depth_texture"),null!==n?n.UNSIGNED_INT_24_8_WEBGL:null):void 0}}}class fs extends Kn{constructor(t=[]){super(),this.cameras=t}}fs.prototype.isArrayCamera=!0;class gs extends Ce{constructor(){super(),this.type="Group"}}gs.prototype.isGroup=!0;const vs={type:"move"};class ys{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return null===this._hand&&(this._hand=new gs,this._hand.matrixAutoUpdate=!1,this._hand.visible=!1,this._hand.joints={},this._hand.inputState={pinching:!1}),this._hand}getTargetRaySpace(){return null===this._targetRay&&(this._targetRay=new gs,this._targetRay.matrixAutoUpdate=!1,this._targetRay.visible=!1,this._targetRay.hasLinearVelocity=!1,this._targetRay.linearVelocity=new Lt,this._targetRay.hasAngularVelocity=!1,this._targetRay.angularVelocity=new Lt),this._targetRay}getGripSpace(){return null===this._grip&&(this._grip=new gs,this._grip.matrixAutoUpdate=!1,this._grip.visible=!1,this._grip.hasLinearVelocity=!1,this._grip.linearVelocity=new Lt,this._grip.hasAngularVelocity=!1,this._grip.angularVelocity=new Lt),this._grip}dispatchEvent(t){return null!==this._targetRay&&this._targetRay.dispatchEvent(t),null!==this._grip&&this._grip.dispatchEvent(t),null!==this._hand&&this._hand.dispatchEvent(t),this}disconnect(t){return this.dispatchEvent({type:"disconnected",data:t}),null!==this._targetRay&&(this._targetRay.visible=!1),null!==this._grip&&(this._grip.visible=!1),null!==this._hand&&(this._hand.visible=!1),this}update(t,e,n){let i=null,r=null,s=null;const a=this._targetRay,o=this._grip,l=this._hand;if(t&&"visible-blurred"!==e.session.visibilityState)if(null!==a&&(i=e.getPose(t.targetRaySpace,n),null!==i&&(a.matrix.fromArray(i.transform.matrix),a.matrix.decompose(a.position,a.rotation,a.scale),i.linearVelocity?(a.hasLinearVelocity=!0,a.linearVelocity.copy(i.linearVelocity)):a.hasLinearVelocity=!1,i.angularVelocity?(a.hasAngularVelocity=!0,a.angularVelocity.copy(i.angularVelocity)):a.hasAngularVelocity=!1,this.dispatchEvent(vs))),l&&t.hand){s=!0;for(const i of t.hand.values()){const t=e.getJointPose(i,n);if(void 0===l.joints[i.jointName]){const t=new gs;t.matrixAutoUpdate=!1,t.visible=!1,l.joints[i.jointName]=t,l.add(t)}const r=l.joints[i.jointName];null!==t&&(r.matrix.fromArray(t.transform.matrix),r.matrix.decompose(r.position,r.rotation,r.scale),r.jointRadius=t.radius),r.visible=null!==t}const i=l.joints["index-finger-tip"],r=l.joints["thumb-tip"],a=i.position.distanceTo(r.position),o=.02,c=.005;l.inputState.pinching&&a>o+c?(l.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!l.inputState.pinching&&a<=o-c&&(l.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else null!==o&&t.gripSpace&&(r=e.getPose(t.gripSpace,n),null!==r&&(o.matrix.fromArray(r.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),r.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(r.linearVelocity)):o.hasLinearVelocity=!1,r.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(r.angularVelocity)):o.hasAngularVelocity=!1));return null!==a&&(a.visible=null!==i),null!==o&&(o.visible=null!==r),null!==l&&(l.visible=null!==s),this}}class xs extends rt{constructor(t,e){super();const n=this,i=t.state;let r=null,s=1,a=null,o="local-floor",l=null;const c=[],h=new Map,u=new Kn;u.layers.enable(1),u.viewport=new St;const d=new Kn;d.layers.enable(2),d.viewport=new St;const p=[u,d],m=new fs;m.layers.enable(1),m.layers.enable(2);let f=null,g=null;function v(t){const e=h.get(t.inputSource);e&&e.dispatchEvent({type:t.type,data:t.inputSource})}function y(){h.forEach((function(t,e){t.disconnect(e)})),h.clear(),f=null,g=null,i.bindXRFramebuffer(null),t.setRenderTarget(t.getRenderTarget()),S.stop(),n.isPresenting=!1,n.dispatchEvent({type:"sessionend"})}function x(t){const e=r.inputSources;for(let t=0;t0&&Rt(s,t,e),a.length>0&&Rt(a,t,e),null!==_&&(J.updateRenderTargetMipmap(_),J.updateMultisampleRenderTarget(_)),!0===t.isScene&&t.onAfterRender(f,t,e),q.buffers.depth.setTest(!0),q.buffers.depth.setMask(!0),q.buffers.color.setMask(!0),q.setPolygonOffset(!1),dt.resetDefaultState(),w=-1,S=null,m.pop(),d=m.length>0?m[m.length-1]:null,p.pop(),u=p.length>0?p[p.length-1]:null},this.getActiveCubeFace=function(){return v},this.getActiveMipmapLevel=function(){return y},this.getRenderTarget=function(){return _},this.setRenderTarget=function(t,e=0,n=0){_=t,v=e,y=n,t&&void 0===Z.get(t).__webglFramebuffer&&J.setupRenderTarget(t);let i=null,r=!1,s=!1;if(t){const n=t.texture;(n.isDataTexture3D||n.isDataTexture2DArray)&&(s=!0);const a=Z.get(t).__webglFramebuffer;t.isWebGLCubeRenderTarget?(i=a[e],r=!0):i=t.isWebGLMultisampleRenderTarget?Z.get(t).__webglMultisampledFramebuffer:a,T.copy(t.viewport),A.copy(t.scissor),L=t.scissorTest}else T.copy(N).multiplyScalar(P).floor(),A.copy(B).multiplyScalar(P).floor(),L=z;if(q.bindFramebuffer(36160,i),q.viewport(T),q.scissor(A),q.setScissorTest(L),r){const i=Z.get(t.texture);pt.framebufferTexture2D(36160,36064,34069+e,i.__webglTexture,n)}else if(s){const i=Z.get(t.texture),r=e||0;pt.framebufferTextureLayer(36160,36064,i.__webglTexture,n||0,r)}},this.readRenderTargetPixels=function(t,e,n,i,r,s,a){if(!t||!t.isWebGLRenderTarget)return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let o=Z.get(t).__webglFramebuffer;if(t.isWebGLCubeRenderTarget&&void 0!==a&&(o=o[a]),o){q.bindFramebuffer(36160,o);try{const a=t.texture,o=a.format,l=a.type;if(o!==E&&ut.convert(o)!==pt.getParameter(35739))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");const c=l===M&&(W.has("EXT_color_buffer_half_float")||j.isWebGL2&&W.has("EXT_color_buffer_float"));if(!(l===x||ut.convert(l)===pt.getParameter(35738)||l===b&&(j.isWebGL2||W.has("OES_texture_float")||W.has("WEBGL_color_buffer_float"))||c))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");36053===pt.checkFramebufferStatus(36160)?e>=0&&e<=t.width-i&&n>=0&&n<=t.height-r&&pt.readPixels(e,n,i,r,ut.convert(o),ut.convert(l),s):console.error("THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete.")}finally{const t=null!==_?Z.get(_).__webglFramebuffer:null;q.bindFramebuffer(36160,t)}}},this.copyFramebufferToTexture=function(t,e,n=0){const i=Math.pow(2,-n),r=Math.floor(e.image.width*i),s=Math.floor(e.image.height*i),a=ut.convert(e.format);J.setTexture2D(e,0),pt.copyTexImage2D(3553,n,a,t.x,t.y,r,s,0),q.unbindTexture()},this.copyTextureToTexture=function(t,e,n,i=0){const r=e.image.width,s=e.image.height,a=ut.convert(n.format),o=ut.convert(n.type);J.setTexture2D(n,0),pt.pixelStorei(37440,n.flipY),pt.pixelStorei(37441,n.premultiplyAlpha),pt.pixelStorei(3317,n.unpackAlignment),e.isDataTexture?pt.texSubImage2D(3553,i,t.x,t.y,r,s,a,o,e.image.data):e.isCompressedTexture?pt.compressedTexSubImage2D(3553,i,t.x,t.y,e.mipmaps[0].width,e.mipmaps[0].height,a,e.mipmaps[0].data):pt.texSubImage2D(3553,i,t.x,t.y,a,o,e.image),0===i&&n.generateMipmaps&&pt.generateMipmap(3553),q.unbindTexture()},this.copyTextureToTexture3D=function(t,e,n,i,r=0){if(f.isWebGL1Renderer)return void console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: can only be used with WebGL2.");const{width:s,height:a,data:o}=n.image,l=ut.convert(i.format),c=ut.convert(i.type);let h;if(i.isDataTexture3D)J.setTexture3D(i,0),h=32879;else{if(!i.isDataTexture2DArray)return void console.warn("THREE.WebGLRenderer.copyTextureToTexture3D: only supports THREE.DataTexture3D and THREE.DataTexture2DArray.");J.setTexture2DArray(i,0),h=35866}pt.pixelStorei(37440,i.flipY),pt.pixelStorei(37441,i.premultiplyAlpha),pt.pixelStorei(3317,i.unpackAlignment);const u=pt.getParameter(3314),d=pt.getParameter(32878),p=pt.getParameter(3316),m=pt.getParameter(3315),g=pt.getParameter(32877);pt.pixelStorei(3314,s),pt.pixelStorei(32878,a),pt.pixelStorei(3316,t.min.x),pt.pixelStorei(3315,t.min.y),pt.pixelStorei(32877,t.min.z),pt.texSubImage3D(h,r,e.x,e.y,e.z,t.max.x-t.min.x+1,t.max.y-t.min.y+1,t.max.z-t.min.z+1,l,c,o),pt.pixelStorei(3314,u),pt.pixelStorei(32878,d),pt.pixelStorei(3316,p),pt.pixelStorei(3315,m),pt.pixelStorei(32877,g),0===r&&i.generateMipmaps&&pt.generateMipmap(h),q.unbindTexture()},this.initTexture=function(t){J.setTexture2D(t,0),q.unbindTexture()},this.resetState=function(){v=0,y=0,_=null,q.reset(),dt.reset()},"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}class bs extends ws{}bs.prototype.isWebGL1Renderer=!0;class Ms{constructor(t,e=25e-5){this.name="",this.color=new tn(t),this.density=e}clone(){return new Ms(this.color,this.density)}toJSON(){return{type:"FogExp2",color:this.color.getHex(),density:this.density}}}Ms.prototype.isFogExp2=!0;class Ss{constructor(t,e=1,n=1e3){this.name="",this.color=new tn(t),this.near=e,this.far=n}clone(){return new Ss(this.color,this.near,this.far)}toJSON(){return{type:"Fog",color:this.color.getHex(),near:this.near,far:this.far}}}Ss.prototype.isFog=!0;class Ts extends Ce{constructor(){super(),this.type="Scene",this.background=null,this.environment=null,this.fog=null,this.overrideMaterial=null,this.autoUpdate=!0,"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(t,e){return super.copy(t,e),null!==t.background&&(this.background=t.background.clone()),null!==t.environment&&(this.environment=t.environment.clone()),null!==t.fog&&(this.fog=t.fog.clone()),null!==t.overrideMaterial&&(this.overrideMaterial=t.overrideMaterial.clone()),this.autoUpdate=t.autoUpdate,this.matrixAutoUpdate=t.matrixAutoUpdate,this}toJSON(t){const e=super.toJSON(t);return null!==this.background&&(e.object.background=this.background.toJSON(t)),null!==this.environment&&(e.object.environment=this.environment.toJSON(t)),null!==this.fog&&(e.object.fog=this.fog.toJSON()),e}}Ts.prototype.isScene=!0;class Es{constructor(t,e){this.array=t,this.stride=e,this.count=void 0!==t?t.length/e:0,this.usage=et,this.updateRange={offset:0,count:-1},this.version=0,this.uuid=ct(),this.onUploadCallback=function(){}}set needsUpdate(t){!0===t&&this.version++}setUsage(t){return this.usage=t,this}copy(t){return this.array=new t.array.constructor(t.array),this.count=t.count,this.stride=t.stride,this.usage=t.usage,this}copyAt(t,e,n){t*=this.stride,n*=e.stride;for(let i=0,r=this.stride;it.far||e.push({distance:o,point:Ps.clone(),uv:je.getUV(Ps,Fs,Os,Hs,Gs,Us,ks,new vt),face:null,object:this})}copy(t){return super.copy(t),void 0!==t.center&&this.center.copy(t.center),this.material=t.material,this}}function Ws(t,e,n,i,r,s){Ns.subVectors(t,n).addScalar(.5).multiply(i),void 0!==r?(Bs.x=s*Ns.x-r*Ns.y,Bs.y=r*Ns.x+s*Ns.y):Bs.copy(Ns),t.copy(e),t.x+=Bs.x,t.y+=Bs.y,t.applyMatrix4(zs)}Vs.prototype.isSprite=!0;const js=new Lt,qs=new Lt;class Xs extends Ce{constructor(){super(),this._currentLevel=0,this.type="LOD",Object.defineProperties(this,{levels:{enumerable:!0,value:[]},isLOD:{value:!0}}),this.autoUpdate=!0}copy(t){super.copy(t,!1);const e=t.levels;for(let t=0,n=e.length;t0){let n,i;for(n=1,i=e.length;n0){js.setFromMatrixPosition(this.matrixWorld);const n=t.ray.origin.distanceTo(js);this.getObjectForDistance(n).raycast(t,e)}}update(t){const e=this.levels;if(e.length>1){js.setFromMatrixPosition(t.matrixWorld),qs.setFromMatrixPosition(this.matrixWorld);const n=js.distanceTo(qs)/t.zoom;let i,r;for(e[0].object.visible=!0,i=1,r=e.length;i=e[i].distance;i++)e[i-1].object.visible=!1,e[i].object.visible=!0;for(this._currentLevel=i-1;io)continue;u.applyMatrix4(this.matrixWorld);const d=t.ray.origin.distanceTo(u);dt.far||e.push({distance:d,point:h.clone().applyMatrix4(this.matrixWorld),index:n,face:null,faceIndex:null,object:this})}}else{for(let n=Math.max(0,s.start),i=Math.min(r.count,s.start+s.count)-1;no)continue;u.applyMatrix4(this.matrixWorld);const i=t.ray.origin.distanceTo(u);it.far||e.push({distance:i,point:h.clone().applyMatrix4(this.matrixWorld),index:n,face:null,faceIndex:null,object:this})}}}else n.isGeometry&&console.error("THREE.Line.raycast() no longer supports THREE.Geometry. Use THREE.BufferGeometry instead.")}updateMorphTargets(){const t=this.geometry;if(t.isBufferGeometry){const e=t.morphAttributes,n=Object.keys(e);if(n.length>0){const t=e[n[0]];if(void 0!==t){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,n=t.length;e0&&console.error("THREE.Line.updateMorphTargets() does not support THREE.Geometry. Use THREE.BufferGeometry instead.")}}}fa.prototype.isLine=!0;const ga=new Lt,va=new Lt;class ya extends fa{constructor(t,e){super(t,e),this.type="LineSegments"}computeLineDistances(){const t=this.geometry;if(t.isBufferGeometry)if(null===t.index){const e=t.attributes.position,n=[];for(let t=0,i=e.count;t0){const t=e[n[0]];if(void 0!==t){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let e=0,n=t.length;e0&&console.error("THREE.Points.updateMorphTargets() does not support THREE.Geometry. Use THREE.BufferGeometry instead.")}}}function Ea(t,e,n,i,r,s,a){const o=ba.distanceSqToPoint(t);if(or.far)return;s.push({distance:l,distanceToRay:Math.sqrt(o),point:n,index:e,face:null,object:a})}}Ta.prototype.isPoints=!0;class Aa extends bt{constructor(t,e,n,i,r,s,a,o,l){super(t,e,n,i,r,s,a,o,l),this.format=void 0!==a?a:T,this.minFilter=void 0!==s?s:g,this.magFilter=void 0!==r?r:g,this.generateMipmaps=!1;const c=this;"requestVideoFrameCallback"in t&&t.requestVideoFrameCallback((function e(){c.needsUpdate=!0,t.requestVideoFrameCallback(e)}))}clone(){return new this.constructor(this.image).copy(this)}update(){const t=this.image;!1==="requestVideoFrameCallback"in t&&t.readyState>=t.HAVE_CURRENT_DATA&&(this.needsUpdate=!0)}}Aa.prototype.isVideoTexture=!0;class La extends bt{constructor(t,e,n,i,r,s,a,o,l,c,h,u){super(null,s,a,o,l,c,i,r,h,u),this.image={width:e,height:n},this.mipmaps=t,this.flipY=!1,this.generateMipmaps=!1}}La.prototype.isCompressedTexture=!0;class Ra extends bt{constructor(t,e,n,i,r,s,a,o,l){super(t,e,n,i,r,s,a,o,l),this.needsUpdate=!0}}Ra.prototype.isCanvasTexture=!0;class Ca extends bt{constructor(t,e,n,i,r,s,a,o,l,c){if((c=void 0!==c?c:A)!==A&&c!==L)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");void 0===n&&c===A&&(n=_),void 0===n&&c===L&&(n=S),super(null,i,r,s,a,o,c,n,l),this.image={width:t,height:e},this.magFilter=void 0!==a?a:p,this.minFilter=void 0!==o?o:p,this.flipY=!1,this.generateMipmaps=!1}}Ca.prototype.isDepthTexture=!0;class Pa extends En{constructor(t=1,e=8,n=0,i=2*Math.PI){super(),this.type="CircleGeometry",this.parameters={radius:t,segments:e,thetaStart:n,thetaLength:i},e=Math.max(3,e);const r=[],s=[],a=[],o=[],l=new Lt,c=new vt;s.push(0,0,0),a.push(0,0,1),o.push(.5,.5);for(let r=0,h=3;r<=e;r++,h+=3){const u=n+r/e*i;l.x=t*Math.cos(u),l.y=t*Math.sin(u),s.push(l.x,l.y,l.z),a.push(0,0,1),c.x=(s[h]/t+1)/2,c.y=(s[h+1]/t+1)/2,o.push(c.x,c.y)}for(let t=1;t<=e;t++)r.push(t,t+1,0);this.setIndex(r),this.setAttribute("position",new mn(s,3)),this.setAttribute("normal",new mn(a,3)),this.setAttribute("uv",new mn(o,2))}}class Da extends En{constructor(t=1,e=1,n=1,i=8,r=1,s=!1,a=0,o=2*Math.PI){super(),this.type="CylinderGeometry",this.parameters={radiusTop:t,radiusBottom:e,height:n,radialSegments:i,heightSegments:r,openEnded:s,thetaStart:a,thetaLength:o};const l=this;i=Math.floor(i),r=Math.floor(r);const c=[],h=[],u=[],d=[];let p=0;const m=[],f=n/2;let g=0;function v(n){const r=p,s=new vt,m=new Lt;let v=0;const y=!0===n?t:e,x=!0===n?1:-1;for(let t=1;t<=i;t++)h.push(0,f*x,0),u.push(0,x,0),d.push(.5,.5),p++;const _=p;for(let t=0;t<=i;t++){const e=t/i*o+a,n=Math.cos(e),r=Math.sin(e);m.x=y*r,m.y=f*x,m.z=y*n,h.push(m.x,m.y,m.z),u.push(0,x,0),s.x=.5*n+.5,s.y=.5*r*x+.5,d.push(s.x,s.y),p++}for(let t=0;t0&&v(!0),e>0&&v(!1)),this.setIndex(c),this.setAttribute("position",new mn(h,3)),this.setAttribute("normal",new mn(u,3)),this.setAttribute("uv",new mn(d,2))}}class Ia extends Da{constructor(t=1,e=1,n=8,i=1,r=!1,s=0,a=2*Math.PI){super(0,t,e,n,i,r,s,a),this.type="ConeGeometry",this.parameters={radius:t,height:e,radialSegments:n,heightSegments:i,openEnded:r,thetaStart:s,thetaLength:a}}}class Na extends En{constructor(t,e,n=1,i=0){super(),this.type="PolyhedronGeometry",this.parameters={vertices:t,indices:e,radius:n,detail:i};const r=[],s=[];function a(t,e,n,i){const r=i+1,s=[];for(let i=0;i<=r;i++){s[i]=[];const a=t.clone().lerp(n,i/r),o=e.clone().lerp(n,i/r),l=r-i;for(let t=0;t<=l;t++)s[i][t]=0===t&&i===r?a:a.clone().lerp(o,t/l)}for(let t=0;t.9&&a<.1&&(e<.2&&(s[t+0]+=1),n<.2&&(s[t+2]+=1),i<.2&&(s[t+4]+=1))}}()}(),this.setAttribute("position",new mn(r,3)),this.setAttribute("normal",new mn(r.slice(),3)),this.setAttribute("uv",new mn(s,2)),0===i?this.computeVertexNormals():this.normalizeNormals()}}class Ba extends Na{constructor(t=1,e=0){const n=(1+Math.sqrt(5))/2,i=1/n;super([-1,-1,-1,-1,-1,1,-1,1,-1,-1,1,1,1,-1,-1,1,-1,1,1,1,-1,1,1,1,0,-i,-n,0,-i,n,0,i,-n,0,i,n,-i,-n,0,-i,n,0,i,-n,0,i,n,0,-n,0,-i,n,0,-i,-n,0,i,n,0,i],[3,11,7,3,7,15,3,15,13,7,19,17,7,17,6,7,6,15,17,4,8,17,8,10,17,10,6,8,0,16,8,16,2,8,2,10,0,12,1,0,1,18,0,18,16,6,10,2,6,2,13,6,13,15,2,16,18,2,18,3,2,3,13,18,1,9,18,9,11,18,11,3,4,14,12,4,12,0,4,0,8,11,9,5,11,5,19,11,19,7,19,5,14,19,14,4,19,4,17,1,12,14,1,14,5,1,5,9],t,e),this.type="DodecahedronGeometry",this.parameters={radius:t,detail:e}}}const za=new Lt,Fa=new Lt,Oa=new Lt,Ha=new je;class Ga extends En{constructor(t,e){if(super(),this.type="EdgesGeometry",this.parameters={thresholdAngle:e},e=void 0!==e?e:1,!0===t.isGeometry)return void console.error("THREE.EdgesGeometry no longer supports THREE.Geometry. Use THREE.BufferGeometry instead.");const n=Math.pow(10,4),i=Math.cos(ot*e),r=t.getIndex(),s=t.getAttribute("position"),a=r?r.count:s.count,o=[0,0,0],l=["a","b","c"],c=new Array(3),h={},u=[];for(let t=0;t80*n){o=c=t[0],l=h=t[1];for(let e=n;ec&&(c=u),d>h&&(h=d);p=Math.max(c-o,h-l),p=0!==p?1/p:0}return Wa(s,a,n,o,l,p),a};function ka(t,e,n,i,r){let s,a;if(r===function(t,e,n,i){let r=0;for(let s=e,a=n-i;s0)for(s=e;s=e;s-=i)a=co(s,t[s],t[s+1],a);return a&&io(a,a.next)&&(ho(a),a=a.next),a}function Va(t,e){if(!t)return t;e||(e=t);let n,i=t;do{if(n=!1,i.steiner||!io(i,i.next)&&0!==no(i.prev,i,i.next))i=i.next;else{if(ho(i),i=e=i.prev,i===i.next)break;n=!0}}while(n||i!==e);return e}function Wa(t,e,n,i,r,s,a){if(!t)return;!a&&s&&function(t,e,n,i){let r=t;do{null===r.z&&(r.z=Ka(r.x,r.y,e,n,i)),r.prevZ=r.prev,r.nextZ=r.next,r=r.next}while(r!==t);r.prevZ.nextZ=null,r.prevZ=null,function(t){let e,n,i,r,s,a,o,l,c=1;do{for(n=t,t=null,s=null,a=0;n;){for(a++,i=n,o=0,e=0;e0||l>0&&i;)0!==o&&(0===l||!i||n.z<=i.z)?(r=n,n=n.nextZ,o--):(r=i,i=i.nextZ,l--),s?s.nextZ=r:t=r,r.prevZ=s,s=r;n=i}s.nextZ=null,c*=2}while(a>1)}(r)}(t,i,r,s);let o,l,c=t;for(;t.prev!==t.next;)if(o=t.prev,l=t.next,s?qa(t,i,r,s):ja(t))e.push(o.i/n),e.push(t.i/n),e.push(l.i/n),ho(t),t=l.next,c=l.next;else if((t=l)===c){a?1===a?Wa(t=Xa(Va(t),e,n),e,n,i,r,s,2):2===a&&Ya(t,e,n,i,r,s):Wa(Va(t),e,n,i,r,s,1);break}}function ja(t){const e=t.prev,n=t,i=t.next;if(no(e,n,i)>=0)return!1;let r=t.next.next;for(;r!==t.prev;){if(to(e.x,e.y,n.x,n.y,i.x,i.y,r.x,r.y)&&no(r.prev,r,r.next)>=0)return!1;r=r.next}return!0}function qa(t,e,n,i){const r=t.prev,s=t,a=t.next;if(no(r,s,a)>=0)return!1;const o=r.xs.x?r.x>a.x?r.x:a.x:s.x>a.x?s.x:a.x,h=r.y>s.y?r.y>a.y?r.y:a.y:s.y>a.y?s.y:a.y,u=Ka(o,l,e,n,i),d=Ka(c,h,e,n,i);let p=t.prevZ,m=t.nextZ;for(;p&&p.z>=u&&m&&m.z<=d;){if(p!==t.prev&&p!==t.next&&to(r.x,r.y,s.x,s.y,a.x,a.y,p.x,p.y)&&no(p.prev,p,p.next)>=0)return!1;if(p=p.prevZ,m!==t.prev&&m!==t.next&&to(r.x,r.y,s.x,s.y,a.x,a.y,m.x,m.y)&&no(m.prev,m,m.next)>=0)return!1;m=m.nextZ}for(;p&&p.z>=u;){if(p!==t.prev&&p!==t.next&&to(r.x,r.y,s.x,s.y,a.x,a.y,p.x,p.y)&&no(p.prev,p,p.next)>=0)return!1;p=p.prevZ}for(;m&&m.z<=d;){if(m!==t.prev&&m!==t.next&&to(r.x,r.y,s.x,s.y,a.x,a.y,m.x,m.y)&&no(m.prev,m,m.next)>=0)return!1;m=m.nextZ}return!0}function Xa(t,e,n){let i=t;do{const r=i.prev,s=i.next.next;!io(r,s)&&ro(r,i,i.next,s)&&oo(r,s)&&oo(s,r)&&(e.push(r.i/n),e.push(i.i/n),e.push(s.i/n),ho(i),ho(i.next),i=t=s),i=i.next}while(i!==t);return Va(i)}function Ya(t,e,n,i,r,s){let a=t;do{let t=a.next.next;for(;t!==a.prev;){if(a.i!==t.i&&eo(a,t)){let o=lo(a,t);return a=Va(a,a.next),o=Va(o,o.next),Wa(a,e,n,i,r,s),void Wa(o,e,n,i,r,s)}t=t.next}a=a.next}while(a!==t)}function Za(t,e){return t.x-e.x}function Ja(t,e){if(e=function(t,e){let n=e;const i=t.x,r=t.y;let s,a=-1/0;do{if(r<=n.y&&r>=n.next.y&&n.next.y!==n.y){const t=n.x+(r-n.y)*(n.next.x-n.x)/(n.next.y-n.y);if(t<=i&&t>a){if(a=t,t===i){if(r===n.y)return n;if(r===n.next.y)return n.next}s=n.x=n.x&&n.x>=l&&i!==n.x&&to(rs.x||n.x===s.x&&Qa(s,n)))&&(s=n,u=h)),n=n.next}while(n!==o);return s}(t,e)){const n=lo(e,t);Va(e,e.next),Va(n,n.next)}}function Qa(t,e){return no(t.prev,t,e.prev)<0&&no(e.next,t,t.next)<0}function Ka(t,e,n,i,r){return(t=1431655765&((t=858993459&((t=252645135&((t=16711935&((t=32767*(t-n)*r)|t<<8))|t<<4))|t<<2))|t<<1))|(e=1431655765&((e=858993459&((e=252645135&((e=16711935&((e=32767*(e-i)*r)|e<<8))|e<<4))|e<<2))|e<<1))<<1}function $a(t){let e=t,n=t;do{(e.x=0&&(t-a)*(i-o)-(n-a)*(e-o)>=0&&(n-a)*(s-o)-(r-a)*(i-o)>=0}function eo(t,e){return t.next.i!==e.i&&t.prev.i!==e.i&&!function(t,e){let n=t;do{if(n.i!==t.i&&n.next.i!==t.i&&n.i!==e.i&&n.next.i!==e.i&&ro(n,n.next,t,e))return!0;n=n.next}while(n!==t);return!1}(t,e)&&(oo(t,e)&&oo(e,t)&&function(t,e){let n=t,i=!1;const r=(t.x+e.x)/2,s=(t.y+e.y)/2;do{n.y>s!=n.next.y>s&&n.next.y!==n.y&&r<(n.next.x-n.x)*(s-n.y)/(n.next.y-n.y)+n.x&&(i=!i),n=n.next}while(n!==t);return i}(t,e)&&(no(t.prev,t,e.prev)||no(t,e.prev,e))||io(t,e)&&no(t.prev,t,t.next)>0&&no(e.prev,e,e.next)>0)}function no(t,e,n){return(e.y-t.y)*(n.x-e.x)-(e.x-t.x)*(n.y-e.y)}function io(t,e){return t.x===e.x&&t.y===e.y}function ro(t,e,n,i){const r=ao(no(t,e,n)),s=ao(no(t,e,i)),a=ao(no(n,i,t)),o=ao(no(n,i,e));return r!==s&&a!==o||(!(0!==r||!so(t,n,e))||(!(0!==s||!so(t,i,e))||(!(0!==a||!so(n,t,i))||!(0!==o||!so(n,e,i)))))}function so(t,e,n){return e.x<=Math.max(t.x,n.x)&&e.x>=Math.min(t.x,n.x)&&e.y<=Math.max(t.y,n.y)&&e.y>=Math.min(t.y,n.y)}function ao(t){return t>0?1:t<0?-1:0}function oo(t,e){return no(t.prev,t,t.next)<0?no(t,e,t.next)>=0&&no(t,t.prev,e)>=0:no(t,e,t.prev)<0||no(t,t.next,e)<0}function lo(t,e){const n=new uo(t.i,t.x,t.y),i=new uo(e.i,e.x,e.y),r=t.next,s=e.prev;return t.next=e,e.prev=t,n.next=r,r.prev=n,i.next=n,n.prev=i,s.next=i,i.prev=s,i}function co(t,e,n,i){const r=new uo(t,e,n);return i?(r.next=i.next,r.prev=i,i.next.prev=r,i.next=r):(r.prev=r,r.next=r),r}function ho(t){t.next.prev=t.prev,t.prev.next=t.next,t.prevZ&&(t.prevZ.nextZ=t.nextZ),t.nextZ&&(t.nextZ.prevZ=t.prevZ)}function uo(t,e,n){this.i=t,this.x=e,this.y=n,this.prev=null,this.next=null,this.z=null,this.prevZ=null,this.nextZ=null,this.steiner=!1}class po{static area(t){const e=t.length;let n=0;for(let i=e-1,r=0;r2&&t[e-1].equals(t[0])&&t.pop()}function fo(t,e){for(let n=0;nNumber.EPSILON){const u=Math.sqrt(h),d=Math.sqrt(l*l+c*c),p=e.x-o/u,m=e.y+a/u,f=((n.x-c/d-p)*c-(n.y+l/d-m)*l)/(a*c-o*l);i=p+a*f-t.x,r=m+o*f-t.y;const g=i*i+r*r;if(g<=2)return new vt(i,r);s=Math.sqrt(g/2)}else{let t=!1;a>Number.EPSILON?l>Number.EPSILON&&(t=!0):a<-Number.EPSILON?l<-Number.EPSILON&&(t=!0):Math.sign(o)===Math.sign(c)&&(t=!0),t?(i=-o,r=a,s=Math.sqrt(h)):(i=a,r=o,s=Math.sqrt(h/2))}return new vt(i/s,r/s)}const P=[];for(let t=0,e=E.length,n=e-1,i=t+1;t=0;t--){const e=t/p,n=h*Math.cos(e*Math.PI/2),i=u*Math.sin(e*Math.PI/2)+d;for(let t=0,e=E.length;t=0;){const i=n;let r=n-1;r<0&&(r=t.length-1);for(let t=0,n=o+2*p;t=0?(t(i-o,p,h),u.subVectors(c,h)):(t(i+o,p,h),u.subVectors(h,c)),p-o>=0?(t(i,p-o,h),d.subVectors(c,h)):(t(i,p+o,h),d.subVectors(h,c)),l.crossVectors(u,d).normalize(),s.push(l.x,l.y,l.z),a.push(i,p)}}for(let t=0;t0)&&d.push(e,r,l),(t!==n-1||o=i)){l.push(e.times[t]);for(let n=0;ns.tracks[t].times[0]&&(o=s.tracks[t].times[0]);for(let t=0;t=i.times[u]){const t=u*l+o,e=t+l-o;d=Vo.arraySlice(i.values,t,e)}else{const t=i.createInterpolant(),e=o,n=l-o;t.evaluate(s),d=Vo.arraySlice(t.resultBuffer,e,n)}if("quaternion"===r){(new At).fromArray(d).normalize().conjugate().toArray(d)}const p=a.times.length;for(let t=0;t=r)break t;{const a=e[1];t=r)break e}s=n,n=0}}for(;n>>1;te;)--s;if(++s,0!==r||s!==i){r>=s&&(s=Math.max(s,1),r=s-1);const t=this.getValueSize();this.times=Vo.arraySlice(n,r,s),this.values=Vo.arraySlice(this.values,r*t,s*t)}return this}validate(){let t=!0;const e=this.getValueSize();e-Math.floor(e)!=0&&(console.error("THREE.KeyframeTrack: Invalid value size in track.",this),t=!1);const n=this.times,i=this.values,r=n.length;0===r&&(console.error("THREE.KeyframeTrack: Track is empty.",this),t=!1);let s=null;for(let e=0;e!==r;e++){const i=n[e];if("number"==typeof i&&isNaN(i)){console.error("THREE.KeyframeTrack: Time is not a valid number.",this,e,i),t=!1;break}if(null!==s&&s>i){console.error("THREE.KeyframeTrack: Out of order keys.",this,e,i,s),t=!1;break}s=i}if(void 0!==i&&Vo.isTypedArray(i))for(let e=0,n=i.length;e!==n;++e){const n=i[e];if(isNaN(n)){console.error("THREE.KeyframeTrack: Value is not a valid number.",this,e,n),t=!1;break}}return t}optimize(){const t=Vo.arraySlice(this.times),e=Vo.arraySlice(this.values),n=this.getValueSize(),i=this.getInterpolation()===U,r=t.length-1;let s=1;for(let a=1;a0){t[s]=t[r];for(let t=r*n,i=s*n,a=0;a!==n;++a)e[i+a]=e[t+a];++s}return s!==t.length?(this.times=Vo.arraySlice(t,0,s),this.values=Vo.arraySlice(e,0,s*n)):(this.times=t,this.values=e),this}clone(){const t=Vo.arraySlice(this.times,0),e=Vo.arraySlice(this.values,0),n=new(0,this.constructor)(this.name,t,e);return n.createInterpolant=this.createInterpolant,n}}Yo.prototype.TimeBufferType=Float32Array,Yo.prototype.ValueBufferType=Float32Array,Yo.prototype.DefaultInterpolation=G;class Zo extends Yo{}Zo.prototype.ValueTypeName="bool",Zo.prototype.ValueBufferType=Array,Zo.prototype.DefaultInterpolation=H,Zo.prototype.InterpolantFactoryMethodLinear=void 0,Zo.prototype.InterpolantFactoryMethodSmooth=void 0;class Jo extends Yo{}Jo.prototype.ValueTypeName="color";class Qo extends Yo{}Qo.prototype.ValueTypeName="number";class Ko extends Wo{constructor(t,e,n,i){super(t,e,n,i)}interpolate_(t,e,n,i){const r=this.resultBuffer,s=this.sampleValues,a=this.valueSize,o=(n-e)/(i-e);let l=t*a;for(let t=l+a;l!==t;l+=4)At.slerpFlat(r,0,s,l-a,s,l,o);return r}}class $o extends Yo{InterpolantFactoryMethodLinear(t){return new Ko(this.times,this.values,this.getValueSize(),t)}}$o.prototype.ValueTypeName="quaternion",$o.prototype.DefaultInterpolation=G,$o.prototype.InterpolantFactoryMethodSmooth=void 0;class tl extends Yo{}tl.prototype.ValueTypeName="string",tl.prototype.ValueBufferType=Array,tl.prototype.DefaultInterpolation=H,tl.prototype.InterpolantFactoryMethodLinear=void 0,tl.prototype.InterpolantFactoryMethodSmooth=void 0;class el extends Yo{}el.prototype.ValueTypeName="vector";class nl{constructor(t,e=-1,n,i=2500){this.name=t,this.tracks=n,this.duration=e,this.blendMode=i,this.uuid=ct(),this.duration<0&&this.resetDuration()}static parse(t){const e=[],n=t.tracks,i=1/(t.fps||1);for(let t=0,r=n.length;t!==r;++t)e.push(il(n[t]).scale(i));const r=new this(t.name,t.duration,e,t.blendMode);return r.uuid=t.uuid,r}static toJSON(t){const e=[],n=t.tracks,i={name:t.name,duration:t.duration,tracks:e,uuid:t.uuid,blendMode:t.blendMode};for(let t=0,i=n.length;t!==i;++t)e.push(Yo.toJSON(n[t]));return i}static CreateFromMorphTargetSequence(t,e,n,i){const r=e.length,s=[];for(let t=0;t1){const t=s[1];let e=i[t];e||(i[t]=e=[]),e.push(n)}}const s=[];for(const t in i)s.push(this.CreateFromMorphTargetSequence(t,i[t],e,n));return s}static parseAnimation(t,e){if(!t)return console.error("THREE.AnimationClip: No animation in JSONLoader data."),null;const n=function(t,e,n,i,r){if(0!==n.length){const s=[],a=[];Vo.flattenJSON(n,s,a,i),0!==s.length&&r.push(new t(e,s,a))}},i=[],r=t.name||"default",s=t.fps||30,a=t.blendMode;let o=t.length||-1;const l=t.hierarchy||[];for(let t=0;t0||0===t.search(/^data\:image\/jpeg/);r.format=i?T:E,r.needsUpdate=!0,void 0!==e&&e(r)}),n,i),r}}class ml{constructor(){this.type="Curve",this.arcLengthDivisions=200}getPoint(){return console.warn("THREE.Curve: .getPoint() not implemented."),null}getPointAt(t,e){const n=this.getUtoTmapping(t);return this.getPoint(n,e)}getPoints(t=5){const e=[];for(let n=0;n<=t;n++)e.push(this.getPoint(n/t));return e}getSpacedPoints(t=5){const e=[];for(let n=0;n<=t;n++)e.push(this.getPointAt(n/t));return e}getLength(){const t=this.getLengths();return t[t.length-1]}getLengths(t=this.arcLengthDivisions){if(this.cacheArcLengths&&this.cacheArcLengths.length===t+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;const e=[];let n,i=this.getPoint(0),r=0;e.push(0);for(let s=1;s<=t;s++)n=this.getPoint(s/t),r+=n.distanceTo(i),e.push(r),i=n;return this.cacheArcLengths=e,e}updateArcLengths(){this.needsUpdate=!0,this.getLengths()}getUtoTmapping(t,e){const n=this.getLengths();let i=0;const r=n.length;let s;s=e||t*n[r-1];let a,o=0,l=r-1;for(;o<=l;)if(i=Math.floor(o+(l-o)/2),a=n[i]-s,a<0)o=i+1;else{if(!(a>0)){l=i;break}l=i-1}if(i=l,n[i]===s)return i/(r-1);const c=n[i];return(i+(s-c)/(n[i+1]-c))/(r-1)}getTangent(t,e){const n=1e-4;let i=t-n,r=t+n;i<0&&(i=0),r>1&&(r=1);const s=this.getPoint(i),a=this.getPoint(r),o=e||(s.isVector2?new vt:new Lt);return o.copy(a).sub(s).normalize(),o}getTangentAt(t,e){const n=this.getUtoTmapping(t);return this.getTangent(n,e)}computeFrenetFrames(t,e){const n=new Lt,i=[],r=[],s=[],a=new Lt,o=new se;for(let e=0;e<=t;e++){const n=e/t;i[e]=this.getTangentAt(n,new Lt),i[e].normalize()}r[0]=new Lt,s[0]=new Lt;let l=Number.MAX_VALUE;const c=Math.abs(i[0].x),h=Math.abs(i[0].y),u=Math.abs(i[0].z);c<=l&&(l=c,n.set(1,0,0)),h<=l&&(l=h,n.set(0,1,0)),u<=l&&n.set(0,0,1),a.crossVectors(i[0],n).normalize(),r[0].crossVectors(i[0],a),s[0].crossVectors(i[0],r[0]);for(let e=1;e<=t;e++){if(r[e]=r[e-1].clone(),s[e]=s[e-1].clone(),a.crossVectors(i[e-1],i[e]),a.length()>Number.EPSILON){a.normalize();const t=Math.acos(ht(i[e-1].dot(i[e]),-1,1));r[e].applyMatrix4(o.makeRotationAxis(a,t))}s[e].crossVectors(i[e],r[e])}if(!0===e){let e=Math.acos(ht(r[0].dot(r[t]),-1,1));e/=t,i[0].dot(a.crossVectors(r[0],r[t]))>0&&(e=-e);for(let n=1;n<=t;n++)r[n].applyMatrix4(o.makeRotationAxis(i[n],e*n)),s[n].crossVectors(i[n],r[n])}return{tangents:i,normals:r,binormals:s}}clone(){return(new this.constructor).copy(this)}copy(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}toJSON(){const t={metadata:{version:4.5,type:"Curve",generator:"Curve.toJSON"}};return t.arcLengthDivisions=this.arcLengthDivisions,t.type=this.type,t}fromJSON(t){return this.arcLengthDivisions=t.arcLengthDivisions,this}}class fl extends ml{constructor(t=0,e=0,n=1,i=1,r=0,s=2*Math.PI,a=!1,o=0){super(),this.type="EllipseCurve",this.aX=t,this.aY=e,this.xRadius=n,this.yRadius=i,this.aStartAngle=r,this.aEndAngle=s,this.aClockwise=a,this.aRotation=o}getPoint(t,e){const n=e||new vt,i=2*Math.PI;let r=this.aEndAngle-this.aStartAngle;const s=Math.abs(r)i;)r-=i;r0?0:(Math.floor(Math.abs(l)/r)+1)*r:0===c&&l===r-1&&(l=r-2,c=1),this.closed||l>0?a=i[(l-1)%r]:(yl.subVectors(i[0],i[1]).add(i[0]),a=yl);const h=i[l%r],u=i[(l+1)%r];if(this.closed||l+2i.length-2?i.length-1:s+1],h=i[s>i.length-3?i.length-1:s+2];return n.set(Ml(a,o.x,l.x,c.x,h.x),Ml(a,o.y,l.y,c.y,h.y)),n}copy(t){super.copy(t),this.points=[];for(let e=0,n=t.points.length;e=e){const t=n[i]-e,r=this.curves[i],s=r.getLength(),a=0===s?0:1-t/s;return r.getPointAt(a)}i++}return null}getLength(){const t=this.getCurveLengths();return t[t.length-1]}updateArcLengths(){this.needsUpdate=!0,this.cacheLengths=null,this.getCurveLengths()}getCurveLengths(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;const t=[];let e=0;for(let n=0,i=this.curves.length;n1&&!e[e.length-1].equals(e[0])&&e.push(e[0]),e}copy(t){super.copy(t),this.curves=[];for(let e=0,n=t.curves.length;e0){const t=l.getPoint(0);t.equals(this.currentPoint)||this.lineTo(t.x,t.y)}this.curves.push(l);const c=l.getPoint(1);return this.currentPoint.copy(c),this}copy(t){return super.copy(t),this.currentPoint.copy(t.currentPoint),this}toJSON(){const t=super.toJSON();return t.currentPoint=this.currentPoint.toArray(),t}fromJSON(t){return super.fromJSON(t),this.currentPoint.fromArray(t.currentPoint),this}}class zl extends Bl{constructor(t){super(t),this.uuid=ct(),this.type="Shape",this.holes=[]}getPointsHoles(t){const e=[];for(let n=0,i=this.holes.length;n0:i.vertexColors=t.vertexColors),void 0!==t.uniforms)for(const e in t.uniforms){const r=t.uniforms[e];switch(i.uniforms[e]={},r.type){case"t":i.uniforms[e].value=n(r.value);break;case"c":i.uniforms[e].value=(new tn).setHex(r.value);break;case"v2":i.uniforms[e].value=(new vt).fromArray(r.value);break;case"v3":i.uniforms[e].value=(new Lt).fromArray(r.value);break;case"v4":i.uniforms[e].value=(new St).fromArray(r.value);break;case"m3":i.uniforms[e].value=(new yt).fromArray(r.value);break;case"m4":i.uniforms[e].value=(new se).fromArray(r.value);break;default:i.uniforms[e].value=r.value}}if(void 0!==t.defines&&(i.defines=t.defines),void 0!==t.vertexShader&&(i.vertexShader=t.vertexShader),void 0!==t.fragmentShader&&(i.fragmentShader=t.fragmentShader),void 0!==t.extensions)for(const e in t.extensions)i.extensions[e]=t.extensions[e];if(void 0!==t.shading&&(i.flatShading=1===t.shading),void 0!==t.size&&(i.size=t.size),void 0!==t.sizeAttenuation&&(i.sizeAttenuation=t.sizeAttenuation),void 0!==t.map&&(i.map=n(t.map)),void 0!==t.matcap&&(i.matcap=n(t.matcap)),void 0!==t.alphaMap&&(i.alphaMap=n(t.alphaMap)),void 0!==t.bumpMap&&(i.bumpMap=n(t.bumpMap)),void 0!==t.bumpScale&&(i.bumpScale=t.bumpScale),void 0!==t.normalMap&&(i.normalMap=n(t.normalMap)),void 0!==t.normalMapType&&(i.normalMapType=t.normalMapType),void 0!==t.normalScale){let e=t.normalScale;!1===Array.isArray(e)&&(e=[e,e]),i.normalScale=(new vt).fromArray(e)}return void 0!==t.displacementMap&&(i.displacementMap=n(t.displacementMap)),void 0!==t.displacementScale&&(i.displacementScale=t.displacementScale),void 0!==t.displacementBias&&(i.displacementBias=t.displacementBias),void 0!==t.roughnessMap&&(i.roughnessMap=n(t.roughnessMap)),void 0!==t.metalnessMap&&(i.metalnessMap=n(t.metalnessMap)),void 0!==t.emissiveMap&&(i.emissiveMap=n(t.emissiveMap)),void 0!==t.emissiveIntensity&&(i.emissiveIntensity=t.emissiveIntensity),void 0!==t.specularMap&&(i.specularMap=n(t.specularMap)),void 0!==t.envMap&&(i.envMap=n(t.envMap)),void 0!==t.envMapIntensity&&(i.envMapIntensity=t.envMapIntensity),void 0!==t.reflectivity&&(i.reflectivity=t.reflectivity),void 0!==t.refractionRatio&&(i.refractionRatio=t.refractionRatio),void 0!==t.lightMap&&(i.lightMap=n(t.lightMap)),void 0!==t.lightMapIntensity&&(i.lightMapIntensity=t.lightMapIntensity),void 0!==t.aoMap&&(i.aoMap=n(t.aoMap)),void 0!==t.aoMapIntensity&&(i.aoMapIntensity=t.aoMapIntensity),void 0!==t.gradientMap&&(i.gradientMap=n(t.gradientMap)),void 0!==t.clearcoatMap&&(i.clearcoatMap=n(t.clearcoatMap)),void 0!==t.clearcoatRoughnessMap&&(i.clearcoatRoughnessMap=n(t.clearcoatRoughnessMap)),void 0!==t.clearcoatNormalMap&&(i.clearcoatNormalMap=n(t.clearcoatNormalMap)),void 0!==t.clearcoatNormalScale&&(i.clearcoatNormalScale=(new vt).fromArray(t.clearcoatNormalScale)),void 0!==t.transmission&&(i.transmission=t.transmission),void 0!==t.transmissionMap&&(i.transmissionMap=n(t.transmissionMap)),i}setTextures(t){return this.textures=t,this}}class rc{static decodeText(t){if("undefined"!=typeof TextDecoder)return(new TextDecoder).decode(t);let e="";for(let n=0,i=t.length;nNumber.EPSILON){if(l<0&&(n=e[s],o=-o,a=e[r],l=-l),t.ya.y)continue;if(t.y===n.y){if(t.x===n.x)return!0}else{const e=l*(t.x-n.x)-o*(t.y-n.y);if(0===e)return!0;if(e<0)continue;i=!i}}else{if(t.y!==n.y)continue;if(a.x<=t.x&&t.x<=n.x||n.x<=t.x&&t.x<=a.x)return!0}}return i}const r=po.isClockWise,s=this.subPaths;if(0===s.length)return[];if(!0===e)return n(s);let a,o,l;const c=[];if(1===s.length)return o=s[0],l=new zl,l.curves=o.curves,c.push(l),c;let h=!r(s[0].getPoints());h=t?!h:h;const u=[],d=[];let p,m,f=[],g=0;d[g]=void 0,f[g]=[];for(let e=0,n=s.length;e1){let t=!1;const e=[];for(let t=0,e=d.length;t0&&(t||(f=u))}for(let t=0,e=d.length;t0){this.source.connect(this.filters[0]);for(let t=1,e=this.filters.length;t0){this.source.disconnect(this.filters[0]);for(let t=1,e=this.filters.length;t0&&this._mixBufferRegionAdditive(n,i,this._addIndex*e,1,e);for(let t=e,r=e+e;t!==r;++t)if(n[t]!==n[t+e]){a.setValue(n,i);break}}saveOriginalState(){const t=this.binding,e=this.buffer,n=this.valueSize,i=n*this._origIndex;t.getValue(e,i);for(let t=n,r=i;t!==r;++t)e[t]=e[i+t%n];this._setIdentity(),this.cumulativeWeight=0,this.cumulativeWeightAdditive=0}restoreOriginalState(){const t=3*this.valueSize;this.binding.setValue(this.buffer,t)}_setAdditiveIdentityNumeric(){const t=this._addIndex*this.valueSize,e=t+this.valueSize;for(let n=t;n=.5)for(let i=0;i!==r;++i)t[e+i]=t[n+i]}_slerp(t,e,n,i){At.slerpFlat(t,e,t,e,t,n,i)}_slerpAdditive(t,e,n,i,r){const s=this._workIndex*r;At.multiplyQuaternionsFlat(t,s,t,e,t,n),At.slerpFlat(t,e,t,e,t,s,i)}_lerp(t,e,n,i,r){const s=1-i;for(let a=0;a!==r;++a){const r=e+a;t[r]=t[r]*s+t[n+a]*i}}_lerpAdditive(t,e,n,i,r){for(let s=0;s!==r;++s){const r=e+s;t[r]=t[r]+t[n+s]*i}}}const Bc="\\[\\]\\.:\\/",zc=new RegExp("[\\[\\]\\.:\\/]","g"),Fc="[^\\[\\]\\.:\\/]",Oc="[^"+Bc.replace("\\.","")+"]",Hc=/((?:WC+[\/:])*)/.source.replace("WC",Fc),Gc=/(WCOD+)?/.source.replace("WCOD",Oc),Uc=/(?:\.(WC+)(?:\[(.+)\])?)?/.source.replace("WC",Fc),kc=/\.(WC+)(?:\[(.+)\])?/.source.replace("WC",Fc),Vc=new RegExp("^"+Hc+Gc+Uc+kc+"$"),Wc=["material","materials","bones"];class jc{constructor(t,e,n){this.path=e,this.parsedPath=n||jc.parseTrackName(e),this.node=jc.findNode(t,this.parsedPath.nodeName)||t,this.rootNode=t,this.getValue=this._getValue_unbound,this.setValue=this._setValue_unbound}static create(t,e,n){return t&&t.isAnimationObjectGroup?new jc.Composite(t,e,n):new jc(t,e,n)}static sanitizeNodeName(t){return t.replace(/\s/g,"_").replace(zc,"")}static parseTrackName(t){const e=Vc.exec(t);if(!e)throw new Error("PropertyBinding: Cannot parse trackName: "+t);const n={nodeName:e[2],objectName:e[3],objectIndex:e[4],propertyName:e[5],propertyIndex:e[6]},i=n.nodeName&&n.nodeName.lastIndexOf(".");if(void 0!==i&&-1!==i){const t=n.nodeName.substring(i+1);-1!==Wc.indexOf(t)&&(n.nodeName=n.nodeName.substring(0,i),n.objectName=t)}if(null===n.propertyName||0===n.propertyName.length)throw new Error("PropertyBinding: can not parse propertyName from trackName: "+t);return n}static findNode(t,e){if(!e||""===e||"."===e||-1===e||e===t.name||e===t.uuid)return t;if(t.skeleton){const n=t.skeleton.getBoneByName(e);if(void 0!==n)return n}if(t.children){const n=function(t){for(let i=0;i=r){const s=r++,c=t[s];e[c.uuid]=l,t[l]=c,e[o]=s,t[s]=a;for(let t=0,e=i;t!==e;++t){const e=n[t],i=e[s],r=e[l];e[l]=i,e[s]=r}}}this.nCachedObjects_=r}uncache(){const t=this._objects,e=this._indicesByUUID,n=this._bindings,i=n.length;let r=this.nCachedObjects_,s=t.length;for(let a=0,o=arguments.length;a!==o;++a){const o=arguments[a].uuid,l=e[o];if(void 0!==l)if(delete e[o],l0&&(e[a.uuid]=l),t[l]=a,t.pop();for(let t=0,e=i;t!==e;++t){const e=n[t];e[l]=e[r],e.pop()}}}this.nCachedObjects_=r}subscribe_(t,e){const n=this._bindingsIndicesByPath;let i=n[t];const r=this._bindings;if(void 0!==i)return r[i];const s=this._paths,a=this._parsedPaths,o=this._objects,l=o.length,c=this.nCachedObjects_,h=new Array(l);i=r.length,n[t]=i,s.push(t),a.push(e),r.push(h);for(let n=c,i=o.length;n!==i;++n){const i=o[n];h[n]=new jc(i,t,e)}return h}unsubscribe_(t){const e=this._bindingsIndicesByPath,n=e[t];if(void 0!==n){const i=this._paths,r=this._parsedPaths,s=this._bindings,a=s.length-1,o=s[a];e[t[a]]=n,s[n]=o,s.pop(),r[n]=r[a],r.pop(),i[n]=i[a],i.pop()}}}qc.prototype.isAnimationObjectGroup=!0;class Xc{constructor(t,e,n=null,i=e.blendMode){this._mixer=t,this._clip=e,this._localRoot=n,this.blendMode=i;const r=e.tracks,s=r.length,a=new Array(s),o={endingStart:k,endingEnd:k};for(let t=0;t!==s;++t){const e=r[t].createInterpolant(null);a[t]=e,e.settings=o}this._interpolantSettings=o,this._interpolants=a,this._propertyBindings=new Array(s),this._cacheIndex=null,this._byClipCacheIndex=null,this._timeScaleInterpolant=null,this._weightInterpolant=null,this.loop=2201,this._loopCount=-1,this._startTime=null,this.time=0,this.timeScale=1,this._effectiveTimeScale=1,this.weight=1,this._effectiveWeight=1,this.repetitions=1/0,this.paused=!1,this.enabled=!0,this.clampWhenFinished=!1,this.zeroSlopeAtStart=!0,this.zeroSlopeAtEnd=!0}play(){return this._mixer._activateAction(this),this}stop(){return this._mixer._deactivateAction(this),this.reset()}reset(){return this.paused=!1,this.enabled=!0,this.time=0,this._loopCount=-1,this._startTime=null,this.stopFading().stopWarping()}isRunning(){return this.enabled&&!this.paused&&0!==this.timeScale&&null===this._startTime&&this._mixer._isActiveAction(this)}isScheduled(){return this._mixer._isActiveAction(this)}startAt(t){return this._startTime=t,this}setLoop(t,e){return this.loop=t,this.repetitions=e,this}setEffectiveWeight(t){return this.weight=t,this._effectiveWeight=this.enabled?t:0,this.stopFading()}getEffectiveWeight(){return this._effectiveWeight}fadeIn(t){return this._scheduleFading(t,0,1)}fadeOut(t){return this._scheduleFading(t,1,0)}crossFadeFrom(t,e,n){if(t.fadeOut(e),this.fadeIn(e),n){const n=this._clip.duration,i=t._clip.duration,r=i/n,s=n/i;t.warp(1,r,e),this.warp(s,1,e)}return this}crossFadeTo(t,e,n){return t.crossFadeFrom(this,e,n)}stopFading(){const t=this._weightInterpolant;return null!==t&&(this._weightInterpolant=null,this._mixer._takeBackControlInterpolant(t)),this}setEffectiveTimeScale(t){return this.timeScale=t,this._effectiveTimeScale=this.paused?0:t,this.stopWarping()}getEffectiveTimeScale(){return this._effectiveTimeScale}setDuration(t){return this.timeScale=this._clip.duration/t,this.stopWarping()}syncWith(t){return this.time=t.time,this.timeScale=t.timeScale,this.stopWarping()}halt(t){return this.warp(this._effectiveTimeScale,0,t)}warp(t,e,n){const i=this._mixer,r=i.time,s=this.timeScale;let a=this._timeScaleInterpolant;null===a&&(a=i._lendControlInterpolant(),this._timeScaleInterpolant=a);const o=a.parameterPositions,l=a.sampleValues;return o[0]=r,o[1]=r+n,l[0]=t/s,l[1]=e/s,this}stopWarping(){const t=this._timeScaleInterpolant;return null!==t&&(this._timeScaleInterpolant=null,this._mixer._takeBackControlInterpolant(t)),this}getMixer(){return this._mixer}getClip(){return this._clip}getRoot(){return this._localRoot||this._mixer._root}_update(t,e,n,i){if(!this.enabled)return void this._updateWeight(t);const r=this._startTime;if(null!==r){const i=(t-r)*n;if(i<0||0===n)return;this._startTime=null,e=n*i}e*=this._updateTimeScale(t);const s=this._updateTime(e),a=this._updateWeight(t);if(a>0){const t=this._interpolants,e=this._propertyBindings;switch(this.blendMode){case q:for(let n=0,i=t.length;n!==i;++n)t[n].evaluate(s),e[n].accumulateAdditive(a);break;case j:default:for(let n=0,r=t.length;n!==r;++n)t[n].evaluate(s),e[n].accumulate(i,a)}}}_updateWeight(t){let e=0;if(this.enabled){e=this.weight;const n=this._weightInterpolant;if(null!==n){const i=n.evaluate(t)[0];e*=i,t>n.parameterPositions[1]&&(this.stopFading(),0===i&&(this.enabled=!1))}}return this._effectiveWeight=e,e}_updateTimeScale(t){let e=0;if(!this.paused){e=this.timeScale;const n=this._timeScaleInterpolant;if(null!==n){e*=n.evaluate(t)[0],t>n.parameterPositions[1]&&(this.stopWarping(),0===e?this.paused=!0:this.timeScale=e)}}return this._effectiveTimeScale=e,e}_updateTime(t){const e=this._clip.duration,n=this.loop;let i=this.time+t,r=this._loopCount;const s=2202===n;if(0===t)return-1===r?i:s&&1==(1&r)?e-i:i;if(2200===n){-1===r&&(this._loopCount=0,this._setEndings(!0,!0,!1));t:{if(i>=e)i=e;else{if(!(i<0)){this.time=i;break t}i=0}this.clampWhenFinished?this.paused=!0:this.enabled=!1,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:t<0?-1:1})}}else{if(-1===r&&(t>=0?(r=0,this._setEndings(!0,0===this.repetitions,s)):this._setEndings(0===this.repetitions,!0,s)),i>=e||i<0){const n=Math.floor(i/e);i-=e*n,r+=Math.abs(n);const a=this.repetitions-r;if(a<=0)this.clampWhenFinished?this.paused=!0:this.enabled=!1,i=t>0?e:0,this.time=i,this._mixer.dispatchEvent({type:"finished",action:this,direction:t>0?1:-1});else{if(1===a){const e=t<0;this._setEndings(e,!e,s)}else this._setEndings(!1,!1,s);this._loopCount=r,this.time=i,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:n})}}else this.time=i;if(s&&1==(1&r))return e-i}return i}_setEndings(t,e,n){const i=this._interpolantSettings;n?(i.endingStart=V,i.endingEnd=V):(i.endingStart=t?this.zeroSlopeAtStart?V:k:W,i.endingEnd=e?this.zeroSlopeAtEnd?V:k:W)}_scheduleFading(t,e,n){const i=this._mixer,r=i.time;let s=this._weightInterpolant;null===s&&(s=i._lendControlInterpolant(),this._weightInterpolant=s);const a=s.parameterPositions,o=s.sampleValues;return a[0]=r,o[0]=e,a[1]=r+t,o[1]=n,this}}class Yc extends rt{constructor(t){super(),this._root=t,this._initMemoryManager(),this._accuIndex=0,this.time=0,this.timeScale=1}_bindAction(t,e){const n=t._localRoot||this._root,i=t._clip.tracks,r=i.length,s=t._propertyBindings,a=t._interpolants,o=n.uuid,l=this._bindingsByRootAndName;let c=l[o];void 0===c&&(c={},l[o]=c);for(let t=0;t!==r;++t){const r=i[t],l=r.name;let h=c[l];if(void 0!==h)s[t]=h;else{if(h=s[t],void 0!==h){null===h._cacheIndex&&(++h.referenceCount,this._addInactiveBinding(h,o,l));continue}const i=e&&e._propertyBindings[t].binding.parsedPath;h=new Nc(jc.create(n,l,i),r.ValueTypeName,r.getValueSize()),++h.referenceCount,this._addInactiveBinding(h,o,l),s[t]=h}a[t].resultBuffer=h.buffer}}_activateAction(t){if(!this._isActiveAction(t)){if(null===t._cacheIndex){const e=(t._localRoot||this._root).uuid,n=t._clip.uuid,i=this._actionsByClip[n];this._bindAction(t,i&&i.knownActions[0]),this._addInactiveAction(t,n,e)}const e=t._propertyBindings;for(let t=0,n=e.length;t!==n;++t){const n=e[t];0==n.useCount++&&(this._lendBinding(n),n.saveOriginalState())}this._lendAction(t)}}_deactivateAction(t){if(this._isActiveAction(t)){const e=t._propertyBindings;for(let t=0,n=e.length;t!==n;++t){const n=e[t];0==--n.useCount&&(n.restoreOriginalState(),this._takeBackBinding(n))}this._takeBackAction(t)}}_initMemoryManager(){this._actions=[],this._nActiveActions=0,this._actionsByClip={},this._bindings=[],this._nActiveBindings=0,this._bindingsByRootAndName={},this._controlInterpolants=[],this._nActiveControlInterpolants=0;const t=this;this.stats={actions:{get total(){return t._actions.length},get inUse(){return t._nActiveActions}},bindings:{get total(){return t._bindings.length},get inUse(){return t._nActiveBindings}},controlInterpolants:{get total(){return t._controlInterpolants.length},get inUse(){return t._nActiveControlInterpolants}}}}_isActiveAction(t){const e=t._cacheIndex;return null!==e&&e=0;--e)t[e].stop();return this}update(t){t*=this.timeScale;const e=this._actions,n=this._nActiveActions,i=this.time+=t,r=Math.sign(t),s=this._accuIndex^=1;for(let a=0;a!==n;++a){e[a]._update(i,t,r,s)}const a=this._bindings,o=this._nActiveBindings;for(let t=0;t!==o;++t)a[t].apply(s);return this}setTime(t){this.time=0;for(let t=0;tthis.max.x||t.ythis.max.y)}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y}getParameter(t,e){return void 0===e&&(console.warn("THREE.Box2: .getParameter() target is now required"),e=new vt),e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y))}intersectsBox(t){return!(t.max.xthis.max.x||t.max.ythis.max.y)}clampPoint(t,e){return void 0===e&&(console.warn("THREE.Box2: .clampPoint() target is now required"),e=new vt),e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return th.copy(t).clamp(this.min,this.max).sub(t).length()}intersect(t){return this.min.max(t.min),this.max.min(t.max),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}eh.prototype.isBox2=!0;const nh=new Lt,ih=new Lt;class rh{constructor(t=new Lt,e=new Lt){this.start=t,this.end=e}set(t,e){return this.start.copy(t),this.end.copy(e),this}copy(t){return this.start.copy(t.start),this.end.copy(t.end),this}getCenter(t){return void 0===t&&(console.warn("THREE.Line3: .getCenter() target is now required"),t=new Lt),t.addVectors(this.start,this.end).multiplyScalar(.5)}delta(t){return void 0===t&&(console.warn("THREE.Line3: .delta() target is now required"),t=new Lt),t.subVectors(this.end,this.start)}distanceSq(){return this.start.distanceToSquared(this.end)}distance(){return this.start.distanceTo(this.end)}at(t,e){return void 0===e&&(console.warn("THREE.Line3: .at() target is now required"),e=new Lt),this.delta(e).multiplyScalar(t).add(this.start)}closestPointToPointParameter(t,e){nh.subVectors(t,this.start),ih.subVectors(this.end,this.start);const n=ih.dot(ih);let i=ih.dot(nh)/n;return e&&(i=ht(i,0,1)),i}closestPointToPoint(t,e,n){const i=this.closestPointToPointParameter(t,e);return void 0===n&&(console.warn("THREE.Line3: .closestPointToPoint() target is now required"),n=new Lt),this.delta(n).multiplyScalar(i).add(this.start)}applyMatrix4(t){return this.start.applyMatrix4(t),this.end.applyMatrix4(t),this}equals(t){return t.start.equals(this.start)&&t.end.equals(this.end)}clone(){return(new this.constructor).copy(this)}}class sh extends Ce{constructor(t){super(),this.material=t,this.render=function(){},this.hasPositions=!1,this.hasNormals=!1,this.hasColors=!1,this.hasUvs=!1,this.positionArray=null,this.normalArray=null,this.colorArray=null,this.uvArray=null,this.count=0}}sh.prototype.isImmediateRenderObject=!0;const ah=new Lt;const oh=new Lt,lh=new se,ch=new se;class hh extends ya{constructor(t){const e=uh(t),n=new En,i=[],r=[],s=new tn(0,0,1),a=new tn(0,1,0);for(let t=0;t4?a=Ph[r-8+4-1]:0==r&&(a=0),n.push(a);const o=1/(s-1),l=-o/2,c=1+o/2,h=[l,l,c,l,c,c,l,l,c,c,l,c],u=6,d=6,p=3,m=2,f=1,g=new Float32Array(p*d*u),v=new Float32Array(m*d*u),y=new Float32Array(f*d*u);for(let t=0;t2?0:-1,i=[e,n,0,e+2/3,n,0,e+2/3,n+1,0,e,n,0,e+2/3,n+1,0,e,n+1,0];g.set(i,p*d*t),v.set(h,m*d*t);const r=[t,t,t,t,t,t];y.set(r,f*d*t)}const x=new En;x.setAttribute("position",new sn(g,p)),x.setAttribute("uv",new sn(v,m)),x.setAttribute("faceIndex",new sn(y,f)),t.push(x),i>4&&i--}return{_lodPlanes:t,_sizeLods:e,_sigmas:n}}function Zh(t){const e=new Tt(3*Ch,3*Ch,t);return e.texture.mapping=l,e.texture.name="PMREM.cubeUv",e.scissorTest=!0,e}function Jh(t,e,n,i,r){t.viewport.set(e,n,i,r),t.scissor.set(e,n,i,r)}function Qh(){const t=new vt(1,1);return new Io({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null},texelSize:{value:t},inputEncoding:{value:Nh[3e3]},outputEncoding:{value:Nh[3e3]}},vertexShader:$h(),fragmentShader:`\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform sampler2D envMap;\n\t\t\tuniform vec2 texelSize;\n\n\t\t\t${tu()}\n\n\t\t\t#include \n\n\t\t\tvoid main() {\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\n\t\t\t\tvec3 outputDirection = normalize( vOutputDirection );\n\t\t\t\tvec2 uv = equirectUv( outputDirection );\n\n\t\t\t\tvec2 f = fract( uv / texelSize - 0.5 );\n\t\t\t\tuv -= f * texelSize;\n\t\t\t\tvec3 tl = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;\n\t\t\t\tuv.x += texelSize.x;\n\t\t\t\tvec3 tr = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;\n\t\t\t\tuv.y += texelSize.y;\n\t\t\t\tvec3 br = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;\n\t\t\t\tuv.x -= texelSize.x;\n\t\t\t\tvec3 bl = envMapTexelToLinear( texture2D ( envMap, uv ) ).rgb;\n\n\t\t\t\tvec3 tm = mix( tl, tr, f.x );\n\t\t\t\tvec3 bm = mix( bl, br, f.x );\n\t\t\t\tgl_FragColor.rgb = mix( tm, bm, f.y );\n\n\t\t\t\tgl_FragColor = linearToOutputTexel( gl_FragColor );\n\n\t\t\t}\n\t\t`,blending:0,depthTest:!1,depthWrite:!1})}function Kh(){return new Io({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},inputEncoding:{value:Nh[3e3]},outputEncoding:{value:Nh[3e3]}},vertexShader:$h(),fragmentShader:`\n\n\t\t\tprecision mediump float;\n\t\t\tprecision mediump int;\n\n\t\t\tvarying vec3 vOutputDirection;\n\n\t\t\tuniform samplerCube envMap;\n\n\t\t\t${tu()}\n\n\t\t\tvoid main() {\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t\t\t\tgl_FragColor.rgb = envMapTexelToLinear( textureCube( envMap, vec3( - vOutputDirection.x, vOutputDirection.yz ) ) ).rgb;\n\t\t\t\tgl_FragColor = linearToOutputTexel( gl_FragColor );\n\n\t\t\t}\n\t\t`,blending:0,depthTest:!1,depthWrite:!1})}function $h(){return"\n\n\t\tprecision mediump float;\n\t\tprecision mediump int;\n\n\t\tattribute vec3 position;\n\t\tattribute vec2 uv;\n\t\tattribute float faceIndex;\n\n\t\tvarying vec3 vOutputDirection;\n\n\t\t// RH coordinate system; PMREM face-indexing convention\n\t\tvec3 getDirection( vec2 uv, float face ) {\n\n\t\t\tuv = 2.0 * uv - 1.0;\n\n\t\t\tvec3 direction = vec3( uv, 1.0 );\n\n\t\t\tif ( face == 0.0 ) {\n\n\t\t\t\tdirection = direction.zyx; // ( 1, v, u ) pos x\n\n\t\t\t} else if ( face == 1.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xz *= -1.0; // ( -u, 1, -v ) pos y\n\n\t\t\t} else if ( face == 2.0 ) {\n\n\t\t\t\tdirection.x *= -1.0; // ( -u, v, 1 ) pos z\n\n\t\t\t} else if ( face == 3.0 ) {\n\n\t\t\t\tdirection = direction.zyx;\n\t\t\t\tdirection.xz *= -1.0; // ( -1, v, -u ) neg x\n\n\t\t\t} else if ( face == 4.0 ) {\n\n\t\t\t\tdirection = direction.xzy;\n\t\t\t\tdirection.xy *= -1.0; // ( -u, -1, v ) neg y\n\n\t\t\t} else if ( face == 5.0 ) {\n\n\t\t\t\tdirection.z *= -1.0; // ( u, v, -1 ) neg z\n\n\t\t\t}\n\n\t\t\treturn direction;\n\n\t\t}\n\n\t\tvoid main() {\n\n\t\t\tvOutputDirection = getDirection( uv, faceIndex );\n\t\t\tgl_Position = vec4( position, 1.0 );\n\n\t\t}\n\t"}function tu(){return"\n\n\t\tuniform int inputEncoding;\n\t\tuniform int outputEncoding;\n\n\t\t#include \n\n\t\tvec4 inputTexelToLinear( vec4 value ) {\n\n\t\t\tif ( inputEncoding == 0 ) {\n\n\t\t\t\treturn value;\n\n\t\t\t} else if ( inputEncoding == 1 ) {\n\n\t\t\t\treturn sRGBToLinear( value );\n\n\t\t\t} else if ( inputEncoding == 2 ) {\n\n\t\t\t\treturn RGBEToLinear( value );\n\n\t\t\t} else if ( inputEncoding == 3 ) {\n\n\t\t\t\treturn RGBMToLinear( value, 7.0 );\n\n\t\t\t} else if ( inputEncoding == 4 ) {\n\n\t\t\t\treturn RGBMToLinear( value, 16.0 );\n\n\t\t\t} else if ( inputEncoding == 5 ) {\n\n\t\t\t\treturn RGBDToLinear( value, 256.0 );\n\n\t\t\t} else {\n\n\t\t\t\treturn GammaToLinear( value, 2.2 );\n\n\t\t\t}\n\n\t\t}\n\n\t\tvec4 linearToOutputTexel( vec4 value ) {\n\n\t\t\tif ( outputEncoding == 0 ) {\n\n\t\t\t\treturn value;\n\n\t\t\t} else if ( outputEncoding == 1 ) {\n\n\t\t\t\treturn LinearTosRGB( value );\n\n\t\t\t} else if ( outputEncoding == 2 ) {\n\n\t\t\t\treturn LinearToRGBE( value );\n\n\t\t\t} else if ( outputEncoding == 3 ) {\n\n\t\t\t\treturn LinearToRGBM( value, 7.0 );\n\n\t\t\t} else if ( outputEncoding == 4 ) {\n\n\t\t\t\treturn LinearToRGBM( value, 16.0 );\n\n\t\t\t} else if ( outputEncoding == 5 ) {\n\n\t\t\t\treturn LinearToRGBD( value, 256.0 );\n\n\t\t\t} else {\n\n\t\t\t\treturn LinearToGamma( value, 2.2 );\n\n\t\t\t}\n\n\t\t}\n\n\t\tvec4 envMapTexelToLinear( vec4 color ) {\n\n\t\t\treturn inputTexelToLinear( color );\n\n\t\t}\n\t"}ml.create=function(t,e){return console.log("THREE.Curve.create() has been deprecated"),t.prototype=Object.create(ml.prototype),t.prototype.constructor=t,t.prototype.getPoint=e,t},Bl.prototype.fromPoints=function(t){return console.warn("THREE.Path: .fromPoints() has been renamed to .setFromPoints()."),this.setFromPoints(t)},fh.prototype.setColors=function(){console.error("THREE.GridHelper: setColors() has been deprecated, pass them in the constructor instead.")},hh.prototype.update=function(){console.error("THREE.SkeletonHelper: update() no longer needs to be called.")},ol.prototype.extractUrlBase=function(t){return console.warn("THREE.Loader: .extractUrlBase() has been deprecated. Use THREE.LoaderUtils.extractUrlBase() instead."),rc.extractUrlBase(t)},ol.Handlers={add:function(){console.error("THREE.Loader: Handlers.add() has been removed. Use LoadingManager.addHandler() instead.")},get:function(){console.error("THREE.Loader: Handlers.get() has been removed. Use LoadingManager.getHandler() instead.")}},eh.prototype.center=function(t){return console.warn("THREE.Box2: .center() has been renamed to .getCenter()."),this.getCenter(t)},eh.prototype.empty=function(){return console.warn("THREE.Box2: .empty() has been renamed to .isEmpty()."),this.isEmpty()},eh.prototype.isIntersectionBox=function(t){return console.warn("THREE.Box2: .isIntersectionBox() has been renamed to .intersectsBox()."),this.intersectsBox(t)},eh.prototype.size=function(t){return console.warn("THREE.Box2: .size() has been renamed to .getSize()."),this.getSize(t)},Pt.prototype.center=function(t){return console.warn("THREE.Box3: .center() has been renamed to .getCenter()."),this.getCenter(t)},Pt.prototype.empty=function(){return console.warn("THREE.Box3: .empty() has been renamed to .isEmpty()."),this.isEmpty()},Pt.prototype.isIntersectionBox=function(t){return console.warn("THREE.Box3: .isIntersectionBox() has been renamed to .intersectsBox()."),this.intersectsBox(t)},Pt.prototype.isIntersectionSphere=function(t){return console.warn("THREE.Box3: .isIntersectionSphere() has been renamed to .intersectsSphere()."),this.intersectsSphere(t)},Pt.prototype.size=function(t){return console.warn("THREE.Box3: .size() has been renamed to .getSize()."),this.getSize(t)},Jt.prototype.empty=function(){return console.warn("THREE.Sphere: .empty() has been renamed to .isEmpty()."),this.isEmpty()},ai.prototype.setFromMatrix=function(t){return console.warn("THREE.Frustum: .setFromMatrix() has been renamed to .setFromProjectionMatrix()."),this.setFromProjectionMatrix(t)},rh.prototype.center=function(t){return console.warn("THREE.Line3: .center() has been renamed to .getCenter()."),this.getCenter(t)},yt.prototype.flattenToArrayOffset=function(t,e){return console.warn("THREE.Matrix3: .flattenToArrayOffset() has been deprecated. Use .toArray() instead."),this.toArray(t,e)},yt.prototype.multiplyVector3=function(t){return console.warn("THREE.Matrix3: .multiplyVector3() has been removed. Use vector.applyMatrix3( matrix ) instead."),t.applyMatrix3(this)},yt.prototype.multiplyVector3Array=function(){console.error("THREE.Matrix3: .multiplyVector3Array() has been removed.")},yt.prototype.applyToBufferAttribute=function(t){return console.warn("THREE.Matrix3: .applyToBufferAttribute() has been removed. Use attribute.applyMatrix3( matrix ) instead."),t.applyMatrix3(this)},yt.prototype.applyToVector3Array=function(){console.error("THREE.Matrix3: .applyToVector3Array() has been removed.")},yt.prototype.getInverse=function(t){return console.warn("THREE.Matrix3: .getInverse() has been removed. Use matrixInv.copy( matrix ).invert(); instead."),this.copy(t).invert()},se.prototype.extractPosition=function(t){return console.warn("THREE.Matrix4: .extractPosition() has been renamed to .copyPosition()."),this.copyPosition(t)},se.prototype.flattenToArrayOffset=function(t,e){return console.warn("THREE.Matrix4: .flattenToArrayOffset() has been deprecated. Use .toArray() instead."),this.toArray(t,e)},se.prototype.getPosition=function(){return console.warn("THREE.Matrix4: .getPosition() has been removed. Use Vector3.setFromMatrixPosition( matrix ) instead."),(new Lt).setFromMatrixColumn(this,3)},se.prototype.setRotationFromQuaternion=function(t){return console.warn("THREE.Matrix4: .setRotationFromQuaternion() has been renamed to .makeRotationFromQuaternion()."),this.makeRotationFromQuaternion(t)},se.prototype.multiplyToArray=function(){console.warn("THREE.Matrix4: .multiplyToArray() has been removed.")},se.prototype.multiplyVector3=function(t){return console.warn("THREE.Matrix4: .multiplyVector3() has been removed. Use vector.applyMatrix4( matrix ) instead."),t.applyMatrix4(this)},se.prototype.multiplyVector4=function(t){return console.warn("THREE.Matrix4: .multiplyVector4() has been removed. Use vector.applyMatrix4( matrix ) instead."),t.applyMatrix4(this)},se.prototype.multiplyVector3Array=function(){console.error("THREE.Matrix4: .multiplyVector3Array() has been removed.")},se.prototype.rotateAxis=function(t){console.warn("THREE.Matrix4: .rotateAxis() has been removed. Use Vector3.transformDirection( matrix ) instead."),t.transformDirection(this)},se.prototype.crossVector=function(t){return console.warn("THREE.Matrix4: .crossVector() has been removed. Use vector.applyMatrix4( matrix ) instead."),t.applyMatrix4(this)},se.prototype.translate=function(){console.error("THREE.Matrix4: .translate() has been removed.")},se.prototype.rotateX=function(){console.error("THREE.Matrix4: .rotateX() has been removed.")},se.prototype.rotateY=function(){console.error("THREE.Matrix4: .rotateY() has been removed.")},se.prototype.rotateZ=function(){console.error("THREE.Matrix4: .rotateZ() has been removed.")},se.prototype.rotateByAxis=function(){console.error("THREE.Matrix4: .rotateByAxis() has been removed.")},se.prototype.applyToBufferAttribute=function(t){return console.warn("THREE.Matrix4: .applyToBufferAttribute() has been removed. Use attribute.applyMatrix4( matrix ) instead."),t.applyMatrix4(this)},se.prototype.applyToVector3Array=function(){console.error("THREE.Matrix4: .applyToVector3Array() has been removed.")},se.prototype.makeFrustum=function(t,e,n,i,r,s){return console.warn("THREE.Matrix4: .makeFrustum() has been removed. Use .makePerspective( left, right, top, bottom, near, far ) instead."),this.makePerspective(t,e,i,n,r,s)},se.prototype.getInverse=function(t){return console.warn("THREE.Matrix4: .getInverse() has been removed. Use matrixInv.copy( matrix ).invert(); instead."),this.copy(t).invert()},Ne.prototype.isIntersectionLine=function(t){return console.warn("THREE.Plane: .isIntersectionLine() has been renamed to .intersectsLine()."),this.intersectsLine(t)},At.prototype.multiplyVector3=function(t){return console.warn("THREE.Quaternion: .multiplyVector3() has been removed. Use is now vector.applyQuaternion( quaternion ) instead."),t.applyQuaternion(this)},At.prototype.inverse=function(){return console.warn("THREE.Quaternion: .inverse() has been renamed to invert()."),this.invert()},re.prototype.isIntersectionBox=function(t){return console.warn("THREE.Ray: .isIntersectionBox() has been renamed to .intersectsBox()."),this.intersectsBox(t)},re.prototype.isIntersectionPlane=function(t){return console.warn("THREE.Ray: .isIntersectionPlane() has been renamed to .intersectsPlane()."),this.intersectsPlane(t)},re.prototype.isIntersectionSphere=function(t){return console.warn("THREE.Ray: .isIntersectionSphere() has been renamed to .intersectsSphere()."),this.intersectsSphere(t)},je.prototype.area=function(){return console.warn("THREE.Triangle: .area() has been renamed to .getArea()."),this.getArea()},je.prototype.barycoordFromPoint=function(t,e){return console.warn("THREE.Triangle: .barycoordFromPoint() has been renamed to .getBarycoord()."),this.getBarycoord(t,e)},je.prototype.midpoint=function(t){return console.warn("THREE.Triangle: .midpoint() has been renamed to .getMidpoint()."),this.getMidpoint(t)},je.prototypenormal=function(t){return console.warn("THREE.Triangle: .normal() has been renamed to .getNormal()."),this.getNormal(t)},je.prototype.plane=function(t){return console.warn("THREE.Triangle: .plane() has been renamed to .getPlane()."),this.getPlane(t)},je.barycoordFromPoint=function(t,e,n,i,r){return console.warn("THREE.Triangle: .barycoordFromPoint() has been renamed to .getBarycoord()."),je.getBarycoord(t,e,n,i,r)},je.normal=function(t,e,n,i){return console.warn("THREE.Triangle: .normal() has been renamed to .getNormal()."),je.getNormal(t,e,n,i)},zl.prototype.extractAllPoints=function(t){return console.warn("THREE.Shape: .extractAllPoints() has been removed. Use .extractPoints() instead."),this.extractPoints(t)},zl.prototype.extrude=function(t){return console.warn("THREE.Shape: .extrude() has been removed. Use ExtrudeGeometry() instead."),new go(this,t)},zl.prototype.makeGeometry=function(t){return console.warn("THREE.Shape: .makeGeometry() has been removed. Use ShapeGeometry() instead."),new Mo(this,t)},vt.prototype.fromAttribute=function(t,e,n){return console.warn("THREE.Vector2: .fromAttribute() has been renamed to .fromBufferAttribute()."),this.fromBufferAttribute(t,e,n)},vt.prototype.distanceToManhattan=function(t){return console.warn("THREE.Vector2: .distanceToManhattan() has been renamed to .manhattanDistanceTo()."),this.manhattanDistanceTo(t)},vt.prototype.lengthManhattan=function(){return console.warn("THREE.Vector2: .lengthManhattan() has been renamed to .manhattanLength()."),this.manhattanLength()},Lt.prototype.setEulerFromRotationMatrix=function(){console.error("THREE.Vector3: .setEulerFromRotationMatrix() has been removed. Use Euler.setFromRotationMatrix() instead.")},Lt.prototype.setEulerFromQuaternion=function(){console.error("THREE.Vector3: .setEulerFromQuaternion() has been removed. Use Euler.setFromQuaternion() instead.")},Lt.prototype.getPositionFromMatrix=function(t){return console.warn("THREE.Vector3: .getPositionFromMatrix() has been renamed to .setFromMatrixPosition()."),this.setFromMatrixPosition(t)},Lt.prototype.getScaleFromMatrix=function(t){return console.warn("THREE.Vector3: .getScaleFromMatrix() has been renamed to .setFromMatrixScale()."),this.setFromMatrixScale(t)},Lt.prototype.getColumnFromMatrix=function(t,e){return console.warn("THREE.Vector3: .getColumnFromMatrix() has been renamed to .setFromMatrixColumn()."),this.setFromMatrixColumn(e,t)},Lt.prototype.applyProjection=function(t){return console.warn("THREE.Vector3: .applyProjection() has been removed. Use .applyMatrix4( m ) instead."),this.applyMatrix4(t)},Lt.prototype.fromAttribute=function(t,e,n){return console.warn("THREE.Vector3: .fromAttribute() has been renamed to .fromBufferAttribute()."),this.fromBufferAttribute(t,e,n)},Lt.prototype.distanceToManhattan=function(t){return console.warn("THREE.Vector3: .distanceToManhattan() has been renamed to .manhattanDistanceTo()."),this.manhattanDistanceTo(t)},Lt.prototype.lengthManhattan=function(){return console.warn("THREE.Vector3: .lengthManhattan() has been renamed to .manhattanLength()."),this.manhattanLength()},St.prototype.fromAttribute=function(t,e,n){return console.warn("THREE.Vector4: .fromAttribute() has been renamed to .fromBufferAttribute()."),this.fromBufferAttribute(t,e,n)},St.prototype.lengthManhattan=function(){return console.warn("THREE.Vector4: .lengthManhattan() has been renamed to .manhattanLength()."),this.manhattanLength()},Ce.prototype.getChildByName=function(t){return console.warn("THREE.Object3D: .getChildByName() has been renamed to .getObjectByName()."),this.getObjectByName(t)},Ce.prototype.renderDepth=function(){console.warn("THREE.Object3D: .renderDepth has been removed. Use .renderOrder, instead.")},Ce.prototype.translate=function(t,e){return console.warn("THREE.Object3D: .translate() has been removed. Use .translateOnAxis( axis, distance ) instead."),this.translateOnAxis(e,t)},Ce.prototype.getWorldRotation=function(){console.error("THREE.Object3D: .getWorldRotation() has been removed. Use THREE.Object3D.getWorldQuaternion( target ) instead.")},Ce.prototype.applyMatrix=function(t){return console.warn("THREE.Object3D: .applyMatrix() has been renamed to .applyMatrix4()."),this.applyMatrix4(t)},Object.defineProperties(Ce.prototype,{eulerOrder:{get:function(){return console.warn("THREE.Object3D: .eulerOrder is now .rotation.order."),this.rotation.order},set:function(t){console.warn("THREE.Object3D: .eulerOrder is now .rotation.order."),this.rotation.order=t}},useQuaternion:{get:function(){console.warn("THREE.Object3D: .useQuaternion has been removed. The library now uses quaternions by default.")},set:function(){console.warn("THREE.Object3D: .useQuaternion has been removed. The library now uses quaternions by default.")}}}),Wn.prototype.setDrawMode=function(){console.error("THREE.Mesh: .setDrawMode() has been removed. The renderer now always assumes THREE.TrianglesDrawMode. Transform your geometry via BufferGeometryUtils.toTrianglesDrawMode() if necessary.")},Object.defineProperties(Wn.prototype,{drawMode:{get:function(){return console.error("THREE.Mesh: .drawMode has been removed. The renderer now always assumes THREE.TrianglesDrawMode."),0},set:function(){console.error("THREE.Mesh: .drawMode has been removed. The renderer now always assumes THREE.TrianglesDrawMode. Transform your geometry via BufferGeometryUtils.toTrianglesDrawMode() if necessary.")}}}),$s.prototype.initBones=function(){console.error("THREE.SkinnedMesh: initBones() has been removed.")},Kn.prototype.setLens=function(t,e){console.warn("THREE.PerspectiveCamera.setLens is deprecated. Use .setFocalLength and .filmGauge for a photographic setup."),void 0!==e&&(this.filmGauge=e),this.setFocalLength(t)},Object.defineProperties(Fl.prototype,{onlyShadow:{set:function(){console.warn("THREE.Light: .onlyShadow has been removed.")}},shadowCameraFov:{set:function(t){console.warn("THREE.Light: .shadowCameraFov is now .shadow.camera.fov."),this.shadow.camera.fov=t}},shadowCameraLeft:{set:function(t){console.warn("THREE.Light: .shadowCameraLeft is now .shadow.camera.left."),this.shadow.camera.left=t}},shadowCameraRight:{set:function(t){console.warn("THREE.Light: .shadowCameraRight is now .shadow.camera.right."),this.shadow.camera.right=t}},shadowCameraTop:{set:function(t){console.warn("THREE.Light: .shadowCameraTop is now .shadow.camera.top."),this.shadow.camera.top=t}},shadowCameraBottom:{set:function(t){console.warn("THREE.Light: .shadowCameraBottom is now .shadow.camera.bottom."),this.shadow.camera.bottom=t}},shadowCameraNear:{set:function(t){console.warn("THREE.Light: .shadowCameraNear is now .shadow.camera.near."),this.shadow.camera.near=t}},shadowCameraFar:{set:function(t){console.warn("THREE.Light: .shadowCameraFar is now .shadow.camera.far."),this.shadow.camera.far=t}},shadowCameraVisible:{set:function(){console.warn("THREE.Light: .shadowCameraVisible has been removed. Use new THREE.CameraHelper( light.shadow.camera ) instead.")}},shadowBias:{set:function(t){console.warn("THREE.Light: .shadowBias is now .shadow.bias."),this.shadow.bias=t}},shadowDarkness:{set:function(){console.warn("THREE.Light: .shadowDarkness has been removed.")}},shadowMapWidth:{set:function(t){console.warn("THREE.Light: .shadowMapWidth is now .shadow.mapSize.width."),this.shadow.mapSize.width=t}},shadowMapHeight:{set:function(t){console.warn("THREE.Light: .shadowMapHeight is now .shadow.mapSize.height."),this.shadow.mapSize.height=t}}}),Object.defineProperties(sn.prototype,{length:{get:function(){return console.warn("THREE.BufferAttribute: .length has been deprecated. Use .count instead."),this.array.length}},dynamic:{get:function(){return console.warn("THREE.BufferAttribute: .dynamic has been deprecated. Use .usage instead."),this.usage===nt},set:function(){console.warn("THREE.BufferAttribute: .dynamic has been deprecated. Use .usage instead."),this.setUsage(nt)}}}),sn.prototype.setDynamic=function(t){return console.warn("THREE.BufferAttribute: .setDynamic() has been deprecated. Use .setUsage() instead."),this.setUsage(!0===t?nt:et),this},sn.prototype.copyIndicesArray=function(){console.error("THREE.BufferAttribute: .copyIndicesArray() has been removed.")},sn.prototype.setArray=function(){console.error("THREE.BufferAttribute: .setArray has been removed. Use BufferGeometry .setAttribute to replace/resize attribute buffers")},En.prototype.addIndex=function(t){console.warn("THREE.BufferGeometry: .addIndex() has been renamed to .setIndex()."),this.setIndex(t)},En.prototype.addAttribute=function(t,e){return console.warn("THREE.BufferGeometry: .addAttribute() has been renamed to .setAttribute()."),e&&e.isBufferAttribute||e&&e.isInterleavedBufferAttribute?"index"===t?(console.warn("THREE.BufferGeometry.addAttribute: Use .setIndex() for index attribute."),this.setIndex(e),this):this.setAttribute(t,e):(console.warn("THREE.BufferGeometry: .addAttribute() now expects ( name, attribute )."),this.setAttribute(t,new sn(arguments[1],arguments[2])))},En.prototype.addDrawCall=function(t,e,n){void 0!==n&&console.warn("THREE.BufferGeometry: .addDrawCall() no longer supports indexOffset."),console.warn("THREE.BufferGeometry: .addDrawCall() is now .addGroup()."),this.addGroup(t,e)},En.prototype.clearDrawCalls=function(){console.warn("THREE.BufferGeometry: .clearDrawCalls() is now .clearGroups()."),this.clearGroups()},En.prototype.computeOffsets=function(){console.warn("THREE.BufferGeometry: .computeOffsets() has been removed.")},En.prototype.removeAttribute=function(t){return console.warn("THREE.BufferGeometry: .removeAttribute() has been renamed to .deleteAttribute()."),this.deleteAttribute(t)},En.prototype.applyMatrix=function(t){return console.warn("THREE.BufferGeometry: .applyMatrix() has been renamed to .applyMatrix4()."),this.applyMatrix4(t)},Object.defineProperties(En.prototype,{drawcalls:{get:function(){return console.error("THREE.BufferGeometry: .drawcalls has been renamed to .groups."),this.groups}},offsets:{get:function(){return console.warn("THREE.BufferGeometry: .offsets has been renamed to .groups."),this.groups}}}),Es.prototype.setDynamic=function(t){return console.warn("THREE.InterleavedBuffer: .setDynamic() has been deprecated. Use .setUsage() instead."),this.setUsage(!0===t?nt:et),this},Es.prototype.setArray=function(){console.error("THREE.InterleavedBuffer: .setArray has been removed. Use BufferGeometry .setAttribute to replace/resize attribute buffers")},go.prototype.getArrays=function(){console.error("THREE.ExtrudeGeometry: .getArrays() has been removed.")},go.prototype.addShapeList=function(){console.error("THREE.ExtrudeGeometry: .addShapeList() has been removed.")},go.prototype.addShape=function(){console.error("THREE.ExtrudeGeometry: .addShape() has been removed.")},Ts.prototype.dispose=function(){console.error("THREE.Scene: .dispose() has been removed.")},Zc.prototype.onUpdate=function(){return console.warn("THREE.Uniform: .onUpdate() has been removed. Use object.onBeforeRender() instead."),this},Object.defineProperties(Xe.prototype,{wrapAround:{get:function(){console.warn("THREE.Material: .wrapAround has been removed.")},set:function(){console.warn("THREE.Material: .wrapAround has been removed.")}},overdraw:{get:function(){console.warn("THREE.Material: .overdraw has been removed.")},set:function(){console.warn("THREE.Material: .overdraw has been removed.")}},wrapRGB:{get:function(){return console.warn("THREE.Material: .wrapRGB has been removed."),new tn}},shading:{get:function(){console.error("THREE."+this.type+": .shading has been removed. Use the boolean .flatShading instead.")},set:function(t){console.warn("THREE."+this.type+": .shading has been removed. Use the boolean .flatShading instead."),this.flatShading=1===t}},stencilMask:{get:function(){return console.warn("THREE."+this.type+": .stencilMask has been removed. Use .stencilFuncMask instead."),this.stencilFuncMask},set:function(t){console.warn("THREE."+this.type+": .stencilMask has been removed. Use .stencilFuncMask instead."),this.stencilFuncMask=t}}}),Object.defineProperties(Jn.prototype,{derivatives:{get:function(){return console.warn("THREE.ShaderMaterial: .derivatives has been moved to .extensions.derivatives."),this.extensions.derivatives},set:function(t){console.warn("THREE. ShaderMaterial: .derivatives has been moved to .extensions.derivatives."),this.extensions.derivatives=t}}}),ws.prototype.clearTarget=function(t,e,n,i){console.warn("THREE.WebGLRenderer: .clearTarget() has been deprecated. Use .setRenderTarget() and .clear() instead."),this.setRenderTarget(t),this.clear(e,n,i)},ws.prototype.animate=function(t){console.warn("THREE.WebGLRenderer: .animate() is now .setAnimationLoop()."),this.setAnimationLoop(t)},ws.prototype.getCurrentRenderTarget=function(){return console.warn("THREE.WebGLRenderer: .getCurrentRenderTarget() is now .getRenderTarget()."),this.getRenderTarget()},ws.prototype.getMaxAnisotropy=function(){return console.warn("THREE.WebGLRenderer: .getMaxAnisotropy() is now .capabilities.getMaxAnisotropy()."),this.capabilities.getMaxAnisotropy()},ws.prototype.getPrecision=function(){return console.warn("THREE.WebGLRenderer: .getPrecision() is now .capabilities.precision."),this.capabilities.precision},ws.prototype.resetGLState=function(){return console.warn("THREE.WebGLRenderer: .resetGLState() is now .state.reset()."),this.state.reset()},ws.prototype.supportsFloatTextures=function(){return console.warn("THREE.WebGLRenderer: .supportsFloatTextures() is now .extensions.get( 'OES_texture_float' )."),this.extensions.get("OES_texture_float")},ws.prototype.supportsHalfFloatTextures=function(){return console.warn("THREE.WebGLRenderer: .supportsHalfFloatTextures() is now .extensions.get( 'OES_texture_half_float' )."),this.extensions.get("OES_texture_half_float")},ws.prototype.supportsStandardDerivatives=function(){return console.warn("THREE.WebGLRenderer: .supportsStandardDerivatives() is now .extensions.get( 'OES_standard_derivatives' )."),this.extensions.get("OES_standard_derivatives")},ws.prototype.supportsCompressedTextureS3TC=function(){return console.warn("THREE.WebGLRenderer: .supportsCompressedTextureS3TC() is now .extensions.get( 'WEBGL_compressed_texture_s3tc' )."),this.extensions.get("WEBGL_compressed_texture_s3tc")},ws.prototype.supportsCompressedTexturePVRTC=function(){return console.warn("THREE.WebGLRenderer: .supportsCompressedTexturePVRTC() is now .extensions.get( 'WEBGL_compressed_texture_pvrtc' )."),this.extensions.get("WEBGL_compressed_texture_pvrtc")},ws.prototype.supportsBlendMinMax=function(){return console.warn("THREE.WebGLRenderer: .supportsBlendMinMax() is now .extensions.get( 'EXT_blend_minmax' )."),this.extensions.get("EXT_blend_minmax")},ws.prototype.supportsVertexTextures=function(){return console.warn("THREE.WebGLRenderer: .supportsVertexTextures() is now .capabilities.vertexTextures."),this.capabilities.vertexTextures},ws.prototype.supportsInstancedArrays=function(){return console.warn("THREE.WebGLRenderer: .supportsInstancedArrays() is now .extensions.get( 'ANGLE_instanced_arrays' )."),this.extensions.get("ANGLE_instanced_arrays")},ws.prototype.enableScissorTest=function(t){console.warn("THREE.WebGLRenderer: .enableScissorTest() is now .setScissorTest()."),this.setScissorTest(t)},ws.prototype.initMaterial=function(){console.warn("THREE.WebGLRenderer: .initMaterial() has been removed.")},ws.prototype.addPrePlugin=function(){console.warn("THREE.WebGLRenderer: .addPrePlugin() has been removed.")},ws.prototype.addPostPlugin=function(){console.warn("THREE.WebGLRenderer: .addPostPlugin() has been removed.")},ws.prototype.updateShadowMap=function(){console.warn("THREE.WebGLRenderer: .updateShadowMap() has been removed.")},ws.prototype.setFaceCulling=function(){console.warn("THREE.WebGLRenderer: .setFaceCulling() has been removed.")},ws.prototype.allocTextureUnit=function(){console.warn("THREE.WebGLRenderer: .allocTextureUnit() has been removed.")},ws.prototype.setTexture=function(){console.warn("THREE.WebGLRenderer: .setTexture() has been removed.")},ws.prototype.setTexture2D=function(){console.warn("THREE.WebGLRenderer: .setTexture2D() has been removed.")},ws.prototype.setTextureCube=function(){console.warn("THREE.WebGLRenderer: .setTextureCube() has been removed.")},ws.prototype.getActiveMipMapLevel=function(){return console.warn("THREE.WebGLRenderer: .getActiveMipMapLevel() is now .getActiveMipmapLevel()."),this.getActiveMipmapLevel()},Object.defineProperties(ws.prototype,{shadowMapEnabled:{get:function(){return this.shadowMap.enabled},set:function(t){console.warn("THREE.WebGLRenderer: .shadowMapEnabled is now .shadowMap.enabled."),this.shadowMap.enabled=t}},shadowMapType:{get:function(){return this.shadowMap.type},set:function(t){console.warn("THREE.WebGLRenderer: .shadowMapType is now .shadowMap.type."),this.shadowMap.type=t}},shadowMapCullFace:{get:function(){console.warn("THREE.WebGLRenderer: .shadowMapCullFace has been removed. Set Material.shadowSide instead.")},set:function(){console.warn("THREE.WebGLRenderer: .shadowMapCullFace has been removed. Set Material.shadowSide instead.")}},context:{get:function(){return console.warn("THREE.WebGLRenderer: .context has been removed. Use .getContext() instead."),this.getContext()}},vr:{get:function(){return console.warn("THREE.WebGLRenderer: .vr has been renamed to .xr"),this.xr}},gammaInput:{get:function(){return console.warn("THREE.WebGLRenderer: .gammaInput has been removed. Set the encoding for textures via Texture.encoding instead."),!1},set:function(){console.warn("THREE.WebGLRenderer: .gammaInput has been removed. Set the encoding for textures via Texture.encoding instead.")}},gammaOutput:{get:function(){return console.warn("THREE.WebGLRenderer: .gammaOutput has been removed. Set WebGLRenderer.outputEncoding instead."),!1},set:function(t){console.warn("THREE.WebGLRenderer: .gammaOutput has been removed. Set WebGLRenderer.outputEncoding instead."),this.outputEncoding=!0===t?Y:X}},toneMappingWhitePoint:{get:function(){return console.warn("THREE.WebGLRenderer: .toneMappingWhitePoint has been removed."),1},set:function(){console.warn("THREE.WebGLRenderer: .toneMappingWhitePoint has been removed.")}}}),Object.defineProperties(us.prototype,{cullFace:{get:function(){console.warn("THREE.WebGLRenderer: .shadowMap.cullFace has been removed. Set Material.shadowSide instead.")},set:function(){console.warn("THREE.WebGLRenderer: .shadowMap.cullFace has been removed. Set Material.shadowSide instead.")}},renderReverseSided:{get:function(){console.warn("THREE.WebGLRenderer: .shadowMap.renderReverseSided has been removed. Set Material.shadowSide instead.")},set:function(){console.warn("THREE.WebGLRenderer: .shadowMap.renderReverseSided has been removed. Set Material.shadowSide instead.")}},renderSingleSided:{get:function(){console.warn("THREE.WebGLRenderer: .shadowMap.renderSingleSided has been removed. Set Material.shadowSide instead.")},set:function(){console.warn("THREE.WebGLRenderer: .shadowMap.renderSingleSided has been removed. Set Material.shadowSide instead.")}}}),Object.defineProperties(Tt.prototype,{wrapS:{get:function(){return console.warn("THREE.WebGLRenderTarget: .wrapS is now .texture.wrapS."),this.texture.wrapS},set:function(t){console.warn("THREE.WebGLRenderTarget: .wrapS is now .texture.wrapS."),this.texture.wrapS=t}},wrapT:{get:function(){return console.warn("THREE.WebGLRenderTarget: .wrapT is now .texture.wrapT."),this.texture.wrapT},set:function(t){console.warn("THREE.WebGLRenderTarget: .wrapT is now .texture.wrapT."),this.texture.wrapT=t}},magFilter:{get:function(){return console.warn("THREE.WebGLRenderTarget: .magFilter is now .texture.magFilter."),this.texture.magFilter},set:function(t){console.warn("THREE.WebGLRenderTarget: .magFilter is now .texture.magFilter."),this.texture.magFilter=t}},minFilter:{get:function(){return console.warn("THREE.WebGLRenderTarget: .minFilter is now .texture.minFilter."),this.texture.minFilter},set:function(t){console.warn("THREE.WebGLRenderTarget: .minFilter is now .texture.minFilter."),this.texture.minFilter=t}},anisotropy:{get:function(){return console.warn("THREE.WebGLRenderTarget: .anisotropy is now .texture.anisotropy."),this.texture.anisotropy},set:function(t){console.warn("THREE.WebGLRenderTarget: .anisotropy is now .texture.anisotropy."),this.texture.anisotropy=t}},offset:{get:function(){return console.warn("THREE.WebGLRenderTarget: .offset is now .texture.offset."),this.texture.offset},set:function(t){console.warn("THREE.WebGLRenderTarget: .offset is now .texture.offset."),this.texture.offset=t}},repeat:{get:function(){return console.warn("THREE.WebGLRenderTarget: .repeat is now .texture.repeat."),this.texture.repeat},set:function(t){console.warn("THREE.WebGLRenderTarget: .repeat is now .texture.repeat."),this.texture.repeat=t}},format:{get:function(){return console.warn("THREE.WebGLRenderTarget: .format is now .texture.format."),this.texture.format},set:function(t){console.warn("THREE.WebGLRenderTarget: .format is now .texture.format."),this.texture.format=t}},type:{get:function(){return console.warn("THREE.WebGLRenderTarget: .type is now .texture.type."),this.texture.type},set:function(t){console.warn("THREE.WebGLRenderTarget: .type is now .texture.type."),this.texture.type=t}},generateMipmaps:{get:function(){return console.warn("THREE.WebGLRenderTarget: .generateMipmaps is now .texture.generateMipmaps."),this.texture.generateMipmaps},set:function(t){console.warn("THREE.WebGLRenderTarget: .generateMipmaps is now .texture.generateMipmaps."),this.texture.generateMipmaps=t}}}),Lc.prototype.load=function(t){console.warn("THREE.Audio: .load has been deprecated. Use THREE.AudioLoader instead.");const e=this;return(new vc).load(t,(function(t){e.setBuffer(t)})),this},Ic.prototype.getData=function(){return console.warn("THREE.AudioAnalyser: .getData() is now .getFrequencyData()."),this.getFrequencyData()},ti.prototype.updateCubeMap=function(t,e){return console.warn("THREE.CubeCamera: .updateCubeMap() is now .update()."),this.update(t,e)},ti.prototype.clear=function(t,e,n,i){return console.warn("THREE.CubeCamera: .clear() is now .renderTarget.clear()."),this.renderTarget.clear(t,e,n,i)},_t.crossOrigin=void 0,_t.loadTexture=function(t,e,n,i){console.warn("THREE.ImageUtils.loadTexture has been deprecated. Use THREE.TextureLoader() instead.");const r=new pl;r.setCrossOrigin(this.crossOrigin);const s=r.load(t,n,void 0,i);return e&&(s.mapping=e),s},_t.loadTextureCube=function(t,e,n,i){console.warn("THREE.ImageUtils.loadTextureCube has been deprecated. Use THREE.CubeTextureLoader() instead.");const r=new ul;r.setCrossOrigin(this.crossOrigin);const s=r.load(t,n,void 0,i);return e&&(s.mapping=e),s},_t.loadCompressedTexture=function(){console.error("THREE.ImageUtils.loadCompressedTexture has been removed. Use THREE.DDSLoader instead.")},_t.loadCompressedTextureCube=function(){console.error("THREE.ImageUtils.loadCompressedTextureCube has been removed. Use THREE.DDSLoader instead.")};const eu={createMultiMaterialObject:function(){console.error("THREE.SceneUtils has been moved to /examples/jsm/utils/SceneUtils.js")},detach:function(){console.error("THREE.SceneUtils has been moved to /examples/jsm/utils/SceneUtils.js")},attach:function(){console.error("THREE.SceneUtils has been moved to /examples/jsm/utils/SceneUtils.js")}};"undefined"!=typeof __THREE_DEVTOOLS__&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("register",{detail:{revision:e}})),"undefined"!=typeof window&&(window.__THREE__?console.warn("WARNING: Multiple instances of Three.js being imported."):window.__THREE__=e),t.ACESFilmicToneMapping=4,t.AddEquation=n,t.AddOperation=2,t.AdditiveAnimationBlendMode=q,t.AdditiveBlending=2,t.AlphaFormat=1021,t.AlwaysDepth=1,t.AlwaysStencilFunc=519,t.AmbientLight=$l,t.AmbientLightProbe=xc,t.AnimationClip=nl,t.AnimationLoader=class extends ol{constructor(t){super(t)}load(t,e,n,i){const r=this,s=new cl(this.manager);s.setPath(this.path),s.setRequestHeader(this.requestHeader),s.setWithCredentials(this.withCredentials),s.load(t,(function(n){try{e(r.parse(JSON.parse(n)))}catch(e){i?i(e):console.error(e),r.manager.itemError(t)}}),n,i)}parse(t){const e=[];for(let n=0;n.99999)this.quaternion.set(0,0,0,1);else if(t.y<-.99999)this.quaternion.set(1,0,0,0);else{Sh.set(t.z,0,-t.x).normalize();const e=Math.acos(t.y);this.quaternion.setFromAxisAngle(Sh,e)}}setLength(t,e=.2*t,n=.2*e){this.line.scale.set(1,Math.max(1e-4,t-e),1),this.line.updateMatrix(),this.cone.scale.set(n,e,n),this.cone.position.y=t,this.cone.updateMatrix()}setColor(t){this.line.material.color.set(t),this.cone.material.color.set(t)}copy(t){return super.copy(t,!1),this.line.copy(t.line),this.cone.copy(t.cone),this}},t.Audio=Lc,t.AudioAnalyser=Ic,t.AudioContext=gc,t.AudioListener=class extends Ce{constructor(){super(),this.type="AudioListener",this.context=gc.getContext(),this.gain=this.context.createGain(),this.gain.connect(this.context.destination),this.filter=null,this.timeDelta=0,this._clock=new bc}getInput(){return this.gain}removeFilter(){return null!==this.filter&&(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination),this.gain.connect(this.context.destination),this.filter=null),this}getFilter(){return this.filter}setFilter(t){return null!==this.filter?(this.gain.disconnect(this.filter),this.filter.disconnect(this.context.destination)):this.gain.disconnect(this.context.destination),this.filter=t,this.gain.connect(this.filter),this.filter.connect(this.context.destination),this}getMasterVolume(){return this.gain.gain.value}setMasterVolume(t){return this.gain.gain.setTargetAtTime(t,this.context.currentTime,.01),this}updateMatrixWorld(t){super.updateMatrixWorld(t);const e=this.context.listener,n=this.up;if(this.timeDelta=this._clock.getDelta(),this.matrixWorld.decompose(Sc,Tc,Ec),Ac.set(0,0,-1).applyQuaternion(Tc),e.positionX){const t=this.context.currentTime+this.timeDelta;e.positionX.linearRampToValueAtTime(Sc.x,t),e.positionY.linearRampToValueAtTime(Sc.y,t),e.positionZ.linearRampToValueAtTime(Sc.z,t),e.forwardX.linearRampToValueAtTime(Ac.x,t),e.forwardY.linearRampToValueAtTime(Ac.y,t),e.forwardZ.linearRampToValueAtTime(Ac.z,t),e.upX.linearRampToValueAtTime(n.x,t),e.upY.linearRampToValueAtTime(n.y,t),e.upZ.linearRampToValueAtTime(n.z,t)}else e.setPosition(Sc.x,Sc.y,Sc.z),e.setOrientation(Ac.x,Ac.y,Ac.z,n.x,n.y,n.z)}},t.AudioLoader=vc,t.AxesHelper=Ah,t.AxisHelper=function(t){return console.warn("THREE.AxisHelper has been renamed to THREE.AxesHelper."),new Ah(t)},t.BackSide=1,t.BasicDepthPacking=3200,t.BasicShadowMap=0,t.BinaryTextureLoader=function(t){return console.warn("THREE.BinaryTextureLoader has been renamed to THREE.DataTextureLoader."),new dl(t)},t.Bone=ta,t.BooleanKeyframeTrack=Zo,t.BoundingBoxHelper=function(t,e){return console.warn("THREE.BoundingBoxHelper has been deprecated. Creating a THREE.BoxHelper instead."),new Mh(t,e)},t.Box2=eh,t.Box3=Pt,t.Box3Helper=class extends ya{constructor(t,e=16776960){const n=new Uint16Array([0,1,1,2,2,3,3,0,4,5,5,6,6,7,7,4,0,4,1,5,2,6,3,7]),i=new En;i.setIndex(new sn(n,1)),i.setAttribute("position",new mn([1,1,1,-1,1,1,-1,-1,1,1,-1,1,1,1,-1,-1,1,-1,-1,-1,-1,1,-1,-1],3)),super(i,new ca({color:e,toneMapped:!1})),this.box=t,this.type="Box3Helper",this.geometry.computeBoundingSphere()}updateMatrixWorld(t){const e=this.box;e.isEmpty()||(e.getCenter(this.position),e.getSize(this.scale),this.scale.multiplyScalar(.5),super.updateMatrixWorld(t))}},t.BoxBufferGeometry=qn,t.BoxGeometry=qn,t.BoxHelper=Mh,t.BufferAttribute=sn,t.BufferGeometry=En,t.BufferGeometryLoader=oc,t.ByteType=1010,t.Cache=rl,t.Camera=Qn,t.CameraHelper=class extends ya{constructor(t){const e=new En,n=new ca({color:16777215,vertexColors:!0,toneMapped:!1}),i=[],r=[],s={},a=new tn(16755200),o=new tn(16711680),l=new tn(43775),c=new tn(16777215),h=new tn(3355443);function u(t,e,n){d(t,n),d(e,n)}function d(t,e){i.push(0,0,0),r.push(e.r,e.g,e.b),void 0===s[t]&&(s[t]=[]),s[t].push(i.length/3-1)}u("n1","n2",a),u("n2","n4",a),u("n4","n3",a),u("n3","n1",a),u("f1","f2",a),u("f2","f4",a),u("f4","f3",a),u("f3","f1",a),u("n1","f1",a),u("n2","f2",a),u("n3","f3",a),u("n4","f4",a),u("p","n1",o),u("p","n2",o),u("p","n3",o),u("p","n4",o),u("u1","u2",l),u("u2","u3",l),u("u3","u1",l),u("c","t",c),u("p","c",h),u("cn1","cn2",h),u("cn3","cn4",h),u("cf1","cf2",h),u("cf3","cf4",h),e.setAttribute("position",new mn(i,3)),e.setAttribute("color",new mn(r,3)),super(e,n),this.type="CameraHelper",this.camera=t,this.camera.updateProjectionMatrix&&this.camera.updateProjectionMatrix(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,this.pointMap=s,this.update()}update(){const t=this.geometry,e=this.pointMap;_h.projectionMatrixInverse.copy(this.camera.projectionMatrixInverse),wh("c",e,t,_h,0,0,-1),wh("t",e,t,_h,0,0,1),wh("n1",e,t,_h,-1,-1,-1),wh("n2",e,t,_h,1,-1,-1),wh("n3",e,t,_h,-1,1,-1),wh("n4",e,t,_h,1,1,-1),wh("f1",e,t,_h,-1,-1,1),wh("f2",e,t,_h,1,-1,1),wh("f3",e,t,_h,-1,1,1),wh("f4",e,t,_h,1,1,1),wh("u1",e,t,_h,.7,1.1,-1),wh("u2",e,t,_h,-.7,1.1,-1),wh("u3",e,t,_h,0,2,-1),wh("cf1",e,t,_h,-1,0,1),wh("cf2",e,t,_h,1,0,1),wh("cf3",e,t,_h,0,-1,1),wh("cf4",e,t,_h,0,1,1),wh("cn1",e,t,_h,-1,0,-1),wh("cn2",e,t,_h,1,0,-1),wh("cn3",e,t,_h,0,-1,-1),wh("cn4",e,t,_h,0,1,-1),t.getAttribute("position").needsUpdate=!0}dispose(){this.geometry.dispose(),this.material.dispose()}},t.CanvasRenderer=function(){console.error("THREE.CanvasRenderer has been removed")},t.CanvasTexture=Ra,t.CatmullRomCurve3=bl,t.CineonToneMapping=3,t.CircleBufferGeometry=Pa,t.CircleGeometry=Pa,t.ClampToEdgeWrapping=u,t.Clock=bc,t.Color=tn,t.ColorKeyframeTrack=Jo,t.CompressedTexture=La,t.CompressedTextureLoader=class extends ol{constructor(t){super(t)}load(t,e,n,i){const r=this,s=[],a=new La,o=new cl(this.manager);o.setPath(this.path),o.setResponseType("arraybuffer"),o.setRequestHeader(this.requestHeader),o.setWithCredentials(r.withCredentials);let l=0;function c(c){o.load(t[c],(function(t){const n=r.parse(t,!0);s[c]={width:n.width,height:n.height,format:n.format,mipmaps:n.mipmaps},l+=1,6===l&&(1===n.mipmapCount&&(a.minFilter=g),a.image=s,a.format=n.format,a.needsUpdate=!0,e&&e(a))}),n,i)}if(Array.isArray(t))for(let e=0,n=t.length;e>16&32768,i=e>>12&2047;const r=e>>23&255;return r<103?n:r>142?(n|=31744,n|=(255==r?0:1)&&8388607&e,n):r<113?(i|=2048,n|=(i>>114-r)+(i>>113-r&1),n):(n|=r-112<<10|i>>1,n+=1&i,n)}},t.DecrementStencilOp=7683,t.DecrementWrapStencilOp=34056,t.DefaultLoadingManager=al,t.DepthFormat=A,t.DepthStencilFormat=L,t.DepthTexture=Ca,t.DirectionalLight=Kl,t.DirectionalLightHelper=class extends Ce{constructor(t,e,n){super(),this.light=t,this.light.updateMatrixWorld(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,this.color=n,void 0===e&&(e=1);let i=new En;i.setAttribute("position",new mn([-e,e,0,e,e,0,e,-e,0,-e,-e,0,-e,e,0],3));const r=new ca({fog:!1,toneMapped:!1});this.lightPlane=new fa(i,r),this.add(this.lightPlane),i=new En,i.setAttribute("position",new mn([0,0,0,0,0,1],3)),this.targetLine=new fa(i,r),this.add(this.targetLine),this.update()}dispose(){this.lightPlane.geometry.dispose(),this.lightPlane.material.dispose(),this.targetLine.geometry.dispose(),this.targetLine.material.dispose()}update(){gh.setFromMatrixPosition(this.light.matrixWorld),vh.setFromMatrixPosition(this.light.target.matrixWorld),yh.subVectors(vh,gh),this.lightPlane.lookAt(vh),void 0!==this.color?(this.lightPlane.material.color.set(this.color),this.targetLine.material.color.set(this.color)):(this.lightPlane.material.color.copy(this.light.color),this.targetLine.material.color.copy(this.light.color)),this.targetLine.lookAt(vh),this.targetLine.scale.z=yh.length()}},t.DiscreteInterpolant=Xo,t.DodecahedronBufferGeometry=Ba,t.DodecahedronGeometry=Ba,t.DoubleSide=2,t.DstAlphaFactor=206,t.DstColorFactor=208,t.DynamicBufferAttribute=function(t,e){return console.warn("THREE.DynamicBufferAttribute has been removed. Use new THREE.BufferAttribute().setUsage( THREE.DynamicDrawUsage ) instead."),new sn(t,e).setUsage(nt)},t.DynamicCopyUsage=35050,t.DynamicDrawUsage=nt,t.DynamicReadUsage=35049,t.EdgesGeometry=Ga,t.EdgesHelper=function(t,e){return console.warn("THREE.EdgesHelper has been removed. Use THREE.EdgesGeometry instead."),new ya(new Ga(t.geometry),new ca({color:void 0!==e?e:16777215}))},t.EllipseCurve=fl,t.EqualDepth=4,t.EqualStencilFunc=514,t.EquirectangularReflectionMapping=a,t.EquirectangularRefractionMapping=o,t.Euler=fe,t.EventDispatcher=rt,t.ExtrudeBufferGeometry=go,t.ExtrudeGeometry=go,t.FaceColors=1,t.FileLoader=cl,t.FlatShading=1,t.Float16BufferAttribute=pn,t.Float32Attribute=function(t,e){return console.warn("THREE.Float32Attribute has been removed. Use new THREE.Float32BufferAttribute() instead."),new mn(t,e)},t.Float32BufferAttribute=mn,t.Float64Attribute=function(t,e){return console.warn("THREE.Float64Attribute has been removed. Use new THREE.Float64BufferAttribute() instead."),new fn(t,e)},t.Float64BufferAttribute=fn,t.FloatType=b,t.Fog=Ss,t.FogExp2=Ms,t.Font=pc,t.FontLoader=class extends ol{constructor(t){super(t)}load(t,e,n,i){const r=this,s=new cl(this.manager);s.setPath(this.path),s.setRequestHeader(this.requestHeader),s.setWithCredentials(r.withCredentials),s.load(t,(function(t){let n;try{n=JSON.parse(t)}catch(e){console.warn("THREE.FontLoader: typeface.js support is being deprecated. Use typeface.json instead."),n=JSON.parse(t.substring(65,t.length-2))}const i=r.parse(n);e&&e(i)}),n,i)}parse(t){return new pc(t)}},t.FrontSide=0,t.Frustum=ai,t.GLBufferAttribute=Qc,t.GLSL1="100",t.GLSL3=it,t.GammaEncoding=Z,t.GreaterDepth=6,t.GreaterEqualDepth=5,t.GreaterEqualStencilFunc=518,t.GreaterStencilFunc=516,t.GridHelper=fh,t.Group=gs,t.HalfFloatType=M,t.HemisphereLight=Ol,t.HemisphereLightHelper=class extends Ce{constructor(t,e,n){super(),this.light=t,this.light.updateMatrixWorld(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,this.color=n;const i=new _o(e);i.rotateY(.5*Math.PI),this.material=new en({wireframe:!0,fog:!1,toneMapped:!1}),void 0===this.color&&(this.material.vertexColors=!0);const r=i.getAttribute("position"),s=new Float32Array(3*r.count);i.setAttribute("color",new sn(s,3)),this.add(new Wn(i,this.material)),this.update()}dispose(){this.children[0].geometry.dispose(),this.children[0].material.dispose()}update(){const t=this.children[0];if(void 0!==this.color)this.material.color.set(this.color);else{const e=t.geometry.getAttribute("color");ph.copy(this.light.color),mh.copy(this.light.groundColor);for(let t=0,n=e.count;t0){const n=new sl(e);r=new hl(n),r.setCrossOrigin(this.crossOrigin);for(let e=0,n=t.length;e\n\n\t\t\tvec3 getSample( float theta, vec3 axis ) {\n\n\t\t\t\tfloat cosTheta = cos( theta );\n\t\t\t\t// Rodrigues' axis-angle rotation\n\t\t\t\tvec3 sampleDirection = vOutputDirection * cosTheta\n\t\t\t\t\t+ cross( axis, vOutputDirection ) * sin( theta )\n\t\t\t\t\t+ axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta );\n\n\t\t\t\treturn bilinearCubeUV( envMap, sampleDirection, mipInt );\n\n\t\t\t}\n\n\t\t\tvoid main() {\n\n\t\t\t\tvec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection );\n\n\t\t\t\tif ( all( equal( axis, vec3( 0.0 ) ) ) ) {\n\n\t\t\t\t\taxis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x );\n\n\t\t\t\t}\n\n\t\t\t\taxis = normalize( axis );\n\n\t\t\t\tgl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 );\n\t\t\t\tgl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis );\n\n\t\t\t\tfor ( int i = 1; i < n; i++ ) {\n\n\t\t\t\t\tif ( i >= samples ) {\n\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\t}\n\n\t\t\t\t\tfloat theta = dTheta * float( i );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis );\n\t\t\t\t\tgl_FragColor.rgb += weights[ i ] * getSample( theta, axis );\n\n\t\t\t\t}\n\n\t\t\t\tgl_FragColor = linearToOutputTexel( gl_FragColor );\n\n\t\t\t}\n\t\t`,blending:0,depthTest:!1,depthWrite:!1})}(Ih),this._equirectShader=null,this._cubemapShader=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,n=.1,i=100){kh=this._renderer.getRenderTarget();const r=this._allocateTargets();return this._sceneToCubeUV(t,n,i,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t){return this._fromTexture(t)}fromCubemap(t){return this._fromTexture(t)}compileCubemapShader(){null===this._cubemapShader&&(this._cubemapShader=Kh(),this._compileMaterial(this._cubemapShader))}compileEquirectangularShader(){null===this._equirectShader&&(this._equirectShader=Qh(),this._compileMaterial(this._equirectShader))}dispose(){this._blurMaterial.dispose(),null!==this._cubemapShader&&this._cubemapShader.dispose(),null!==this._equirectShader&&this._equirectShader.dispose();for(let t=0;t2?Ch:0,Ch,Ch),o.setRenderTarget(i),u&&o.render(zh,r),o.render(t,r)}o.toneMapping=h,o.outputEncoding=c,o.autoClear=l}_textureToCubeUV(t,e){const n=this._renderer;t.isCubeTexture?null==this._cubemapShader&&(this._cubemapShader=Kh()):null==this._equirectShader&&(this._equirectShader=Qh());const i=t.isCubeTexture?this._cubemapShader:this._equirectShader,r=new Wn(Oh[0],i),s=i.uniforms;s.envMap.value=t,t.isCubeTexture||s.texelSize.value.set(1/t.image.width,1/t.image.height),s.inputEncoding.value=Nh[t.encoding],s.outputEncoding.value=Nh[e.texture.encoding],Jh(e,0,0,3*Ch,2*Ch),n.setRenderTarget(e),n.render(r,Fh)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;for(let e=1;eIh&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to 20`);const f=[];let g=0;for(let t=0;t4?i-8+4:0),3*v,2*v),o.setRenderTarget(e),o.render(c,Fh)}},t.ParametricBufferGeometry=wo,t.ParametricGeometry=wo,t.Particle=function(t){return console.warn("THREE.Particle has been renamed to THREE.Sprite."),new Vs(t)},t.ParticleBasicMaterial=function(t){return console.warn("THREE.ParticleBasicMaterial has been renamed to THREE.PointsMaterial."),new _a(t)},t.ParticleSystem=function(t,e){return console.warn("THREE.ParticleSystem has been renamed to THREE.Points."),new Ta(t,e)},t.ParticleSystemMaterial=function(t){return console.warn("THREE.ParticleSystemMaterial has been renamed to THREE.PointsMaterial."),new _a(t)},t.Path=Bl,t.PerspectiveCamera=Kn,t.Plane=Ne,t.PlaneBufferGeometry=ci,t.PlaneGeometry=ci,t.PlaneHelper=class extends fa{constructor(t,e=1,n=16776960){const i=n,r=new En;r.setAttribute("position",new mn([1,-1,1,-1,1,1,-1,-1,1,1,1,1,-1,1,1,-1,-1,1,1,-1,1,1,1,1,0,0,1,0,0,0],3)),r.computeBoundingSphere(),super(r,new ca({color:i,toneMapped:!1})),this.type="PlaneHelper",this.plane=t,this.size=e;const s=new En;s.setAttribute("position",new mn([1,1,1,-1,1,1,-1,-1,1,1,1,1,-1,-1,1,1,-1,1],3)),s.computeBoundingSphere(),this.add(new Wn(s,new en({color:i,opacity:.2,transparent:!0,depthWrite:!1,toneMapped:!1})))}updateMatrixWorld(t){let e=-this.plane.constant;Math.abs(e)<1e-8&&(e=1e-8),this.scale.set(.5*this.size,.5*this.size,e),this.children[0].material.side=e<0?1:0,this.lookAt(this.plane.normal),super.updateMatrixWorld(t)}},t.PointCloud=function(t,e){return console.warn("THREE.PointCloud has been renamed to THREE.Points."),new Ta(t,e)},t.PointCloudMaterial=function(t){return console.warn("THREE.PointCloudMaterial has been renamed to THREE.PointsMaterial."),new _a(t)},t.PointLight=Zl,t.PointLightHelper=class extends Wn{constructor(t,e,n){super(new So(e,4,2),new en({wireframe:!0,fog:!1,toneMapped:!1})),this.light=t,this.light.updateMatrixWorld(),this.color=n,this.type="PointLightHelper",this.matrix=this.light.matrixWorld,this.matrixAutoUpdate=!1,this.update()}dispose(){this.geometry.dispose(),this.material.dispose()}update(){void 0!==this.color?this.material.color.set(this.color):this.material.color.copy(this.light.color)}},t.Points=Ta,t.PointsMaterial=_a,t.PolarGridHelper=class extends ya{constructor(t=10,e=16,n=8,i=64,r=4473924,s=8947848){r=new tn(r),s=new tn(s);const a=[],o=[];for(let n=0;n<=e;n++){const i=n/e*(2*Math.PI),l=Math.sin(i)*t,c=Math.cos(i)*t;a.push(0,0,0),a.push(l,0,c);const h=1&n?r:s;o.push(h.r,h.g,h.b),o.push(h.r,h.g,h.b)}for(let e=0;e<=n;e++){const l=1&e?r:s,c=t-t/n*e;for(let t=0;t float: + """Temperatures default to 0 when the firmware didn't report one, so the + front-end's Math.max / .toFixed never sees null/NaN.""" + return float(v) if v is not None else 0.0 + + +def build_payload(temps: list[dict[str, Any]], + positions: list[float], + timestamp: float) -> dict[str, Any]: + """Build the Marcus-compatible 'motor_update' payload. + + `temps` — arm.get_motor_temps(): [{motor_id, surface, winding}] + `positions` — arm.get_current_q(): joint angles indexed by motor id + """ + temperatures: list[dict[str, Any]] = [] + for t in temps or []: + i = t.get("motor_id") + surface = t.get("surface") + winding = t.get("winding") + if surface is not None and winding is not None: + avg = (_coerce(surface) + _coerce(winding)) / 2.0 + else: + avg = _coerce(surface if surface is not None else winding) + entry: dict[str, Any] = { + "motor_id": i, + "motor_name": MOTOR_NAMES.get(i, f"Motor {i}"), + "mesh_name": MOTOR_TO_MESH.get(i, ""), + "surface": _coerce(surface), + "winding": _coerce(winding), + "temp1": _coerce(surface), + "temp2": _coerce(winding), + "avg": avg, + } + if positions and isinstance(i, int) and i < len(positions): + entry["position"] = float(positions[i]) + temperatures.append(entry) + + pos_list: list[dict[str, Any]] = [ + {"motor_id": i, "position": float(q), "link_name": MOTOR_TO_MESH.get(i)} + for i, q in enumerate(positions or []) + ] + return {"temperatures": temperatures, "positions": pos_list, + "timestamp": timestamp} diff --git a/dashboard/websockets/motor_temps.py b/dashboard/websockets/motor_temps.py new file mode 100644 index 0000000..c70e32f --- /dev/null +++ b/dashboard/websockets/motor_temps.py @@ -0,0 +1,81 @@ +"""WebSocket endpoint streaming G1 motor temperatures to the 3D dashboard (N1). + +Polls the arm controller's throttled rt/lowstate snapshot (arm.get_motor_temps +/ arm.get_current_q — NO second DDS subscriber, no second ChannelFactoryInitialize) +and pushes a Marcus-compatible 'motor_update' payload to each connected client. + +Front-end: dashboard/static/temp3d/index.html (ported three.js view), which +opens this socket via a tiny shim in place of socket.io. +""" + +from __future__ import annotations + +import asyncio +import threading +import time + +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +from Project.Sanad.core.logger import get_logger +from Project.Sanad.dashboard.temp_motor_map import build_payload + +log = get_logger("motor_temps_ws") + +router = APIRouter() + +MAX_WATCHERS = 20 +PUSH_HZ = 8.0 # ~8 fps is plenty for a temperature heatmap + +_count = 0 +_count_lock = threading.Lock() + + +def _get_arm(): + """Lazy import — avoids a circular import on dashboard load.""" + try: + from Project.Sanad.main import arm # type: ignore + return arm + except Exception: + return None + + +@router.websocket("/ws/motor-temps") +async def motor_temps_ws(ws: WebSocket): + await ws.accept() + + global _count + with _count_lock: + if _count >= MAX_WATCHERS: + await ws.close(code=1013, reason="Too many temperature watchers") + return + _count += 1 + + period = 1.0 / PUSH_HZ + try: + while True: + arm = _get_arm() + temps: list = [] + positions: list = [] + if arm is not None: + try: + temps = arm.get_motor_temps() + except Exception: + temps = [] + try: + positions = arm.get_current_q() + except Exception: + positions = [] + payload = build_payload(temps, positions, time.time()) + await ws.send_json(payload) + await asyncio.sleep(period) + except WebSocketDisconnect: + pass + except Exception: + # Any other error (client gone mid-send, serialise issue) closes cleanly. + try: + await ws.close() + except Exception: + pass + finally: + with _count_lock: + _count -= 1 diff --git a/dashboard/websockets/terminal.py b/dashboard/websockets/terminal.py new file mode 100644 index 0000000..af27838 --- /dev/null +++ b/dashboard/websockets/terminal.py @@ -0,0 +1,323 @@ +"""WebSocket → PTY bridge for the dashboard's Terminal tab. + +Spawns a shell (bash by default) inside a pseudo-terminal on the robot and +relays stdin/stdout to a browser xterm.js instance over WebSocket. From the +operator's seat this is functionally identical to an in-browser +`ssh unitree@` — except no SSH handshake is needed because the +dashboard process already runs as unitree on the robot. The Terminal tab +connects to ws:///ws/terminal and you land in unitree's shell +directly. + +PROTOCOL — text frames only. Control vs. keystrokes are disambiguated by +the leading byte: + client → server: + "\\x1f" + json-encoded control object (init / resize) + e.g. "\\x1f{\\"type\\":\\"init\\",\\"cols\\":80,\\"rows\\":24}" + keystrokes — written to PTY + server → client: + PTY stdout/stderr chunks + +The \\x1f prefix (ASCII Unit Separator) is the disambiguator. If we just +JSON-sniffed every message, a user pasting `{"type":"resize",...}` into +their shell would silently resize the PTY instead of pasting the text. + +SECURITY NOTE: anyone who can reach the dashboard URL gets shell access +as the unitree user. The dashboard already exposes equally-powerful +endpoints (E-STOP, motion replay, audio mute, etc.) so this isn't a new +threat class — but it IS a single-bullet kill switch for the robot. Bind +the dashboard to a trusted network only. +""" + +from __future__ import annotations + +import asyncio +import fcntl +import json +import os +import pty +import select +import shutil +import signal +import struct +import termios +import threading + +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +from Project.Sanad.core.logger import get_logger + +log = get_logger("terminal_ws") + +router = APIRouter() + +# Magic prefix that distinguishes control messages from raw keystrokes. +# ASCII 0x1F (Unit Separator) — not produced by normal keyboard input, +# so user-pasted JSON can never spoof a control frame. +_CTRL_PREFIX = "\x1f" + +# Concurrent-session cap so a runaway tab can't spawn 50 bashes on the robot. +_MAX_SESSIONS = 4 +_active: set[int] = set() +_active_lock = threading.Lock() + +# Bounded queue depth between the PTY reader thread and the WS sender. +# A chatty shell command (e.g. `yes`, `cat /dev/urandom`) at gigabytes/sec +# would otherwise pile up unbounded asyncio tasks + string refs. Past the +# cap we drop chunks and surface a single drop notice — ANSI may corrupt +# briefly but the session stays alive. +_SEND_QUEUE_MAX = 64 + + +def _resolve_shell() -> list[str]: + """Pick a sensible shell. SHELL env first, then /bin/bash, then sh.""" + sh = os.environ.get("SHELL", "") + if sh and shutil.which(sh): + return [sh, "-i"] + if shutil.which("/bin/bash"): + return ["/bin/bash", "-i"] + return ["/bin/sh", "-i"] + + +def _set_pty_size(fd: int, cols: int, rows: int) -> None: + """Inform the PTY of its new window size so curses-style apps (htop, + less, vim) lay out correctly.""" + try: + # TIOCSWINSZ payload: rows, cols, xpixel, ypixel (xpixel/ypixel + # unused, kept 0). + fcntl.ioctl(fd, termios.TIOCSWINSZ, + struct.pack("HHHH", rows, cols, 0, 0)) + except Exception as exc: + log.debug("TIOCSWINSZ failed (cols=%s rows=%s): %s", cols, rows, exc) + + +async def _reap_child(pid: int) -> None: + """SIGHUP → wait briefly → SIGKILL → wait briefly → giveup. + + Earlier version SIGKILLed unconditionally because the WNOHANG check + happened immediately after SIGHUP (which never returns true that fast). + Now we poll for up to ~1.5s after SIGHUP before escalating. + """ + async def _wait_exit(timeout_s: float, interval_s: float = 0.1) -> bool: + end = asyncio.get_running_loop().time() + timeout_s + while asyncio.get_running_loop().time() < end: + try: + done_pid, _ = os.waitpid(pid, os.WNOHANG) + except ChildProcessError: + return True # already reaped + except OSError: + return False + if done_pid: + return True + await asyncio.sleep(interval_s) + return False + + # 1. Polite request + try: + os.kill(pid, signal.SIGHUP) + except ProcessLookupError: + return + except OSError as exc: + log.debug("SIGHUP pid=%d: %s", pid, exc) + return + + if await _wait_exit(1.5): + return + + # 2. Force + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + return + except OSError as exc: + log.debug("SIGKILL pid=%d: %s", pid, exc) + return + + if not await _wait_exit(1.0): + log.warning("terminal child pid=%d failed to exit after SIGKILL", pid) + + +@router.websocket("/ws/terminal") +async def terminal_ws(ws: WebSocket) -> None: + """Bridge a browser xterm.js to a shell PTY on the robot.""" + await ws.accept() + + # Concurrent-session guard. + with _active_lock: + if len(_active) >= _MAX_SESSIONS: + await ws.send_text( + f"\r\n[terminal] Refused — already have {_MAX_SESSIONS} " + f"open sessions. Close another tab and reconnect.\r\n" + ) + await ws.close(code=1008) + return + + # Fork + exec the shell. Parent gets the master fd; child becomes the + # shell with stdin/stdout/stderr wired to the slave end. + cmd = _resolve_shell() + try: + pid, fd = pty.fork() + except OSError as exc: + log.error("pty.fork failed: %s", exc) + await ws.send_text(f"\r\n[terminal] pty.fork failed: {exc}\r\n") + await ws.close(code=1011) + return + + if pid == 0: + # CHILD — set env so the shell is interactive and looks right. + os.environ.setdefault("TERM", "xterm-256color") + os.environ.setdefault("LANG", os.environ.get("LANG", "en_US.UTF-8")) + try: + os.execvp(cmd[0], cmd) + except OSError as exc: + # exec failed — printing to fd 2 reaches the parent via the + # PTY so the browser sees the error before we _exit. + os.write(2, f"[terminal] exec failed: {exc}\n".encode()) + os._exit(127) + return # unreachable in child + + # PARENT + with _active_lock: + _active.add(pid) + log.info("terminal session started pid=%d cmd=%s", pid, cmd[0]) + + loop = asyncio.get_running_loop() + closed = asyncio.Event() + # Bounded queue + dedicated sender task = backpressure. If the queue + # fills up we drop the chunk and bump _dropped so we can surface a + # short notice in the stream. + send_q: asyncio.Queue[str] = asyncio.Queue(maxsize=_SEND_QUEUE_MAX) + dropped = 0 + + def _reader_thread() -> None: + """Drain PTY master fd → queue. Runs in a daemon thread because + select.select on a pipe blocks; asyncio has no portable + equivalent for arbitrary fds on Windows (and we want one code + path).""" + nonlocal dropped + try: + while not closed.is_set(): + try: + r, _, _ = select.select([fd], [], [], 0.1) + except (OSError, ValueError): + break + if not r: + continue + try: + data = os.read(fd, 4096) + except OSError: + break + if not data: # EOF — child exited / PTY closed + break + try: + text = data.decode("utf-8", errors="replace") + except Exception: + continue + # put_nowait raises on full — we drop and count. + try: + loop.call_soon_threadsafe(_enqueue, text) + except RuntimeError: + # loop closed — bail + break + finally: + loop.call_soon_threadsafe(closed.set) + + def _enqueue(text: str) -> None: + nonlocal dropped + try: + send_q.put_nowait(text) + except asyncio.QueueFull: + dropped += 1 + + async def _sender_task() -> None: + """Drains send_q → WebSocket. Single producer, single consumer + means no extra locking needed. Backoff on send failure and let + the closed flag end the session.""" + nonlocal dropped + while not closed.is_set(): + try: + text = await asyncio.wait_for(send_q.get(), timeout=0.5) + except asyncio.TimeoutError: + continue + try: + await ws.send_text(text) + except Exception as exc: + log.info("terminal ws.send failed (likely client gone): %s", exc) + closed.set() + return + # If we dropped chunks since the last successful send, tell + # the user once so the ANSI corruption isn't mysterious. + if dropped: + d = dropped + dropped = 0 + try: + await ws.send_text( + f"\r\n\x1b[2m[term: dropped {d} chunk(s) — slow client]" + f"\x1b[0m\r\n", + ) + except Exception: + closed.set() + return + + reader = threading.Thread(target=_reader_thread, daemon=True, + name=f"terminal-rx-{pid}") + reader.start() + sender = asyncio.create_task(_sender_task()) + + # Initial sizing — xterm.js will send a {type:"init",...} control + # frame right after onopen with the actual window size. + _set_pty_size(fd, 80, 24) + + try: + while not closed.is_set(): + try: + msg = await asyncio.wait_for(ws.receive_text(), timeout=0.5) + except asyncio.TimeoutError: + continue + except WebSocketDisconnect: + break + + if not msg: + continue + + # Control frame? Must start with the magic prefix. User-typed + # / pasted text can never spoof this — \x1f isn't producible + # by normal keyboard input. + if msg[:1] == _CTRL_PREFIX: + try: + ctrl = json.loads(msg[1:]) + except (json.JSONDecodeError, ValueError): + ctrl = None + if isinstance(ctrl, dict) and ctrl.get("type") in ("init", "resize"): + cols = int(ctrl.get("cols") or 80) + rows = int(ctrl.get("rows") or 24) + _set_pty_size(fd, cols, rows) + # Either way, control frames are NEVER forwarded to PTY. + continue + + # Plain keystrokes — write to PTY master. + try: + os.write(fd, msg.encode("utf-8", errors="replace")) + except OSError as exc: + log.info("terminal pty write failed (child likely exited): %s", exc) + break + finally: + closed.set() + try: + sender.cancel() + except Exception: + pass + try: + await _reap_child(pid) + except Exception as exc: + log.debug("reap_child pid=%d: %s", pid, exc) + try: + os.close(fd) + except OSError: + pass + with _active_lock: + _active.discard(pid) + log.info("terminal session ended pid=%d", pid) + try: + await ws.close() + except Exception: + pass diff --git a/data/.recognition_state.json b/data/.recognition_state.json new file mode 100644 index 0000000..f309b66 --- /dev/null +++ b/data/.recognition_state.json @@ -0,0 +1,10 @@ +{ + "vision_enabled": false, + "face_rec_enabled": false, + "gallery_version": 0, + "zone_rec_enabled": false, + "zones_version": 0, + "nav_target_zone_id": 0, + "nav_target_place_id": 0, + "movement_enabled": false +} \ No newline at end of file diff --git a/data/audio/Etisalat.wav b/data/audio/Etisalat.wav new file mode 100644 index 0000000..4fd522e Binary files /dev/null and b/data/audio/Etisalat.wav differ diff --git a/data/audio/Etisalat_1.wav b/data/audio/Etisalat_1.wav new file mode 100644 index 0000000..4fd522e Binary files /dev/null and b/data/audio/Etisalat_1.wav differ diff --git a/data/audio/Etisalat_1_raw.wav b/data/audio/Etisalat_1_raw.wav new file mode 100644 index 0000000..4fd522e Binary files /dev/null and b/data/audio/Etisalat_1_raw.wav differ diff --git a/data/audio/Etisalat_raw.wav b/data/audio/Etisalat_raw.wav new file mode 100644 index 0000000..4fd522e Binary files /dev/null and b/data/audio/Etisalat_raw.wav differ diff --git a/data/audio/Gccc.wav b/data/audio/Gccc.wav new file mode 100644 index 0000000..4eea79d Binary files /dev/null and b/data/audio/Gccc.wav differ diff --git a/data/audio/Gccc_1.wav b/data/audio/Gccc_1.wav new file mode 100644 index 0000000..4eea79d Binary files /dev/null and b/data/audio/Gccc_1.wav differ diff --git a/data/audio/Gccc_1_raw.wav b/data/audio/Gccc_1_raw.wav new file mode 100644 index 0000000..4eea79d Binary files /dev/null and b/data/audio/Gccc_1_raw.wav differ diff --git a/data/audio/Gccc_raw.wav b/data/audio/Gccc_raw.wav new file mode 100644 index 0000000..4eea79d Binary files /dev/null and b/data/audio/Gccc_raw.wav differ diff --git a/data/audio/Gccmm.wav b/data/audio/Gccmm.wav new file mode 100644 index 0000000..56bf5ca Binary files /dev/null and b/data/audio/Gccmm.wav differ diff --git a/data/audio/Gccmm_1.wav b/data/audio/Gccmm_1.wav new file mode 100644 index 0000000..56bf5ca Binary files /dev/null and b/data/audio/Gccmm_1.wav differ diff --git a/data/audio/Gccmm_1_raw.wav b/data/audio/Gccmm_1_raw.wav new file mode 100644 index 0000000..56bf5ca Binary files /dev/null and b/data/audio/Gccmm_1_raw.wav differ diff --git a/data/audio/Gccmm_raw.wav b/data/audio/Gccmm_raw.wav new file mode 100644 index 0000000..56bf5ca Binary files /dev/null and b/data/audio/Gccmm_raw.wav differ diff --git a/data/audio/Ghy.wav b/data/audio/Ghy.wav new file mode 100644 index 0000000..95d7e38 Binary files /dev/null and b/data/audio/Ghy.wav differ diff --git a/data/audio/Ghy_1.wav b/data/audio/Ghy_1.wav new file mode 100644 index 0000000..95d7e38 Binary files /dev/null and b/data/audio/Ghy_1.wav differ diff --git a/data/audio/Ghy_1_raw.wav b/data/audio/Ghy_1_raw.wav new file mode 100644 index 0000000..95d7e38 Binary files /dev/null and b/data/audio/Ghy_1_raw.wav differ diff --git a/data/audio/Ghy_raw.wav b/data/audio/Ghy_raw.wav new file mode 100644 index 0000000..95d7e38 Binary files /dev/null and b/data/audio/Ghy_raw.wav differ diff --git a/data/audio/Ghyd.wav b/data/audio/Ghyd.wav new file mode 100644 index 0000000..ce6ffac Binary files /dev/null and b/data/audio/Ghyd.wav differ diff --git a/data/audio/Ghyd_1.wav b/data/audio/Ghyd_1.wav new file mode 100644 index 0000000..f463168 Binary files /dev/null and b/data/audio/Ghyd_1.wav differ diff --git a/data/audio/Ghyd_1_raw.wav b/data/audio/Ghyd_1_raw.wav new file mode 100644 index 0000000..f463168 Binary files /dev/null and b/data/audio/Ghyd_1_raw.wav differ diff --git a/data/audio/Ghyd_2.wav b/data/audio/Ghyd_2.wav new file mode 100644 index 0000000..f463168 Binary files /dev/null and b/data/audio/Ghyd_2.wav differ diff --git a/data/audio/Ghyd_2_raw.wav b/data/audio/Ghyd_2_raw.wav new file mode 100644 index 0000000..f463168 Binary files /dev/null and b/data/audio/Ghyd_2_raw.wav differ diff --git a/data/audio/Ghyd_raw.wav b/data/audio/Ghyd_raw.wav new file mode 100644 index 0000000..ce6ffac Binary files /dev/null and b/data/audio/Ghyd_raw.wav differ diff --git a/data/audio/Ghyddf.wav b/data/audio/Ghyddf.wav new file mode 100644 index 0000000..980bab3 Binary files /dev/null and b/data/audio/Ghyddf.wav differ diff --git a/data/audio/Ghyddf_1.wav b/data/audio/Ghyddf_1.wav new file mode 100644 index 0000000..980bab3 Binary files /dev/null and b/data/audio/Ghyddf_1.wav differ diff --git a/data/audio/Ghyddf_1_raw.wav b/data/audio/Ghyddf_1_raw.wav new file mode 100644 index 0000000..980bab3 Binary files /dev/null and b/data/audio/Ghyddf_1_raw.wav differ diff --git a/data/audio/Ghyddf_raw.wav b/data/audio/Ghyddf_raw.wav new file mode 100644 index 0000000..980bab3 Binary files /dev/null and b/data/audio/Ghyddf_raw.wav differ diff --git a/data/audio/Test_20260515_100606.wav b/data/audio/Test_20260515_100606.wav new file mode 100644 index 0000000..99f6f11 Binary files /dev/null and b/data/audio/Test_20260515_100606.wav differ diff --git a/data/audio/Test_20260515_100606_raw.wav b/data/audio/Test_20260515_100606_raw.wav new file mode 100644 index 0000000..99f6f11 Binary files /dev/null and b/data/audio/Test_20260515_100606_raw.wav differ diff --git a/data/audio/e26.wav b/data/audio/e26.wav new file mode 100644 index 0000000..72c3355 Binary files /dev/null and b/data/audio/e26.wav differ diff --git a/data/audio/e265.wav b/data/audio/e265.wav new file mode 100644 index 0000000..8008a83 Binary files /dev/null and b/data/audio/e265.wav differ diff --git a/data/audio/e265_raw.wav b/data/audio/e265_raw.wav new file mode 100644 index 0000000..8008a83 Binary files /dev/null and b/data/audio/e265_raw.wav differ diff --git a/data/audio/e267.wav b/data/audio/e267.wav new file mode 100644 index 0000000..8f02f05 Binary files /dev/null and b/data/audio/e267.wav differ diff --git a/data/audio/e267_1.wav b/data/audio/e267_1.wav new file mode 100644 index 0000000..8f02f05 Binary files /dev/null and b/data/audio/e267_1.wav differ diff --git a/data/audio/e267_1_raw.wav b/data/audio/e267_1_raw.wav new file mode 100644 index 0000000..8f02f05 Binary files /dev/null and b/data/audio/e267_1_raw.wav differ diff --git a/data/audio/e267_raw.wav b/data/audio/e267_raw.wav new file mode 100644 index 0000000..8f02f05 Binary files /dev/null and b/data/audio/e267_raw.wav differ diff --git a/data/audio/e268.wav b/data/audio/e268.wav new file mode 100644 index 0000000..a299ebf Binary files /dev/null and b/data/audio/e268.wav differ diff --git a/data/audio/e268_1.wav b/data/audio/e268_1.wav new file mode 100644 index 0000000..5f28b08 Binary files /dev/null and b/data/audio/e268_1.wav differ diff --git a/data/audio/e268_1_raw.wav b/data/audio/e268_1_raw.wav new file mode 100644 index 0000000..5f28b08 Binary files /dev/null and b/data/audio/e268_1_raw.wav differ diff --git a/data/audio/e268_2.wav b/data/audio/e268_2.wav new file mode 100644 index 0000000..5f28b08 Binary files /dev/null and b/data/audio/e268_2.wav differ diff --git a/data/audio/e268_2_raw.wav b/data/audio/e268_2_raw.wav new file mode 100644 index 0000000..5f28b08 Binary files /dev/null and b/data/audio/e268_2_raw.wav differ diff --git a/data/audio/e268_raw.wav b/data/audio/e268_raw.wav new file mode 100644 index 0000000..a299ebf Binary files /dev/null and b/data/audio/e268_raw.wav differ diff --git a/data/audio/e26_1.wav b/data/audio/e26_1.wav new file mode 100644 index 0000000..6ce9124 Binary files /dev/null and b/data/audio/e26_1.wav differ diff --git a/data/audio/e26_1_raw.wav b/data/audio/e26_1_raw.wav new file mode 100644 index 0000000..6ce9124 Binary files /dev/null and b/data/audio/e26_1_raw.wav differ diff --git a/data/audio/e26_2.wav b/data/audio/e26_2.wav new file mode 100644 index 0000000..6ce9124 Binary files /dev/null and b/data/audio/e26_2.wav differ diff --git a/data/audio/e26_2_raw.wav b/data/audio/e26_2_raw.wav new file mode 100644 index 0000000..6ce9124 Binary files /dev/null and b/data/audio/e26_2_raw.wav differ diff --git a/data/audio/e26_3.wav b/data/audio/e26_3.wav new file mode 100644 index 0000000..8a7233a Binary files /dev/null and b/data/audio/e26_3.wav differ diff --git a/data/audio/e26_3_raw.wav b/data/audio/e26_3_raw.wav new file mode 100644 index 0000000..8a7233a Binary files /dev/null and b/data/audio/e26_3_raw.wav differ diff --git a/data/audio/e26_4.wav b/data/audio/e26_4.wav new file mode 100644 index 0000000..8008a83 Binary files /dev/null and b/data/audio/e26_4.wav differ diff --git a/data/audio/e26_4_raw.wav b/data/audio/e26_4_raw.wav new file mode 100644 index 0000000..8008a83 Binary files /dev/null and b/data/audio/e26_4_raw.wav differ diff --git a/data/audio/e26_raw.wav b/data/audio/e26_raw.wav new file mode 100644 index 0000000..72c3355 Binary files /dev/null and b/data/audio/e26_raw.wav differ diff --git a/data/audio/gh.wav b/data/audio/gh.wav new file mode 100644 index 0000000..2d6990d Binary files /dev/null and b/data/audio/gh.wav differ diff --git a/data/audio/gh_1.wav b/data/audio/gh_1.wav new file mode 100644 index 0000000..2d6990d Binary files /dev/null and b/data/audio/gh_1.wav differ diff --git a/data/audio/gh_1_raw.wav b/data/audio/gh_1_raw.wav new file mode 100644 index 0000000..2d6990d Binary files /dev/null and b/data/audio/gh_1_raw.wav differ diff --git a/data/audio/gh_raw.wav b/data/audio/gh_raw.wav new file mode 100644 index 0000000..2d6990d Binary files /dev/null and b/data/audio/gh_raw.wav differ diff --git a/data/audio/ghghtttt.wav b/data/audio/ghghtttt.wav new file mode 100644 index 0000000..4e50d0e Binary files /dev/null and b/data/audio/ghghtttt.wav differ diff --git a/data/audio/ghghtttt_1.wav b/data/audio/ghghtttt_1.wav new file mode 100644 index 0000000..4e50d0e Binary files /dev/null and b/data/audio/ghghtttt_1.wav differ diff --git a/data/audio/ghghtttt_1_raw.wav b/data/audio/ghghtttt_1_raw.wav new file mode 100644 index 0000000..4e50d0e Binary files /dev/null and b/data/audio/ghghtttt_1_raw.wav differ diff --git a/data/audio/ghghtttt_raw.wav b/data/audio/ghghtttt_raw.wav new file mode 100644 index 0000000..4e50d0e Binary files /dev/null and b/data/audio/ghghtttt_raw.wav differ diff --git a/data/audio/h1l.wav b/data/audio/h1l.wav new file mode 100644 index 0000000..05d7c3b Binary files /dev/null and b/data/audio/h1l.wav differ diff --git a/data/audio/h1l_1.wav b/data/audio/h1l_1.wav new file mode 100644 index 0000000..05d7c3b Binary files /dev/null and b/data/audio/h1l_1.wav differ diff --git a/data/audio/h1l_1_raw.wav b/data/audio/h1l_1_raw.wav new file mode 100644 index 0000000..05d7c3b Binary files /dev/null and b/data/audio/h1l_1_raw.wav differ diff --git a/data/audio/h1l_raw.wav b/data/audio/h1l_raw.wav new file mode 100644 index 0000000..05d7c3b Binary files /dev/null and b/data/audio/h1l_raw.wav differ diff --git a/data/audio/hh.wav b/data/audio/hh.wav new file mode 100644 index 0000000..adce27a Binary files /dev/null and b/data/audio/hh.wav differ diff --git a/data/audio/hh_1.wav b/data/audio/hh_1.wav new file mode 100644 index 0000000..adce27a Binary files /dev/null and b/data/audio/hh_1.wav differ diff --git a/data/audio/hh_1_raw.wav b/data/audio/hh_1_raw.wav new file mode 100644 index 0000000..adce27a Binary files /dev/null and b/data/audio/hh_1_raw.wav differ diff --git a/data/audio/hh_raw.wav b/data/audio/hh_raw.wav new file mode 100644 index 0000000..adce27a Binary files /dev/null and b/data/audio/hh_raw.wav differ diff --git a/data/audio/hhhh222.wav b/data/audio/hhhh222.wav new file mode 100644 index 0000000..fbd31f9 Binary files /dev/null and b/data/audio/hhhh222.wav differ diff --git a/data/audio/hhhh222_raw.wav b/data/audio/hhhh222_raw.wav new file mode 100644 index 0000000..fbd31f9 Binary files /dev/null and b/data/audio/hhhh222_raw.wav differ diff --git a/data/audio/najjar.wav b/data/audio/najjar.wav new file mode 100644 index 0000000..217345a Binary files /dev/null and b/data/audio/najjar.wav differ diff --git a/data/audio/najjar_1.wav b/data/audio/najjar_1.wav new file mode 100644 index 0000000..217345a Binary files /dev/null and b/data/audio/najjar_1.wav differ diff --git a/data/audio/najjar_1_raw.wav b/data/audio/najjar_1_raw.wav new file mode 100644 index 0000000..217345a Binary files /dev/null and b/data/audio/najjar_1_raw.wav differ diff --git a/data/audio/najjar_raw.wav b/data/audio/najjar_raw.wav new file mode 100644 index 0000000..217345a Binary files /dev/null and b/data/audio/najjar_raw.wav differ diff --git a/data/audio/records.json b/data/audio/records.json index 9d4031d..01848ad 100644 --- a/data/audio/records.json +++ b/data/audio/records.json @@ -1,15 +1,15 @@ { - "total_records": 4, + "total_records": 36, "last_updated": "2026-04-19 20:55:59", "records": [ { - "record_name": "testing_14", - "text": "يا هلا مسهلا معاك بوسينده،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.", + "record_name": "t1", + "text": "Hello, and welcome to e& at Yas Mall.\n\nI am your interactive AI assistant, here to welcome guests, guide customers through the store, and support the e& team by creating a smarter, more personalized customer experience.\n", "replay_count": 1, "timeline": { - "audio_generated_at": "2026-05-12 10:40:29", - "last_playback_finished_at": "2026-05-12 10:40:40", - "saved_at": "2026-05-12 10:40:40" + "audio_generated_at": "2026-05-15 10:25:07", + "last_playback_finished_at": "2026-05-15 10:25:22", + "saved_at": "2026-05-15 10:25:22" }, "audio_capture": { "backend": "parec", @@ -19,33 +19,33 @@ }, "files": { "speaker_recording": { - "name": "testing_14.wav", - "path": "testing_14.wav", - "size_bytes": 468480, + "name": "t1.wav", + "path": "t1.wav", + "size_bytes": 695040, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 9.76 + "duration_seconds": 14.48 }, "gemini_raw_output": { - "name": "testing_14_raw.wav", - "path": "testing_14_raw.wav", - "size_bytes": 468480, + "name": "t1_raw.wav", + "path": "t1_raw.wav", + "size_bytes": 695040, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 9.76 + "duration_seconds": 14.48 } } }, { - "record_name": "testing_15", - "text": "يا هلا مسهلا معاك بوسينده،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.", + "record_name": "t1_1", + "text": "Hello, and welcome to e& at Yas Mall.\n\nI am your interactive AI assistant, here to welcome guests, guide customers through the store, and support the e& team by creating a smarter, more personalized customer experience.\n", "replay_count": 1, "timeline": { - "audio_generated_at": "2026-05-12 10:41:16", - "last_playback_finished_at": "2026-05-12 10:41:26", - "saved_at": "2026-05-12 10:41:26" + "audio_generated_at": "2026-05-15 10:25:07", + "last_playback_finished_at": "2026-05-15 10:25:22", + "saved_at": "2026-05-15 10:25:26" }, "audio_capture": { "backend": "parec", @@ -55,33 +55,33 @@ }, "files": { "speaker_recording": { - "name": "testing_15.wav", - "path": "testing_15.wav", - "size_bytes": 472320, + "name": "t1_1.wav", + "path": "t1_1.wav", + "size_bytes": 695040, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 9.84 + "duration_seconds": 14.48 }, "gemini_raw_output": { - "name": "testing_15_raw.wav", - "path": "testing_15_raw.wav", - "size_bytes": 472320, + "name": "t1_1_raw.wav", + "path": "t1_1_raw.wav", + "size_bytes": 695040, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 9.84 + "duration_seconds": 14.48 } } }, { - "record_name": "testing_16", - "text": "يا هلا مسهلا معاك بُوسْنَيْدَة،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.", + "record_name": "t2", + "text": "I can see Hazem, General Manager at YS Lootah Tech.\n\nمرحباً أستاذ حازم، أهلاً وسهلاً بك في e& في ياس مول. سعيد بوجودك معنا اليوم.", "replay_count": 1, "timeline": { - "audio_generated_at": "2026-05-12 10:42:31", - "last_playback_finished_at": "2026-05-12 10:42:40", - "saved_at": "2026-05-12 10:42:40" + "audio_generated_at": "2026-05-15 10:26:12", + "last_playback_finished_at": "2026-05-15 10:26:22", + "saved_at": "2026-05-15 10:26:22" }, "audio_capture": { "backend": "parec", @@ -91,33 +91,33 @@ }, "files": { "speaker_recording": { - "name": "testing_16.wav", - "path": "testing_16.wav", - "size_bytes": 426240, + "name": "t2.wav", + "path": "t2.wav", + "size_bytes": 485760, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 8.88 + "duration_seconds": 10.12 }, "gemini_raw_output": { - "name": "testing_16_raw.wav", - "path": "testing_16_raw.wav", - "size_bytes": 426240, + "name": "t2_raw.wav", + "path": "t2_raw.wav", + "size_bytes": 485760, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 8.88 + "duration_seconds": 10.12 } } }, { - "record_name": "testing_17", - "text": "يا هلا مسهلا معاك بُوسْنَيْدَة،\nحاب تعمل معي فيديو مميز؟\nتواصل وياي وانا فريقي جاهزين خلنا نبدع معا ونعمل ترند.", + "record_name": "t2_1", + "text": "I can see Hazem, General Manager at YS Lootah Tech.\n\nمرحباً أستاذ حازم، أهلاً وسهلاً بك في e& في ياس مول. سعيد بوجودك معنا اليوم.", "replay_count": 1, "timeline": { - "audio_generated_at": "2026-05-12 10:42:31", - "last_playback_finished_at": "2026-05-12 10:42:40", - "saved_at": "2026-05-12 10:42:42" + "audio_generated_at": "2026-05-15 10:26:12", + "last_playback_finished_at": "2026-05-15 10:26:22", + "saved_at": "2026-05-15 10:26:25" }, "audio_capture": { "backend": "parec", @@ -127,22 +127,1174 @@ }, "files": { "speaker_recording": { - "name": "testing_17.wav", - "path": "testing_17.wav", - "size_bytes": 426240, + "name": "t2_1.wav", + "path": "t2_1.wav", + "size_bytes": 485760, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 8.88 + "duration_seconds": 10.12 }, "gemini_raw_output": { - "name": "testing_17_raw.wav", - "path": "testing_17_raw.wav", - "size_bytes": 426240, + "name": "t2_1_raw.wav", + "path": "t2_1_raw.wav", + "size_bytes": 485760, "sample_rate": 24000, "channels": 1, "sample_width_bytes": 2, - "duration_seconds": 8.88 + "duration_seconds": 10.12 + } + } + }, + { + "record_name": "t3", + "text": "I can see Liam, Sales Director from Wisemate. Hello Liam, it is a pleasure to welcome you today.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:26:41", + "last_playback_finished_at": "2026-05-15 10:26:47", + "saved_at": "2026-05-15 10:26:47" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t3.wav", + "path": "t3.wav", + "size_bytes": 274560, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.72 + }, + "gemini_raw_output": { + "name": "t3_raw.wav", + "path": "t3_raw.wav", + "size_bytes": 274560, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.72 + } + } + }, + { + "record_name": "t4", + "text": "I can see TT, Sales Director from Wisemate. Hello TT, welcome to e& at Yas Mall, it is great to have you here.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:26:53", + "last_playback_finished_at": "2026-05-15 10:27:04", + "saved_at": "2026-05-15 10:27:04" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t4.wav", + "path": "t4.wav", + "size_bytes": 276480, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.76 + }, + "gemini_raw_output": { + "name": "t4_raw.wav", + "path": "t4_raw.wav", + "size_bytes": 276480, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.76 + } + } + }, + { + "record_name": "t4_1", + "text": "I can see TT, Sales Director from Wisemate. Hello TT, welcome to e& at Yas Mall, it is great to have you here.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:26:53", + "last_playback_finished_at": "2026-05-15 10:27:04", + "saved_at": "2026-05-15 10:27:06" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t4_1.wav", + "path": "t4_1.wav", + "size_bytes": 276480, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.76 + }, + "gemini_raw_output": { + "name": "t4_1_raw.wav", + "path": "t4_1_raw.wav", + "size_bytes": 276480, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.76 + } + } + }, + { + "record_name": "t5", + "text": "TT, allow me to focus on your experience for a moment.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:27:39", + "last_playback_finished_at": "2026-05-15 10:27:43", + "saved_at": "2026-05-15 10:27:43" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t5.wav", + "path": "t5.wav", + "size_bytes": 190080, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.96 + }, + "gemini_raw_output": { + "name": "t5_raw.wav", + "path": "t5_raw.wav", + "size_bytes": 190080, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.96 + } + } + }, + { + "record_name": "t5_1", + "text": "TT, allow me to focus on your experience for a moment.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:27:39", + "last_playback_finished_at": "2026-05-15 10:27:43", + "saved_at": "2026-05-15 10:27:50" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t5_1.wav", + "path": "t5_1.wav", + "size_bytes": 190080, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.96 + }, + "gemini_raw_output": { + "name": "t5_1_raw.wav", + "path": "t5_1_raw.wav", + "size_bytes": 190080, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.96 + } + } + }, + { + "record_name": "t6_1", + "text": "I can see that your recent NEO Broadband issue at your villa has been resolved, and I hope your connection is now stable.\n\nBased on your profile and previous purchases, I noticed that you currently have a Samsung Dual Fold. Since you are one of our elite customers, I believe you may be interested in the latest Samsung Triple Fold.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:28:10", + "last_playback_finished_at": "2026-05-15 10:28:32", + "saved_at": "2026-05-15 10:28:32" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t6_1.wav", + "path": "t6_1.wav", + "size_bytes": 1015680, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 21.16 + }, + "gemini_raw_output": { + "name": "t6_1_raw.wav", + "path": "t6_1_raw.wav", + "size_bytes": 1015680, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 21.16 + } + } + }, + { + "record_name": "t6_2", + "text": "I can see that your recent NEO Broadband issue at your villa has been resolved, and I hope your connection is now stable.\n\nBased on your profile and previous purchases, I noticed that you currently have a Samsung Dual Fold. Since you are one of our elite customers, I believe you may be interested in the latest Samsung Triple Fold.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:28:10", + "last_playback_finished_at": "2026-05-15 10:28:32", + "saved_at": "2026-05-15 10:28:45" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t6_2.wav", + "path": "t6_2.wav", + "size_bytes": 1015680, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 21.16 + }, + "gemini_raw_output": { + "name": "t6_2_raw.wav", + "path": "t6_2_raw.wav", + "size_bytes": 1015680, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 21.16 + } + } + }, + { + "record_name": "t7", + "text": "Please follow me to the display area.\n\nWith e& SmartPay, you can take the device on easy monthly installments over 6, 12, 18, 24, or 36 months. I will also check your eligibility and whether your processing fees can be waived.\n", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:29:26", + "last_playback_finished_at": "2026-05-15 10:29:47", + "saved_at": "2026-05-15 10:29:47" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t7.wav", + "path": "t7.wav", + "size_bytes": 768000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 16.0 + }, + "gemini_raw_output": { + "name": "t7_raw.wav", + "path": "t7_raw.wav", + "size_bytes": 768000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 16.0 + } + } + }, + { + "record_name": "t7_1", + "text": "Please follow me to the display area.\n\nWith e& SmartPay, you can take the device on easy monthly installments over 6, 12, 18, 24, or 36 months. I will also check your eligibility and whether your processing fees can be waived.\n", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:29:26", + "last_playback_finished_at": "2026-05-15 10:29:47", + "saved_at": "2026-05-15 10:29:58" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t7_1.wav", + "path": "t7_1.wav", + "size_bytes": 768000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 16.0 + }, + "gemini_raw_output": { + "name": "t7_1_raw.wav", + "path": "t7_1_raw.wav", + "size_bytes": 768000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 16.0 + } + } + }, + { + "record_name": "t8", + "text": "Good news, TT. You may be eligible for a 36-month SmartPay plan with zero processing fees.\n\nAlso, through our Trade-In program, your Samsung Dual Fold can be used to get a discount on the new device.\n\nSince you already use e& services at your villa, we can also help you upgrade your home experience with e& SmartHome solutions, including smart lighting, climate control, security, and entertainment.\n\nOne of our e& advisors will now assist you with the final offer and activation.\n\nThank you for visiting e&. Human warmth meets embodied AI.”", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:30:40", + "last_playback_finished_at": "2026-05-15 10:31:21", + "saved_at": "2026-05-15 10:31:21" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t8.wav", + "path": "t8.wav", + "size_bytes": 1733760, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 36.12 + }, + "gemini_raw_output": { + "name": "t8_raw.wav", + "path": "t8_raw.wav", + "size_bytes": 1733760, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 36.12 + } + } + }, + { + "record_name": "t8_1", + "text": "Good news, TT. You may be eligible for a 36-month SmartPay plan with zero processing fees.\n\nAlso, through our Trade-In program, your Samsung Dual Fold can be used to get a discount on the new device.\n\nSince you already use e& services at your villa, we can also help you upgrade your home experience with e& SmartHome solutions, including smart lighting, climate control, security, and entertainment.\n\nOne of our e& advisors will now assist you with the final offer and activation.\n\nThank you for visiting e&. Human warmth meets embodied AI.”", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:30:40", + "last_playback_finished_at": "2026-05-15 10:31:21", + "saved_at": "2026-05-15 10:31:25" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t8_1.wav", + "path": "t8_1.wav", + "size_bytes": 1733760, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 36.12 + }, + "gemini_raw_output": { + "name": "t8_1_raw.wav", + "path": "t8_1_raw.wav", + "size_bytes": 1733760, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 36.12 + } + } + }, + { + "record_name": "t9", + "text": "I can see TT, Sales Director from Wisemate. Hello TT, welcome to e& at Yas Mall, it is great to have you here.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:36:15", + "last_playback_finished_at": "2026-05-15 10:36:28", + "saved_at": "2026-05-15 10:36:28" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t9.wav", + "path": "t9.wav", + "size_bytes": 376320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 7.84 + }, + "gemini_raw_output": { + "name": "t9_raw.wav", + "path": "t9_raw.wav", + "size_bytes": 376320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 7.84 + } + } + }, + { + "record_name": "t9_1", + "text": "I can see TT, Sales Director from Wisemate. Hello TT, welcome to e& at Yas Mall, it is great to have you here.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-15 10:36:15", + "last_playback_finished_at": "2026-05-15 10:36:28", + "saved_at": "2026-05-15 10:36:30" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "t9_1.wav", + "path": "t9_1.wav", + "size_bytes": 376320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 7.84 + }, + "gemini_raw_output": { + "name": "t9_1_raw.wav", + "path": "t9_1_raw.wav", + "size_bytes": 376320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 7.84 + } + } + }, + { + "record_name": "h1l", + "text": "والله محتار بصراحة اخذ الورق العنب او الفلفل اصفر محشي لبنة", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 11:51:31", + "last_playback_finished_at": "2026-05-19 11:51:38", + "saved_at": "2026-05-19 11:51:38" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "h1l.wav", + "path": "h1l.wav", + "size_bytes": 257280, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.36 + }, + "gemini_raw_output": { + "name": "h1l_raw.wav", + "path": "h1l_raw.wav", + "size_bytes": 257280, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.36 + } + } + }, + { + "record_name": "h1l_1", + "text": "والله محتار بصراحة اخذ الورق العنب او الفلفل اصفر محشي لبنة", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 11:51:31", + "last_playback_finished_at": "2026-05-19 11:51:38", + "saved_at": "2026-05-19 11:51:44" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "h1l_1.wav", + "path": "h1l_1.wav", + "size_bytes": 257280, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.36 + }, + "gemini_raw_output": { + "name": "h1l_1_raw.wav", + "path": "h1l_1_raw.wav", + "size_bytes": 257280, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 5.36 + } + } + }, + { + "record_name": "Ghy", + "text": "شو يسوون هني", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 12:20:16", + "last_playback_finished_at": "2026-05-19 12:20:17", + "saved_at": "2026-05-19 12:20:17" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Ghy.wav", + "path": "Ghy.wav", + "size_bytes": 49920, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.04 + }, + "gemini_raw_output": { + "name": "Ghy_raw.wav", + "path": "Ghy_raw.wav", + "size_bytes": 49920, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.04 + } + } + }, + { + "record_name": "Ghy_1", + "text": "شو يسوون هني", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 12:20:16", + "last_playback_finished_at": "2026-05-19 12:20:17", + "saved_at": "2026-05-19 12:20:19" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Ghy_1.wav", + "path": "Ghy_1.wav", + "size_bytes": 49920, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.04 + }, + "gemini_raw_output": { + "name": "Ghy_1_raw.wav", + "path": "Ghy_1_raw.wav", + "size_bytes": 49920, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.04 + } + } + }, + { + "record_name": "Ghyd_1", + "text": "و هني الزين", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 12:20:58", + "last_playback_finished_at": "2026-05-19 12:21:04", + "saved_at": "2026-05-19 12:21:04" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Ghyd_1.wav", + "path": "Ghyd_1.wav", + "size_bytes": 48000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.0 + }, + "gemini_raw_output": { + "name": "Ghyd_1_raw.wav", + "path": "Ghyd_1_raw.wav", + "size_bytes": 48000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.0 + } + } + }, + { + "record_name": "Ghyd_2", + "text": "و هني الزين", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 12:20:58", + "last_playback_finished_at": "2026-05-19 12:21:04", + "saved_at": "2026-05-19 12:21:05" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Ghyd_2.wav", + "path": "Ghyd_2.wav", + "size_bytes": 48000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.0 + }, + "gemini_raw_output": { + "name": "Ghyd_2_raw.wav", + "path": "Ghyd_2_raw.wav", + "size_bytes": 48000, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.0 + } + } + }, + { + "record_name": "Ghyddf", + "text": "هي الماكينة حق شو", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 12:21:49", + "last_playback_finished_at": "2026-05-19 12:21:50", + "saved_at": "2026-05-19 12:21:50" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Ghyddf.wav", + "path": "Ghyddf.wav", + "size_bytes": 61440, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.28 + }, + "gemini_raw_output": { + "name": "Ghyddf_raw.wav", + "path": "Ghyddf_raw.wav", + "size_bytes": 61440, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.28 + } + } + }, + { + "record_name": "Ghyddf_1", + "text": "هي الماكينة حق شو", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 12:21:49", + "last_playback_finished_at": "2026-05-19 12:21:50", + "saved_at": "2026-05-19 12:21:53" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Ghyddf_1.wav", + "path": "Ghyddf_1.wav", + "size_bytes": 61440, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.28 + }, + "gemini_raw_output": { + "name": "Ghyddf_1_raw.wav", + "path": "Ghyddf_1_raw.wav", + "size_bytes": 61440, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 1.28 + } + } + }, + { + "record_name": "Gccc", + "text": "Lo chef è andato in pausa pranzo e tornerà tra un’ora.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 13:02:54", + "last_playback_finished_at": "2026-05-19 13:02:59", + "saved_at": "2026-05-19 13:02:59" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Gccc.wav", + "path": "Gccc.wav", + "size_bytes": 184320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.84 + }, + "gemini_raw_output": { + "name": "Gccc_raw.wav", + "path": "Gccc_raw.wav", + "size_bytes": 184320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.84 + } + } + }, + { + "record_name": "Gccc_1", + "text": "Lo chef è andato in pausa pranzo e tornerà tra un’ora.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 13:02:54", + "last_playback_finished_at": "2026-05-19 13:02:59", + "saved_at": "2026-05-19 13:03:02" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Gccc_1.wav", + "path": "Gccc_1.wav", + "size_bytes": 184320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.84 + }, + "gemini_raw_output": { + "name": "Gccc_1_raw.wav", + "path": "Gccc_1_raw.wav", + "size_bytes": 184320, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 3.84 + } + } + }, + { + "record_name": "Gccmm", + "text": "Perché chiedete tutti dello chef italiano? Io sono meglio di lui! Se vuoi lo chef italiano, è andato in pausa… lo trovi lì. Gli date uno stipendio di 50 mila, mentre a me basta che mi carichiate la batteria.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 13:09:23", + "last_playback_finished_at": "2026-05-19 13:09:36", + "saved_at": "2026-05-19 13:09:36" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Gccmm.wav", + "path": "Gccmm.wav", + "size_bytes": 585600, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 12.2 + }, + "gemini_raw_output": { + "name": "Gccmm_raw.wav", + "path": "Gccmm_raw.wav", + "size_bytes": 585600, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 12.2 + } + } + }, + { + "record_name": "Gccmm_1", + "text": "Perché chiedete tutti dello chef italiano? Io sono meglio di lui! Se vuoi lo chef italiano, è andato in pausa… lo trovi lì. Gli date uno stipendio di 50 mila, mentre a me basta che mi carichiate la batteria.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-19 13:09:23", + "last_playback_finished_at": "2026-05-19 13:09:36", + "saved_at": "2026-05-19 13:09:38" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "Gccmm_1.wav", + "path": "Gccmm_1.wav", + "size_bytes": 585600, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 12.2 + }, + "gemini_raw_output": { + "name": "Gccmm_1_raw.wav", + "path": "Gccmm_1_raw.wav", + "size_bytes": 585600, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 12.2 + } + } + }, + { + "record_name": "najjar", + "text": "Happy birthday to you,\nHappy birthday to you,\nHappy birthday, dear Najjar,\nHappy birthday to you!", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-20 11:40:46", + "last_playback_finished_at": "2026-05-20 11:40:56", + "saved_at": "2026-05-20 11:40:56" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "najjar.wav", + "path": "najjar.wav", + "size_bytes": 455040, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 9.48 + }, + "gemini_raw_output": { + "name": "najjar_raw.wav", + "path": "najjar_raw.wav", + "size_bytes": 455040, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 9.48 + } + } + }, + { + "record_name": "e26_4", + "text": "كانت تجربة جميلة جدًا، خصوصًا مع تفاعل الأطفال والشباب والحضور. وطبعًا الهدف من وجودي كان توعوي وتعليمي للتعريف بعالم الروبوتات والذكاء الاصطناعي وقدرات التكنولوجيا الحديثة بطريقة قريبة من المجتمع… وبصراحة أول مرة أدخل مسجد وما حد يطلب مني واي فاي!", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-28 16:50:26", + "last_playback_finished_at": "2026-05-28 16:50:47", + "saved_at": "2026-05-28 16:50:47" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "e26_4.wav", + "path": "e26_4.wav", + "size_bytes": 963840, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 20.08 + }, + "gemini_raw_output": { + "name": "e26_4_raw.wav", + "path": "e26_4_raw.wav", + "size_bytes": 963840, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 20.08 + } + } + }, + { + "record_name": "e265", + "text": "كانت تجربة جميلة جدًا، خصوصًا مع تفاعل الأطفال والشباب والحضور. وطبعًا الهدف من وجودي كان توعوي وتعليمي للتعريف بعالم الروبوتات والذكاء الاصطناعي وقدرات التكنولوجيا الحديثة بطريقة قريبة من المجتمع… وبصراحة أول مرة أدخل مسجد وما حد يطلب مني واي فاي!", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-28 16:50:26", + "last_playback_finished_at": "2026-05-28 16:50:47", + "saved_at": "2026-05-28 16:50:49" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "e265.wav", + "path": "e265.wav", + "size_bytes": 963840, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 20.08 + }, + "gemini_raw_output": { + "name": "e265_raw.wav", + "path": "e265_raw.wav", + "size_bytes": 963840, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 20.08 + } + } + }, + { + "record_name": "e267", + "text": "كانت ردة الفعل جميلة جدًا، في ناس استغربوا، وفي ناس ضحكوا، والأطفال كانوا يبغون يتصورون وياي أكثر من العيدية! والأهم إن وايد ناس صار عندهم فضول يعرفون أكثر عن الروبوتات والتكنولوجيا.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-28 16:52:19", + "last_playback_finished_at": "2026-05-28 16:52:38", + "saved_at": "2026-05-28 16:52:38" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "e267.wav", + "path": "e267.wav", + "size_bytes": 650880, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 13.56 + }, + "gemini_raw_output": { + "name": "e267_raw.wav", + "path": "e267_raw.wav", + "size_bytes": 650880, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 13.56 + } + } + }, + { + "record_name": "e267_1", + "text": "كانت ردة الفعل جميلة جدًا، في ناس استغربوا، وفي ناس ضحكوا، والأطفال كانوا يبغون يتصورون وياي أكثر من العيدية! والأهم إن وايد ناس صار عندهم فضول يعرفون أكثر عن الروبوتات والتكنولوجيا.", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-28 16:52:19", + "last_playback_finished_at": "2026-05-28 16:52:38", + "saved_at": "2026-05-28 16:52:42" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "e267_1.wav", + "path": "e267_1.wav", + "size_bytes": 650880, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 13.56 + }, + "gemini_raw_output": { + "name": "e267_1_raw.wav", + "path": "e267_1_raw.wav", + "size_bytes": 650880, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 13.56 + } + } + }, + { + "record_name": "e268_1", + "text": "أحب أشارك الناس أجواء العيد وأتعرف على الأطفال والشباب وأشجعهم على التعلم والابتكار… وبعد لازم أرجع أشحن البطارية عقب العيد، حتى الروبوتات تتعب", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-28 16:53:28", + "last_playback_finished_at": "2026-05-28 16:53:43", + "saved_at": "2026-05-28 16:53:44" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "e268_1.wav", + "path": "e268_1.wav", + "size_bytes": 499200, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 10.4 + }, + "gemini_raw_output": { + "name": "e268_1_raw.wav", + "path": "e268_1_raw.wav", + "size_bytes": 499200, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 10.4 + } + } + }, + { + "record_name": "e268_2", + "text": "أحب أشارك الناس أجواء العيد وأتعرف على الأطفال والشباب وأشجعهم على التعلم والابتكار… وبعد لازم أرجع أشحن البطارية عقب العيد، حتى الروبوتات تتعب", + "replay_count": 1, + "timeline": { + "audio_generated_at": "2026-05-28 16:53:28", + "last_playback_finished_at": "2026-05-28 16:53:43", + "saved_at": "2026-05-28 16:53:47" + }, + "audio_capture": { + "backend": "parec", + "sink": "alsa_output.platform-sound.analog-stereo", + "monitor_source": "alsa_output.platform-sound.analog-stereo.monitor", + "restored_microphone_source": "alsa_input.platform-sound.analog-stereo" + }, + "files": { + "speaker_recording": { + "name": "e268_2.wav", + "path": "e268_2.wav", + "size_bytes": 499200, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 10.4 + }, + "gemini_raw_output": { + "name": "e268_2_raw.wav", + "path": "e268_2_raw.wav", + "size_bytes": 499200, + "sample_rate": 24000, + "channels": 1, + "sample_width_bytes": 2, + "duration_seconds": 10.4 } } } diff --git a/data/audio/t1.wav b/data/audio/t1.wav new file mode 100644 index 0000000..9a02a65 Binary files /dev/null and b/data/audio/t1.wav differ diff --git a/data/audio/t1_1.wav b/data/audio/t1_1.wav new file mode 100644 index 0000000..9a02a65 Binary files /dev/null and b/data/audio/t1_1.wav differ diff --git a/data/audio/t1_1_raw.wav b/data/audio/t1_1_raw.wav new file mode 100644 index 0000000..9a02a65 Binary files /dev/null and b/data/audio/t1_1_raw.wav differ diff --git a/data/audio/t1_raw.wav b/data/audio/t1_raw.wav new file mode 100644 index 0000000..9a02a65 Binary files /dev/null and b/data/audio/t1_raw.wav differ diff --git a/data/audio/t2.wav b/data/audio/t2.wav new file mode 100644 index 0000000..45f5ee2 Binary files /dev/null and b/data/audio/t2.wav differ diff --git a/data/audio/t2_1.wav b/data/audio/t2_1.wav new file mode 100644 index 0000000..45f5ee2 Binary files /dev/null and b/data/audio/t2_1.wav differ diff --git a/data/audio/t2_1_raw.wav b/data/audio/t2_1_raw.wav new file mode 100644 index 0000000..45f5ee2 Binary files /dev/null and b/data/audio/t2_1_raw.wav differ diff --git a/data/audio/t2_raw.wav b/data/audio/t2_raw.wav new file mode 100644 index 0000000..45f5ee2 Binary files /dev/null and b/data/audio/t2_raw.wav differ diff --git a/data/audio/t3.wav b/data/audio/t3.wav new file mode 100644 index 0000000..07b6936 Binary files /dev/null and b/data/audio/t3.wav differ diff --git a/data/audio/t3_raw.wav b/data/audio/t3_raw.wav new file mode 100644 index 0000000..07b6936 Binary files /dev/null and b/data/audio/t3_raw.wav differ diff --git a/data/audio/t4.wav b/data/audio/t4.wav new file mode 100644 index 0000000..77138a0 Binary files /dev/null and b/data/audio/t4.wav differ diff --git a/data/audio/t4_1.wav b/data/audio/t4_1.wav new file mode 100644 index 0000000..77138a0 Binary files /dev/null and b/data/audio/t4_1.wav differ diff --git a/data/audio/t4_1_raw.wav b/data/audio/t4_1_raw.wav new file mode 100644 index 0000000..77138a0 Binary files /dev/null and b/data/audio/t4_1_raw.wav differ diff --git a/data/audio/t4_raw.wav b/data/audio/t4_raw.wav new file mode 100644 index 0000000..77138a0 Binary files /dev/null and b/data/audio/t4_raw.wav differ diff --git a/data/audio/t5.wav b/data/audio/t5.wav new file mode 100644 index 0000000..458288c Binary files /dev/null and b/data/audio/t5.wav differ diff --git a/data/audio/t5_1.wav b/data/audio/t5_1.wav new file mode 100644 index 0000000..458288c Binary files /dev/null and b/data/audio/t5_1.wav differ diff --git a/data/audio/t5_1_raw.wav b/data/audio/t5_1_raw.wav new file mode 100644 index 0000000..458288c Binary files /dev/null and b/data/audio/t5_1_raw.wav differ diff --git a/data/audio/t5_raw.wav b/data/audio/t5_raw.wav new file mode 100644 index 0000000..458288c Binary files /dev/null and b/data/audio/t5_raw.wav differ diff --git a/data/audio/t6.wav b/data/audio/t6.wav new file mode 100644 index 0000000..458288c Binary files /dev/null and b/data/audio/t6.wav differ diff --git a/data/audio/t6_1.wav b/data/audio/t6_1.wav new file mode 100644 index 0000000..0eed087 Binary files /dev/null and b/data/audio/t6_1.wav differ diff --git a/data/audio/t6_1_raw.wav b/data/audio/t6_1_raw.wav new file mode 100644 index 0000000..0eed087 Binary files /dev/null and b/data/audio/t6_1_raw.wav differ diff --git a/data/audio/t6_2.wav b/data/audio/t6_2.wav new file mode 100644 index 0000000..0eed087 Binary files /dev/null and b/data/audio/t6_2.wav differ diff --git a/data/audio/t6_2_raw.wav b/data/audio/t6_2_raw.wav new file mode 100644 index 0000000..0eed087 Binary files /dev/null and b/data/audio/t6_2_raw.wav differ diff --git a/data/audio/t6_raw.wav b/data/audio/t6_raw.wav new file mode 100644 index 0000000..458288c Binary files /dev/null and b/data/audio/t6_raw.wav differ diff --git a/data/audio/t7.wav b/data/audio/t7.wav new file mode 100644 index 0000000..2f800bd Binary files /dev/null and b/data/audio/t7.wav differ diff --git a/data/audio/t7_1.wav b/data/audio/t7_1.wav new file mode 100644 index 0000000..2f800bd Binary files /dev/null and b/data/audio/t7_1.wav differ diff --git a/data/audio/t7_1_raw.wav b/data/audio/t7_1_raw.wav new file mode 100644 index 0000000..2f800bd Binary files /dev/null and b/data/audio/t7_1_raw.wav differ diff --git a/data/audio/t7_raw.wav b/data/audio/t7_raw.wav new file mode 100644 index 0000000..2f800bd Binary files /dev/null and b/data/audio/t7_raw.wav differ diff --git a/data/audio/t8.wav b/data/audio/t8.wav new file mode 100644 index 0000000..8b690c8 Binary files /dev/null and b/data/audio/t8.wav differ diff --git a/data/audio/t8_1.wav b/data/audio/t8_1.wav new file mode 100644 index 0000000..8b690c8 Binary files /dev/null and b/data/audio/t8_1.wav differ diff --git a/data/audio/t8_1_raw.wav b/data/audio/t8_1_raw.wav new file mode 100644 index 0000000..8b690c8 Binary files /dev/null and b/data/audio/t8_1_raw.wav differ diff --git a/data/audio/t8_raw.wav b/data/audio/t8_raw.wav new file mode 100644 index 0000000..8b690c8 Binary files /dev/null and b/data/audio/t8_raw.wav differ diff --git a/data/audio/t9.wav b/data/audio/t9.wav new file mode 100644 index 0000000..d13aec6 Binary files /dev/null and b/data/audio/t9.wav differ diff --git a/data/audio/t9_1.wav b/data/audio/t9_1.wav new file mode 100644 index 0000000..d13aec6 Binary files /dev/null and b/data/audio/t9_1.wav differ diff --git a/data/audio/t9_1_raw.wav b/data/audio/t9_1_raw.wav new file mode 100644 index 0000000..d13aec6 Binary files /dev/null and b/data/audio/t9_1_raw.wav differ diff --git a/data/audio/t9_raw.wav b/data/audio/t9_raw.wav new file mode 100644 index 0000000..d13aec6 Binary files /dev/null and b/data/audio/t9_raw.wav differ diff --git a/data/faces/face_1/face_1.jpg b/data/faces/face_1/face_1.jpg new file mode 100644 index 0000000..db4e19f Binary files /dev/null and b/data/faces/face_1/face_1.jpg differ diff --git a/data/faces/face_1/face_2.png b/data/faces/face_1/face_2.png new file mode 100644 index 0000000..d7f8ad4 Binary files /dev/null and b/data/faces/face_1/face_2.png differ diff --git a/data/faces/face_1/meta.json b/data/faces/face_1/meta.json new file mode 100644 index 0000000..974e97a --- /dev/null +++ b/data/faces/face_1/meta.json @@ -0,0 +1,5 @@ +{ + "name": "Qassam", + "description": "مهندس روبوتات", + "added_at": "2026-05-14T17:47:55" +} \ No newline at end of file diff --git a/data/faces/face_2/face_1.jpg b/data/faces/face_2/face_1.jpg new file mode 100644 index 0000000..4cd5beb Binary files /dev/null and b/data/faces/face_2/face_1.jpg differ diff --git a/data/faces/face_2/face_2.jpg b/data/faces/face_2/face_2.jpg new file mode 100644 index 0000000..407cbdb Binary files /dev/null and b/data/faces/face_2/face_2.jpg differ diff --git a/data/faces/face_2/meta.json b/data/faces/face_2/meta.json new file mode 100644 index 0000000..2dd81a5 --- /dev/null +++ b/data/faces/face_2/meta.json @@ -0,0 +1,5 @@ +{ + "name": "hazem", + "description": "General Manager \nمدير في شركة ys lootah tech", + "added_at": "2026-05-14T17:50:01" +} \ No newline at end of file diff --git a/data/faces/face_3/face_1.jpg b/data/faces/face_3/face_1.jpg new file mode 100644 index 0000000..22df87b Binary files /dev/null and b/data/faces/face_3/face_1.jpg differ diff --git a/data/faces/face_3/face_2.jpg b/data/faces/face_3/face_2.jpg new file mode 100644 index 0000000..7f48e95 Binary files /dev/null and b/data/faces/face_3/face_2.jpg differ diff --git a/data/faces/face_3/face_3.jpg b/data/faces/face_3/face_3.jpg new file mode 100644 index 0000000..5e370e1 Binary files /dev/null and b/data/faces/face_3/face_3.jpg differ diff --git a/data/faces/face_3/meta.json b/data/faces/face_3/meta.json new file mode 100644 index 0000000..2a318b7 --- /dev/null +++ b/data/faces/face_3/meta.json @@ -0,0 +1,5 @@ +{ + "name": "humam", + "description": "Project manager", + "added_at": "2026-05-14T17:50:46" +} \ No newline at end of file diff --git a/data/faces/face_4/face_1.jpg b/data/faces/face_4/face_1.jpg new file mode 100644 index 0000000..0eb1be4 Binary files /dev/null and b/data/faces/face_4/face_1.jpg differ diff --git a/data/faces/face_4/face_2.jpg b/data/faces/face_4/face_2.jpg new file mode 100644 index 0000000..9afc12c Binary files /dev/null and b/data/faces/face_4/face_2.jpg differ diff --git a/data/faces/face_4/meta.json b/data/faces/face_4/meta.json new file mode 100644 index 0000000..695ac83 --- /dev/null +++ b/data/faces/face_4/meta.json @@ -0,0 +1,5 @@ +{ + "name": "صامت", + "description": "مصورنا صامت بضل بصورني و بنكت عليي", + "added_at": "2026-05-15T08:01:49" +} \ No newline at end of file diff --git a/data/faces/face_6/face_1.jpg b/data/faces/face_6/face_1.jpg new file mode 100644 index 0000000..ce3fef6 Binary files /dev/null and b/data/faces/face_6/face_1.jpg differ diff --git a/data/faces/face_6/meta.json b/data/faces/face_6/meta.json new file mode 100644 index 0000000..aa72745 --- /dev/null +++ b/data/faces/face_6/meta.json @@ -0,0 +1,5 @@ +{ + "name": "Liam Lin", + "description": "Sales Director in Wisemate", + "added_at": "2026-05-15T08:12:51" +} \ No newline at end of file diff --git a/data/faces/face_7/face_1.jpg b/data/faces/face_7/face_1.jpg new file mode 100644 index 0000000..d27e9a3 Binary files /dev/null and b/data/faces/face_7/face_1.jpg differ diff --git a/data/faces/face_7/face_2.jpg b/data/faces/face_7/face_2.jpg new file mode 100644 index 0000000..542fa7f Binary files /dev/null and b/data/faces/face_7/face_2.jpg differ diff --git a/data/faces/face_7/meta.json b/data/faces/face_7/meta.json new file mode 100644 index 0000000..aac89e1 --- /dev/null +++ b/data/faces/face_7/meta.json @@ -0,0 +1,5 @@ +{ + "name": "استاذ يوسف سعيد لوتاه", + "description": "المؤسس والمالك والرئيس التنفيذي لشركة لوتاه للوقود الحيوي (Lootah Biofuels) ومقرها دبي.أبرز أعماله: رائد أعمال إماراتي يركز على حلول الطاقة المستدامة وإنتاج الوقود الحيوي الصديق للبيئة.شركات أخرى: يدير أعمالاً في مجال الذكاء الاصطناعي والتكنولوجيا عبر شركة \"لوتاه", + "added_at": "2026-05-15T08:17:19" +} \ No newline at end of file diff --git a/data/faces/face_8/face_1.jpg b/data/faces/face_8/face_1.jpg new file mode 100644 index 0000000..ad85c87 Binary files /dev/null and b/data/faces/face_8/face_1.jpg differ diff --git a/data/faces/face_8/meta.json b/data/faces/face_8/meta.json new file mode 100644 index 0000000..a8639f7 --- /dev/null +++ b/data/faces/face_8/meta.json @@ -0,0 +1,5 @@ +{ + "name": "TT WANG", + "description": "Sales Director in wisemate", + "added_at": "2026-05-15T09:29:11" +} \ No newline at end of file diff --git a/data/motions/instruction.json b/data/motions/instruction.json new file mode 100644 index 0000000..e796310 --- /dev/null +++ b/data/motions/instruction.json @@ -0,0 +1,72 @@ +{ + "_comment": "N2 Phase 3 — phrase->canonical movement map for Gemini voice control. The dispatcher (voice/movement_dispatch.py) matches these against GEMINI'S OWN spoken (BOT) transcript (the Marcus phrase-confirmation pattern), NOT the user text. Gemini is instructed (system prompt) to speak ONE short canonical confirmation phrase per requested motion, in EN or AR. Bot_phrases are matched as case-insensitive substrings (English) / exact substrings (Arabic). Parametric regexes keep the number.", + "command_cooldown_sec": 1.5, + "max_steps": 8, + "max_degrees": 360, + "degrees_per_step": 15, + + "actions": { + "move_forward": { + "canonical": "move forward", + "bot_phrases": { + "english": ["moving forward", "walking forward", "stepping forward", "stepping closer", "coming closer", "moving ahead"], + "arabic": ["أمشي للأمام", "أتحرك للأمام", "أتقدم للأمام", "أتقدم خطوة", "أتقدّم خطوة"] + } + }, + "move_backward": { + "canonical": "move backward", + "bot_phrases": { + "english": ["moving back", "moving backward", "walking back", "stepping back", "backing up"], + "arabic": ["أمشي للخلف", "أتحرك للخلف", "أرجع للخلف", "أتراجع"] + } + }, + "turn_right": { + "canonical": "turn right", + "bot_phrases": { + "english": ["turning right", "rotating right", "spinning right"], + "arabic": ["أستدير يميناً", "أستدير يمينا", "أستدير لليمين", "أدور يمين"] + } + }, + "turn_left": { + "canonical": "turn left", + "bot_phrases": { + "english": ["turning left", "rotating left", "spinning left"], + "arabic": ["أستدير يساراً", "أستدير يسارا", "أستدير لليسار", "أدور يسار"] + } + }, + "slide_left": { + "canonical": "slide left", + "bot_phrases": { + "english": ["sliding left", "strafing left", "stepping left"], + "arabic": ["أنزلق لليسار", "أتحرك جانباً لليسار"] + } + }, + "slide_right": { + "canonical": "slide right", + "bot_phrases": { + "english": ["sliding right", "strafing right", "stepping right"], + "arabic": ["أنزلق لليمين", "أتحرك جانباً لليمين"] + } + }, + "stop": { + "canonical": "stop", + "bot_phrases": { + "english": ["stopping", "i'll stop", "i will stop", "halting", "holding still"], + "arabic": ["أتوقف", "سأتوقف", "توقفت", "أتوقّف"] + } + } + }, + + "parametric_actions": [ + { "regex": "walking forward (\\d+) steps?", "canonical": "walk forward $1 steps" }, + { "regex": "walking back(?:ward)? (\\d+) steps?", "canonical": "walk backward $1 steps" }, + { "regex": "walking (\\d+) steps?", "canonical": "walk forward $1 steps" }, + { "regex": "turning right (\\d+) deg(?:ree(?:s)?)?", "canonical": "turn right $1 degrees" }, + { "regex": "turning left (\\d+) deg(?:ree(?:s)?)?", "canonical": "turn left $1 degrees" }, + { "regex": "turning (\\d+) deg(?:ree(?:s)?)?", "canonical": "turn right $1 degrees" }, + { "regex": "(?:أمشي للخلف|أتحرك للخلف|أرجع|أتراجع) (\\d+) خطوات", "canonical": "walk backward $1 steps" }, + { "regex": "(?:أمشي|أتحرك|أتقدم)(?: للأمام| للامام)? (\\d+) خطوات", "canonical": "walk forward $1 steps" }, + { "regex": "(?:أستدير|أدور) (?:يميناً|يمينا|يمين|لليمين) (\\d+) درجة", "canonical": "turn right $1 degrees" }, + { "regex": "(?:أستدير|أدور) (?:يساراً|يسارا|يسار|لليسار) (\\d+) درجة", "canonical": "turn left $1 degrees" } + ] +} diff --git a/data/recordings/20260514_174629_robot.wav b/data/recordings/20260514_174629_robot.wav new file mode 100644 index 0000000..4efad26 Binary files /dev/null and b/data/recordings/20260514_174629_robot.wav differ diff --git a/data/recordings/20260514_174700_user.wav b/data/recordings/20260514_174700_user.wav new file mode 100644 index 0000000..ea42e69 Binary files /dev/null and b/data/recordings/20260514_174700_user.wav differ diff --git a/data/recordings/20260514_174701_robot.wav b/data/recordings/20260514_174701_robot.wav new file mode 100644 index 0000000..2c74797 Binary files /dev/null and b/data/recordings/20260514_174701_robot.wav differ diff --git a/data/recordings/20260514_174701_user.wav b/data/recordings/20260514_174701_user.wav new file mode 100644 index 0000000..a7eedf1 Binary files /dev/null and b/data/recordings/20260514_174701_user.wav differ diff --git a/data/recordings/20260514_174714_robot.wav b/data/recordings/20260514_174714_robot.wav new file mode 100644 index 0000000..ae54891 Binary files /dev/null and b/data/recordings/20260514_174714_robot.wav differ diff --git a/data/recordings/20260514_174714_user.wav b/data/recordings/20260514_174714_user.wav new file mode 100644 index 0000000..4928792 Binary files /dev/null and b/data/recordings/20260514_174714_user.wav differ diff --git a/data/recordings/20260514_174723_robot.wav b/data/recordings/20260514_174723_robot.wav new file mode 100644 index 0000000..729b59f Binary files /dev/null and b/data/recordings/20260514_174723_robot.wav differ diff --git a/data/recordings/20260514_174723_user.wav b/data/recordings/20260514_174723_user.wav new file mode 100644 index 0000000..1c091d4 Binary files /dev/null and b/data/recordings/20260514_174723_user.wav differ diff --git a/data/recordings/20260514_175127_robot.wav b/data/recordings/20260514_175127_robot.wav new file mode 100644 index 0000000..0981fa0 Binary files /dev/null and b/data/recordings/20260514_175127_robot.wav differ diff --git a/data/recordings/20260514_175127_user.wav b/data/recordings/20260514_175127_user.wav new file mode 100644 index 0000000..58321c9 Binary files /dev/null and b/data/recordings/20260514_175127_user.wav differ diff --git a/data/recordings/20260514_175134_user.wav b/data/recordings/20260514_175134_user.wav new file mode 100644 index 0000000..c101e63 Binary files /dev/null and b/data/recordings/20260514_175134_user.wav differ diff --git a/data/recordings/20260514_175139_robot.wav b/data/recordings/20260514_175139_robot.wav new file mode 100644 index 0000000..c7c26b9 Binary files /dev/null and b/data/recordings/20260514_175139_robot.wav differ diff --git a/data/recordings/20260514_175147_robot.wav b/data/recordings/20260514_175147_robot.wav new file mode 100644 index 0000000..88aea4a Binary files /dev/null and b/data/recordings/20260514_175147_robot.wav differ diff --git a/data/recordings/20260514_175147_user.wav b/data/recordings/20260514_175147_user.wav new file mode 100644 index 0000000..136898a Binary files /dev/null and b/data/recordings/20260514_175147_user.wav differ diff --git a/data/recordings/20260514_175200_robot.wav b/data/recordings/20260514_175200_robot.wav new file mode 100644 index 0000000..3978066 Binary files /dev/null and b/data/recordings/20260514_175200_robot.wav differ diff --git a/data/recordings/20260514_175200_user.wav b/data/recordings/20260514_175200_user.wav new file mode 100644 index 0000000..571eb2d Binary files /dev/null and b/data/recordings/20260514_175200_user.wav differ diff --git a/data/recordings/20260514_175213_robot.wav b/data/recordings/20260514_175213_robot.wav new file mode 100644 index 0000000..e4cf577 Binary files /dev/null and b/data/recordings/20260514_175213_robot.wav differ diff --git a/data/recordings/20260514_175213_user.wav b/data/recordings/20260514_175213_user.wav new file mode 100644 index 0000000..4437131 Binary files /dev/null and b/data/recordings/20260514_175213_user.wav differ diff --git a/data/recordings/20260514_175221_robot.wav b/data/recordings/20260514_175221_robot.wav new file mode 100644 index 0000000..d756367 Binary files /dev/null and b/data/recordings/20260514_175221_robot.wav differ diff --git a/data/recordings/20260514_175221_user.wav b/data/recordings/20260514_175221_user.wav new file mode 100644 index 0000000..850d7cd Binary files /dev/null and b/data/recordings/20260514_175221_user.wav differ diff --git a/data/recordings/20260514_175231_robot.wav b/data/recordings/20260514_175231_robot.wav new file mode 100644 index 0000000..6b3190f Binary files /dev/null and b/data/recordings/20260514_175231_robot.wav differ diff --git a/data/recordings/20260514_175231_user.wav b/data/recordings/20260514_175231_user.wav new file mode 100644 index 0000000..82cc264 Binary files /dev/null and b/data/recordings/20260514_175231_user.wav differ diff --git a/data/recordings/20260514_175244_robot.wav b/data/recordings/20260514_175244_robot.wav new file mode 100644 index 0000000..e69de29 diff --git a/data/recordings/20260514_175244_user.wav b/data/recordings/20260514_175244_user.wav new file mode 100644 index 0000000..e69de29 diff --git a/data/recordings/20260515_080052_robot.wav b/data/recordings/20260515_080052_robot.wav new file mode 100644 index 0000000..7307d53 Binary files /dev/null and b/data/recordings/20260515_080052_robot.wav differ diff --git a/data/recordings/20260515_080056_robot.wav b/data/recordings/20260515_080056_robot.wav new file mode 100644 index 0000000..2e54790 Binary files /dev/null and b/data/recordings/20260515_080056_robot.wav differ diff --git a/data/recordings/20260515_080056_user.wav b/data/recordings/20260515_080056_user.wav new file mode 100644 index 0000000..61671ca Binary files /dev/null and b/data/recordings/20260515_080056_user.wav differ diff --git a/data/recordings/20260515_080201_user.wav b/data/recordings/20260515_080201_user.wav new file mode 100644 index 0000000..733a0ba Binary files /dev/null and b/data/recordings/20260515_080201_user.wav differ diff --git a/data/recordings/20260515_080205_robot.wav b/data/recordings/20260515_080205_robot.wav new file mode 100644 index 0000000..f7f147f Binary files /dev/null and b/data/recordings/20260515_080205_robot.wav differ diff --git a/data/recordings/20260515_080209_robot.wav b/data/recordings/20260515_080209_robot.wav new file mode 100644 index 0000000..f3d70cd Binary files /dev/null and b/data/recordings/20260515_080209_robot.wav differ diff --git a/data/recordings/20260515_080209_user.wav b/data/recordings/20260515_080209_user.wav new file mode 100644 index 0000000..ea5bd05 Binary files /dev/null and b/data/recordings/20260515_080209_user.wav differ diff --git a/data/recordings/20260515_080221_user.wav b/data/recordings/20260515_080221_user.wav new file mode 100644 index 0000000..301c476 Binary files /dev/null and b/data/recordings/20260515_080221_user.wav differ diff --git a/data/recordings/20260515_080234_robot.wav b/data/recordings/20260515_080234_robot.wav new file mode 100644 index 0000000..831c6a3 Binary files /dev/null and b/data/recordings/20260515_080234_robot.wav differ diff --git a/data/recordings/20260515_080234_user.wav b/data/recordings/20260515_080234_user.wav new file mode 100644 index 0000000..e141821 Binary files /dev/null and b/data/recordings/20260515_080234_user.wav differ diff --git a/data/recordings/20260515_080256_robot.wav b/data/recordings/20260515_080256_robot.wav new file mode 100644 index 0000000..b9b3e8d Binary files /dev/null and b/data/recordings/20260515_080256_robot.wav differ diff --git a/data/recordings/20260515_080256_user.wav b/data/recordings/20260515_080256_user.wav new file mode 100644 index 0000000..ae9f82c Binary files /dev/null and b/data/recordings/20260515_080256_user.wav differ diff --git a/data/recordings/20260515_080302_user.wav b/data/recordings/20260515_080302_user.wav new file mode 100644 index 0000000..29615f5 Binary files /dev/null and b/data/recordings/20260515_080302_user.wav differ diff --git a/data/recordings/20260515_080305_user.wav b/data/recordings/20260515_080305_user.wav new file mode 100644 index 0000000..ff3c5c3 Binary files /dev/null and b/data/recordings/20260515_080305_user.wav differ diff --git a/data/recordings/20260515_080309_robot.wav b/data/recordings/20260515_080309_robot.wav new file mode 100644 index 0000000..3e544c4 Binary files /dev/null and b/data/recordings/20260515_080309_robot.wav differ diff --git a/data/recordings/20260515_080309_user.wav b/data/recordings/20260515_080309_user.wav new file mode 100644 index 0000000..3878685 Binary files /dev/null and b/data/recordings/20260515_080309_user.wav differ diff --git a/data/recordings/20260515_080319_robot.wav b/data/recordings/20260515_080319_robot.wav new file mode 100644 index 0000000..570a8a0 Binary files /dev/null and b/data/recordings/20260515_080319_robot.wav differ diff --git a/data/recordings/20260515_080319_user.wav b/data/recordings/20260515_080319_user.wav new file mode 100644 index 0000000..fcffc9b Binary files /dev/null and b/data/recordings/20260515_080319_user.wav differ diff --git a/data/recordings/20260515_080329_user.wav b/data/recordings/20260515_080329_user.wav new file mode 100644 index 0000000..177461a Binary files /dev/null and b/data/recordings/20260515_080329_user.wav differ diff --git a/data/recordings/20260515_080337_robot.wav b/data/recordings/20260515_080337_robot.wav new file mode 100644 index 0000000..eba3423 Binary files /dev/null and b/data/recordings/20260515_080337_robot.wav differ diff --git a/data/recordings/20260515_080337_user.wav b/data/recordings/20260515_080337_user.wav new file mode 100644 index 0000000..befa4c7 Binary files /dev/null and b/data/recordings/20260515_080337_user.wav differ diff --git a/data/recordings/20260515_080341_robot.wav b/data/recordings/20260515_080341_robot.wav new file mode 100644 index 0000000..b1f360e Binary files /dev/null and b/data/recordings/20260515_080341_robot.wav differ diff --git a/data/recordings/20260515_080341_user.wav b/data/recordings/20260515_080341_user.wav new file mode 100644 index 0000000..8612185 Binary files /dev/null and b/data/recordings/20260515_080341_user.wav differ diff --git a/data/recordings/20260515_080342_robot.wav b/data/recordings/20260515_080342_robot.wav new file mode 100644 index 0000000..830e66d Binary files /dev/null and b/data/recordings/20260515_080342_robot.wav differ diff --git a/data/recordings/20260515_080342_user.wav b/data/recordings/20260515_080342_user.wav new file mode 100644 index 0000000..c30f33f Binary files /dev/null and b/data/recordings/20260515_080342_user.wav differ diff --git a/data/recordings/20260515_080345_user.wav b/data/recordings/20260515_080345_user.wav new file mode 100644 index 0000000..a045134 Binary files /dev/null and b/data/recordings/20260515_080345_user.wav differ diff --git a/data/recordings/20260515_080348_robot.wav b/data/recordings/20260515_080348_robot.wav new file mode 100644 index 0000000..6e6bc34 Binary files /dev/null and b/data/recordings/20260515_080348_robot.wav differ diff --git a/data/recordings/20260515_080348_user.wav b/data/recordings/20260515_080348_user.wav new file mode 100644 index 0000000..457b4d7 Binary files /dev/null and b/data/recordings/20260515_080348_user.wav differ diff --git a/data/recordings/20260515_080401_robot.wav b/data/recordings/20260515_080401_robot.wav new file mode 100644 index 0000000..6cc2ef4 Binary files /dev/null and b/data/recordings/20260515_080401_robot.wav differ diff --git a/data/recordings/20260515_080401_user.wav b/data/recordings/20260515_080401_user.wav new file mode 100644 index 0000000..f31afd3 Binary files /dev/null and b/data/recordings/20260515_080401_user.wav differ diff --git a/data/recordings/20260515_080433_robot.wav b/data/recordings/20260515_080433_robot.wav new file mode 100644 index 0000000..3b78630 Binary files /dev/null and b/data/recordings/20260515_080433_robot.wav differ diff --git a/data/recordings/20260515_080438_robot.wav b/data/recordings/20260515_080438_robot.wav new file mode 100644 index 0000000..e5149bf Binary files /dev/null and b/data/recordings/20260515_080438_robot.wav differ diff --git a/data/recordings/20260515_080438_user.wav b/data/recordings/20260515_080438_user.wav new file mode 100644 index 0000000..ca1a3e5 Binary files /dev/null and b/data/recordings/20260515_080438_user.wav differ diff --git a/data/recordings/20260515_080519_robot.wav b/data/recordings/20260515_080519_robot.wav new file mode 100644 index 0000000..52d09b1 Binary files /dev/null and b/data/recordings/20260515_080519_robot.wav differ diff --git a/data/recordings/20260515_080519_user.wav b/data/recordings/20260515_080519_user.wav new file mode 100644 index 0000000..60cbd1a Binary files /dev/null and b/data/recordings/20260515_080519_user.wav differ diff --git a/data/recordings/20260515_080555_user.wav b/data/recordings/20260515_080555_user.wav new file mode 100644 index 0000000..dd567e1 Binary files /dev/null and b/data/recordings/20260515_080555_user.wav differ diff --git a/data/recordings/20260515_080615_robot.wav b/data/recordings/20260515_080615_robot.wav new file mode 100644 index 0000000..949cbe2 Binary files /dev/null and b/data/recordings/20260515_080615_robot.wav differ diff --git a/data/recordings/20260515_080615_user.wav b/data/recordings/20260515_080615_user.wav new file mode 100644 index 0000000..ffa1afb Binary files /dev/null and b/data/recordings/20260515_080615_user.wav differ diff --git a/data/recordings/20260515_080626_robot.wav b/data/recordings/20260515_080626_robot.wav new file mode 100644 index 0000000..d7c3dbd Binary files /dev/null and b/data/recordings/20260515_080626_robot.wav differ diff --git a/data/recordings/20260515_080626_user.wav b/data/recordings/20260515_080626_user.wav new file mode 100644 index 0000000..177c258 Binary files /dev/null and b/data/recordings/20260515_080626_user.wav differ diff --git a/data/recordings/20260515_080633_robot.wav b/data/recordings/20260515_080633_robot.wav new file mode 100644 index 0000000..6fb7bad Binary files /dev/null and b/data/recordings/20260515_080633_robot.wav differ diff --git a/data/recordings/20260515_080633_user.wav b/data/recordings/20260515_080633_user.wav new file mode 100644 index 0000000..45fd83a Binary files /dev/null and b/data/recordings/20260515_080633_user.wav differ diff --git a/data/recordings/20260515_081625_robot.wav b/data/recordings/20260515_081625_robot.wav new file mode 100644 index 0000000..7d04c69 Binary files /dev/null and b/data/recordings/20260515_081625_robot.wav differ diff --git a/data/recordings/20260515_081915_robot.wav b/data/recordings/20260515_081915_robot.wav new file mode 100644 index 0000000..2affc95 Binary files /dev/null and b/data/recordings/20260515_081915_robot.wav differ diff --git a/data/recordings/20260515_081919_robot.wav b/data/recordings/20260515_081919_robot.wav new file mode 100644 index 0000000..4a5ae2d Binary files /dev/null and b/data/recordings/20260515_081919_robot.wav differ diff --git a/data/recordings/20260515_081919_user.wav b/data/recordings/20260515_081919_user.wav new file mode 100644 index 0000000..e86a31b Binary files /dev/null and b/data/recordings/20260515_081919_user.wav differ diff --git a/data/recordings/20260515_081926_robot.wav b/data/recordings/20260515_081926_robot.wav new file mode 100644 index 0000000..9291859 Binary files /dev/null and b/data/recordings/20260515_081926_robot.wav differ diff --git a/data/recordings/20260515_081926_user.wav b/data/recordings/20260515_081926_user.wav new file mode 100644 index 0000000..b5af554 Binary files /dev/null and b/data/recordings/20260515_081926_user.wav differ diff --git a/data/recordings/20260515_081931_robot.wav b/data/recordings/20260515_081931_robot.wav new file mode 100644 index 0000000..d8b024b Binary files /dev/null and b/data/recordings/20260515_081931_robot.wav differ diff --git a/data/recordings/20260515_081931_user.wav b/data/recordings/20260515_081931_user.wav new file mode 100644 index 0000000..cb1ef22 Binary files /dev/null and b/data/recordings/20260515_081931_user.wav differ diff --git a/data/recordings/20260515_081937_robot.wav b/data/recordings/20260515_081937_robot.wav new file mode 100644 index 0000000..9675886 Binary files /dev/null and b/data/recordings/20260515_081937_robot.wav differ diff --git a/data/recordings/20260515_081937_user.wav b/data/recordings/20260515_081937_user.wav new file mode 100644 index 0000000..b7e56c5 Binary files /dev/null and b/data/recordings/20260515_081937_user.wav differ diff --git a/data/recordings/20260515_081949_robot.wav b/data/recordings/20260515_081949_robot.wav new file mode 100644 index 0000000..cd8b53b Binary files /dev/null and b/data/recordings/20260515_081949_robot.wav differ diff --git a/data/recordings/20260515_081949_user.wav b/data/recordings/20260515_081949_user.wav new file mode 100644 index 0000000..65d7346 Binary files /dev/null and b/data/recordings/20260515_081949_user.wav differ diff --git a/data/recordings/20260515_082011_robot.wav b/data/recordings/20260515_082011_robot.wav new file mode 100644 index 0000000..c387d32 Binary files /dev/null and b/data/recordings/20260515_082011_robot.wav differ diff --git a/data/recordings/20260515_082011_user.wav b/data/recordings/20260515_082011_user.wav new file mode 100644 index 0000000..219b5e8 Binary files /dev/null and b/data/recordings/20260515_082011_user.wav differ diff --git a/data/recordings/20260515_082024_robot.wav b/data/recordings/20260515_082024_robot.wav new file mode 100644 index 0000000..419f45b Binary files /dev/null and b/data/recordings/20260515_082024_robot.wav differ diff --git a/data/recordings/20260515_082024_user.wav b/data/recordings/20260515_082024_user.wav new file mode 100644 index 0000000..c94c444 Binary files /dev/null and b/data/recordings/20260515_082024_user.wav differ diff --git a/data/recordings/20260515_083239_robot.wav b/data/recordings/20260515_083239_robot.wav new file mode 100644 index 0000000..b10b8c3 Binary files /dev/null and b/data/recordings/20260515_083239_robot.wav differ diff --git a/data/recordings/20260515_083245_robot.wav b/data/recordings/20260515_083245_robot.wav new file mode 100644 index 0000000..26cafe3 Binary files /dev/null and b/data/recordings/20260515_083245_robot.wav differ diff --git a/data/recordings/20260515_083245_user.wav b/data/recordings/20260515_083245_user.wav new file mode 100644 index 0000000..f2fc8b7 Binary files /dev/null and b/data/recordings/20260515_083245_user.wav differ diff --git a/data/recordings/20260515_083253_robot.wav b/data/recordings/20260515_083253_robot.wav new file mode 100644 index 0000000..4236cb5 Binary files /dev/null and b/data/recordings/20260515_083253_robot.wav differ diff --git a/data/recordings/20260515_083253_user.wav b/data/recordings/20260515_083253_user.wav new file mode 100644 index 0000000..188f701 Binary files /dev/null and b/data/recordings/20260515_083253_user.wav differ diff --git a/data/recordings/20260515_083300_robot.wav b/data/recordings/20260515_083300_robot.wav new file mode 100644 index 0000000..f300956 Binary files /dev/null and b/data/recordings/20260515_083300_robot.wav differ diff --git a/data/recordings/20260515_083300_user.wav b/data/recordings/20260515_083300_user.wav new file mode 100644 index 0000000..01e6ad6 Binary files /dev/null and b/data/recordings/20260515_083300_user.wav differ diff --git a/data/recordings/20260515_083318_robot.wav b/data/recordings/20260515_083318_robot.wav new file mode 100644 index 0000000..cd1fbbf Binary files /dev/null and b/data/recordings/20260515_083318_robot.wav differ diff --git a/data/recordings/20260515_083318_user.wav b/data/recordings/20260515_083318_user.wav new file mode 100644 index 0000000..5d0bf36 Binary files /dev/null and b/data/recordings/20260515_083318_user.wav differ diff --git a/data/recordings/20260515_083329_user.wav b/data/recordings/20260515_083329_user.wav new file mode 100644 index 0000000..5a4a742 Binary files /dev/null and b/data/recordings/20260515_083329_user.wav differ diff --git a/data/recordings/20260515_083330_robot.wav b/data/recordings/20260515_083330_robot.wav new file mode 100644 index 0000000..e181240 Binary files /dev/null and b/data/recordings/20260515_083330_robot.wav differ diff --git a/data/recordings/20260515_083330_user.wav b/data/recordings/20260515_083330_user.wav new file mode 100644 index 0000000..dc5fb64 Binary files /dev/null and b/data/recordings/20260515_083330_user.wav differ diff --git a/data/recordings/20260515_083338_robot.wav b/data/recordings/20260515_083338_robot.wav new file mode 100644 index 0000000..f0f9dc7 Binary files /dev/null and b/data/recordings/20260515_083338_robot.wav differ diff --git a/data/recordings/20260515_083338_user.wav b/data/recordings/20260515_083338_user.wav new file mode 100644 index 0000000..7f8f5a9 Binary files /dev/null and b/data/recordings/20260515_083338_user.wav differ diff --git a/data/recordings/20260515_083345_user.wav b/data/recordings/20260515_083345_user.wav new file mode 100644 index 0000000..aecec25 Binary files /dev/null and b/data/recordings/20260515_083345_user.wav differ diff --git a/data/recordings/20260515_083348_user.wav b/data/recordings/20260515_083348_user.wav new file mode 100644 index 0000000..91601bd Binary files /dev/null and b/data/recordings/20260515_083348_user.wav differ diff --git a/data/recordings/20260515_083417_robot.wav b/data/recordings/20260515_083417_robot.wav new file mode 100644 index 0000000..a1144dc Binary files /dev/null and b/data/recordings/20260515_083417_robot.wav differ diff --git a/data/recordings/20260515_083421_user.wav b/data/recordings/20260515_083421_user.wav new file mode 100644 index 0000000..fddaaa6 Binary files /dev/null and b/data/recordings/20260515_083421_user.wav differ diff --git a/data/recordings/20260515_083434_user.wav b/data/recordings/20260515_083434_user.wav new file mode 100644 index 0000000..eb1c220 Binary files /dev/null and b/data/recordings/20260515_083434_user.wav differ diff --git a/data/recordings/20260515_083435_robot.wav b/data/recordings/20260515_083435_robot.wav new file mode 100644 index 0000000..0f43366 Binary files /dev/null and b/data/recordings/20260515_083435_robot.wav differ diff --git a/data/recordings/20260515_083435_user.wav b/data/recordings/20260515_083435_user.wav new file mode 100644 index 0000000..22c5510 Binary files /dev/null and b/data/recordings/20260515_083435_user.wav differ diff --git a/data/recordings/20260515_083446_robot.wav b/data/recordings/20260515_083446_robot.wav new file mode 100644 index 0000000..e045e99 Binary files /dev/null and b/data/recordings/20260515_083446_robot.wav differ diff --git a/data/recordings/20260515_083446_user.wav b/data/recordings/20260515_083446_user.wav new file mode 100644 index 0000000..eb07aa7 Binary files /dev/null and b/data/recordings/20260515_083446_user.wav differ diff --git a/data/recordings/20260515_083454_robot.wav b/data/recordings/20260515_083454_robot.wav new file mode 100644 index 0000000..85ba519 Binary files /dev/null and b/data/recordings/20260515_083454_robot.wav differ diff --git a/data/recordings/20260515_083454_user.wav b/data/recordings/20260515_083454_user.wav new file mode 100644 index 0000000..8ee11fc Binary files /dev/null and b/data/recordings/20260515_083454_user.wav differ diff --git a/data/recordings/20260515_084552_robot.wav b/data/recordings/20260515_084552_robot.wav new file mode 100644 index 0000000..31bcd77 Binary files /dev/null and b/data/recordings/20260515_084552_robot.wav differ diff --git a/data/recordings/20260515_092655_robot.wav b/data/recordings/20260515_092655_robot.wav new file mode 100644 index 0000000..9a231a1 Binary files /dev/null and b/data/recordings/20260515_092655_robot.wav differ diff --git a/data/recordings/20260515_092655_user.wav b/data/recordings/20260515_092655_user.wav new file mode 100644 index 0000000..dbff413 Binary files /dev/null and b/data/recordings/20260515_092655_user.wav differ diff --git a/data/recordings/20260515_092658_robot.wav b/data/recordings/20260515_092658_robot.wav new file mode 100644 index 0000000..e66bda0 Binary files /dev/null and b/data/recordings/20260515_092658_robot.wav differ diff --git a/data/recordings/20260515_092658_user.wav b/data/recordings/20260515_092658_user.wav new file mode 100644 index 0000000..64df9a6 Binary files /dev/null and b/data/recordings/20260515_092658_user.wav differ diff --git a/data/recordings/20260515_092659_user.wav b/data/recordings/20260515_092659_user.wav new file mode 100644 index 0000000..3fb9f45 Binary files /dev/null and b/data/recordings/20260515_092659_user.wav differ diff --git a/data/recordings/20260515_092702_robot.wav b/data/recordings/20260515_092702_robot.wav new file mode 100644 index 0000000..9b93a82 Binary files /dev/null and b/data/recordings/20260515_092702_robot.wav differ diff --git a/data/recordings/20260515_092702_user.wav b/data/recordings/20260515_092702_user.wav new file mode 100644 index 0000000..18780a9 Binary files /dev/null and b/data/recordings/20260515_092702_user.wav differ diff --git a/data/recordings/20260515_092713_robot.wav b/data/recordings/20260515_092713_robot.wav new file mode 100644 index 0000000..654b39a Binary files /dev/null and b/data/recordings/20260515_092713_robot.wav differ diff --git a/data/recordings/20260515_092713_user.wav b/data/recordings/20260515_092713_user.wav new file mode 100644 index 0000000..213aec5 Binary files /dev/null and b/data/recordings/20260515_092713_user.wav differ diff --git a/data/recordings/20260515_092725_user.wav b/data/recordings/20260515_092725_user.wav new file mode 100644 index 0000000..0b2fba3 Binary files /dev/null and b/data/recordings/20260515_092725_user.wav differ diff --git a/data/recordings/20260515_092727_robot.wav b/data/recordings/20260515_092727_robot.wav new file mode 100644 index 0000000..558ff81 Binary files /dev/null and b/data/recordings/20260515_092727_robot.wav differ diff --git a/data/recordings/20260515_092727_user.wav b/data/recordings/20260515_092727_user.wav new file mode 100644 index 0000000..ae4f4ec Binary files /dev/null and b/data/recordings/20260515_092727_user.wav differ diff --git a/data/recordings/20260515_092732_robot.wav b/data/recordings/20260515_092732_robot.wav new file mode 100644 index 0000000..4c1abf1 Binary files /dev/null and b/data/recordings/20260515_092732_robot.wav differ diff --git a/data/recordings/20260515_092732_user.wav b/data/recordings/20260515_092732_user.wav new file mode 100644 index 0000000..48adb28 Binary files /dev/null and b/data/recordings/20260515_092732_user.wav differ diff --git a/data/recordings/20260515_092733_user.wav b/data/recordings/20260515_092733_user.wav new file mode 100644 index 0000000..9329bb1 Binary files /dev/null and b/data/recordings/20260515_092733_user.wav differ diff --git a/data/recordings/20260515_092741_robot.wav b/data/recordings/20260515_092741_robot.wav new file mode 100644 index 0000000..29461a5 Binary files /dev/null and b/data/recordings/20260515_092741_robot.wav differ diff --git a/data/recordings/20260515_092741_user.wav b/data/recordings/20260515_092741_user.wav new file mode 100644 index 0000000..7fb82ed Binary files /dev/null and b/data/recordings/20260515_092741_user.wav differ diff --git a/data/recordings/20260515_092752_robot.wav b/data/recordings/20260515_092752_robot.wav new file mode 100644 index 0000000..dab38b0 Binary files /dev/null and b/data/recordings/20260515_092752_robot.wav differ diff --git a/data/recordings/20260515_092752_user.wav b/data/recordings/20260515_092752_user.wav new file mode 100644 index 0000000..d9c922e Binary files /dev/null and b/data/recordings/20260515_092752_user.wav differ diff --git a/data/recordings/20260515_092806_robot.wav b/data/recordings/20260515_092806_robot.wav new file mode 100644 index 0000000..f4d0cfb Binary files /dev/null and b/data/recordings/20260515_092806_robot.wav differ diff --git a/data/recordings/20260515_092806_user.wav b/data/recordings/20260515_092806_user.wav new file mode 100644 index 0000000..1e28081 Binary files /dev/null and b/data/recordings/20260515_092806_user.wav differ diff --git a/data/recordings/20260515_092814_user.wav b/data/recordings/20260515_092814_user.wav new file mode 100644 index 0000000..a26e8a3 Binary files /dev/null and b/data/recordings/20260515_092814_user.wav differ diff --git a/data/recordings/20260515_092840_user.wav b/data/recordings/20260515_092840_user.wav new file mode 100644 index 0000000..ab3aefe Binary files /dev/null and b/data/recordings/20260515_092840_user.wav differ diff --git a/data/recordings/20260515_092846_user.wav b/data/recordings/20260515_092846_user.wav new file mode 100644 index 0000000..b362d4e Binary files /dev/null and b/data/recordings/20260515_092846_user.wav differ diff --git a/data/recordings/20260515_092853_user.wav b/data/recordings/20260515_092853_user.wav new file mode 100644 index 0000000..8956dbe Binary files /dev/null and b/data/recordings/20260515_092853_user.wav differ diff --git a/data/recordings/20260515_092900_robot.wav b/data/recordings/20260515_092900_robot.wav new file mode 100644 index 0000000..ad696be Binary files /dev/null and b/data/recordings/20260515_092900_robot.wav differ diff --git a/data/recordings/20260515_092900_user.wav b/data/recordings/20260515_092900_user.wav new file mode 100644 index 0000000..c0393b0 Binary files /dev/null and b/data/recordings/20260515_092900_user.wav differ diff --git a/data/recordings/20260515_092910_user.wav b/data/recordings/20260515_092910_user.wav new file mode 100644 index 0000000..29da635 Binary files /dev/null and b/data/recordings/20260515_092910_user.wav differ diff --git a/data/recordings/20260515_092915_user.wav b/data/recordings/20260515_092915_user.wav new file mode 100644 index 0000000..77ab4c6 Binary files /dev/null and b/data/recordings/20260515_092915_user.wav differ diff --git a/data/recordings/20260515_092919_robot.wav b/data/recordings/20260515_092919_robot.wav new file mode 100644 index 0000000..6a3a461 Binary files /dev/null and b/data/recordings/20260515_092919_robot.wav differ diff --git a/data/recordings/20260515_092919_user.wav b/data/recordings/20260515_092919_user.wav new file mode 100644 index 0000000..bbff718 Binary files /dev/null and b/data/recordings/20260515_092919_user.wav differ diff --git a/data/recordings/20260515_092931_robot.wav b/data/recordings/20260515_092931_robot.wav new file mode 100644 index 0000000..950dd6b Binary files /dev/null and b/data/recordings/20260515_092931_robot.wav differ diff --git a/data/recordings/20260515_092931_user.wav b/data/recordings/20260515_092931_user.wav new file mode 100644 index 0000000..32c4977 Binary files /dev/null and b/data/recordings/20260515_092931_user.wav differ diff --git a/data/recordings/20260515_092942_user.wav b/data/recordings/20260515_092942_user.wav new file mode 100644 index 0000000..9c089cf Binary files /dev/null and b/data/recordings/20260515_092942_user.wav differ diff --git a/data/recordings/20260515_092945_robot.wav b/data/recordings/20260515_092945_robot.wav new file mode 100644 index 0000000..e9f31b2 Binary files /dev/null and b/data/recordings/20260515_092945_robot.wav differ diff --git a/data/recordings/20260515_092945_user.wav b/data/recordings/20260515_092945_user.wav new file mode 100644 index 0000000..c0078b3 Binary files /dev/null and b/data/recordings/20260515_092945_user.wav differ diff --git a/data/recordings/20260515_092958_user.wav b/data/recordings/20260515_092958_user.wav new file mode 100644 index 0000000..a512240 Binary files /dev/null and b/data/recordings/20260515_092958_user.wav differ diff --git a/data/recordings/20260515_093001_user.wav b/data/recordings/20260515_093001_user.wav new file mode 100644 index 0000000..4aaa857 Binary files /dev/null and b/data/recordings/20260515_093001_user.wav differ diff --git a/data/recordings/20260515_093002_user.wav b/data/recordings/20260515_093002_user.wav new file mode 100644 index 0000000..adb563e Binary files /dev/null and b/data/recordings/20260515_093002_user.wav differ diff --git a/data/recordings/20260515_093006_robot.wav b/data/recordings/20260515_093006_robot.wav new file mode 100644 index 0000000..0f48107 Binary files /dev/null and b/data/recordings/20260515_093006_robot.wav differ diff --git a/data/recordings/20260515_093006_user.wav b/data/recordings/20260515_093006_user.wav new file mode 100644 index 0000000..9dcc598 Binary files /dev/null and b/data/recordings/20260515_093006_user.wav differ diff --git a/data/recordings/20260515_093008_robot.wav b/data/recordings/20260515_093008_robot.wav new file mode 100644 index 0000000..91313d3 Binary files /dev/null and b/data/recordings/20260515_093008_robot.wav differ diff --git a/data/recordings/20260515_093008_user.wav b/data/recordings/20260515_093008_user.wav new file mode 100644 index 0000000..23295b0 Binary files /dev/null and b/data/recordings/20260515_093008_user.wav differ diff --git a/data/recordings/20260515_093010_user.wav b/data/recordings/20260515_093010_user.wav new file mode 100644 index 0000000..7ed7ff1 Binary files /dev/null and b/data/recordings/20260515_093010_user.wav differ diff --git a/data/recordings/20260515_093011_user.wav b/data/recordings/20260515_093011_user.wav new file mode 100644 index 0000000..226f9c9 Binary files /dev/null and b/data/recordings/20260515_093011_user.wav differ diff --git a/data/recordings/20260515_093012_robot.wav b/data/recordings/20260515_093012_robot.wav new file mode 100644 index 0000000..b8fe0ff Binary files /dev/null and b/data/recordings/20260515_093012_robot.wav differ diff --git a/data/recordings/20260515_093012_user.wav b/data/recordings/20260515_093012_user.wav new file mode 100644 index 0000000..41c8f86 Binary files /dev/null and b/data/recordings/20260515_093012_user.wav differ diff --git a/data/recordings/20260515_093019_user.wav b/data/recordings/20260515_093019_user.wav new file mode 100644 index 0000000..e12b4ac Binary files /dev/null and b/data/recordings/20260515_093019_user.wav differ diff --git a/data/recordings/20260515_093021_robot.wav b/data/recordings/20260515_093021_robot.wav new file mode 100644 index 0000000..58eef1e Binary files /dev/null and b/data/recordings/20260515_093021_robot.wav differ diff --git a/data/recordings/20260515_093021_user.wav b/data/recordings/20260515_093021_user.wav new file mode 100644 index 0000000..b9818ee Binary files /dev/null and b/data/recordings/20260515_093021_user.wav differ diff --git a/data/recordings/20260515_093029_user.wav b/data/recordings/20260515_093029_user.wav new file mode 100644 index 0000000..5783c21 Binary files /dev/null and b/data/recordings/20260515_093029_user.wav differ diff --git a/data/recordings/20260515_093030_robot.wav b/data/recordings/20260515_093030_robot.wav new file mode 100644 index 0000000..b63bb33 Binary files /dev/null and b/data/recordings/20260515_093030_robot.wav differ diff --git a/data/recordings/20260515_093030_user.wav b/data/recordings/20260515_093030_user.wav new file mode 100644 index 0000000..8728c38 Binary files /dev/null and b/data/recordings/20260515_093030_user.wav differ diff --git a/data/recordings/20260515_093040_robot.wav b/data/recordings/20260515_093040_robot.wav new file mode 100644 index 0000000..9597ca2 Binary files /dev/null and b/data/recordings/20260515_093040_robot.wav differ diff --git a/data/recordings/20260515_093040_user.wav b/data/recordings/20260515_093040_user.wav new file mode 100644 index 0000000..f0a119b Binary files /dev/null and b/data/recordings/20260515_093040_user.wav differ diff --git a/data/recordings/20260515_093106_robot.wav b/data/recordings/20260515_093106_robot.wav new file mode 100644 index 0000000..afcd30f Binary files /dev/null and b/data/recordings/20260515_093106_robot.wav differ diff --git a/data/recordings/20260515_093106_user.wav b/data/recordings/20260515_093106_user.wav new file mode 100644 index 0000000..f261704 Binary files /dev/null and b/data/recordings/20260515_093106_user.wav differ diff --git a/data/recordings/20260515_093114_user.wav b/data/recordings/20260515_093114_user.wav new file mode 100644 index 0000000..8a7733f Binary files /dev/null and b/data/recordings/20260515_093114_user.wav differ diff --git a/data/recordings/20260515_093120_robot.wav b/data/recordings/20260515_093120_robot.wav new file mode 100644 index 0000000..080ab9a Binary files /dev/null and b/data/recordings/20260515_093120_robot.wav differ diff --git a/data/recordings/20260515_093120_user.wav b/data/recordings/20260515_093120_user.wav new file mode 100644 index 0000000..4458fd7 Binary files /dev/null and b/data/recordings/20260515_093120_user.wav differ diff --git a/data/recordings/20260515_093126_user.wav b/data/recordings/20260515_093126_user.wav new file mode 100644 index 0000000..e425258 Binary files /dev/null and b/data/recordings/20260515_093126_user.wav differ diff --git a/data/recordings/20260515_093129_robot.wav b/data/recordings/20260515_093129_robot.wav new file mode 100644 index 0000000..99b8fb3 Binary files /dev/null and b/data/recordings/20260515_093129_robot.wav differ diff --git a/data/recordings/20260515_093129_user.wav b/data/recordings/20260515_093129_user.wav new file mode 100644 index 0000000..ad951d2 Binary files /dev/null and b/data/recordings/20260515_093129_user.wav differ diff --git a/data/recordings/20260515_093133_robot.wav b/data/recordings/20260515_093133_robot.wav new file mode 100644 index 0000000..a6683e3 Binary files /dev/null and b/data/recordings/20260515_093133_robot.wav differ diff --git a/data/recordings/20260515_093133_user.wav b/data/recordings/20260515_093133_user.wav new file mode 100644 index 0000000..dcdd56a Binary files /dev/null and b/data/recordings/20260515_093133_user.wav differ diff --git a/data/recordings/20260515_093137_user.wav b/data/recordings/20260515_093137_user.wav new file mode 100644 index 0000000..8ab6d6d Binary files /dev/null and b/data/recordings/20260515_093137_user.wav differ diff --git a/data/recordings/20260515_093139_robot.wav b/data/recordings/20260515_093139_robot.wav new file mode 100644 index 0000000..ab27578 Binary files /dev/null and b/data/recordings/20260515_093139_robot.wav differ diff --git a/data/recordings/20260515_093139_user.wav b/data/recordings/20260515_093139_user.wav new file mode 100644 index 0000000..90859ec Binary files /dev/null and b/data/recordings/20260515_093139_user.wav differ diff --git a/data/recordings/20260515_093144_user.wav b/data/recordings/20260515_093144_user.wav new file mode 100644 index 0000000..59cb2c2 Binary files /dev/null and b/data/recordings/20260515_093144_user.wav differ diff --git a/data/recordings/20260515_093146_user.wav b/data/recordings/20260515_093146_user.wav new file mode 100644 index 0000000..5d36f1f Binary files /dev/null and b/data/recordings/20260515_093146_user.wav differ diff --git a/data/recordings/20260515_093149_robot.wav b/data/recordings/20260515_093149_robot.wav new file mode 100644 index 0000000..38de491 Binary files /dev/null and b/data/recordings/20260515_093149_robot.wav differ diff --git a/data/recordings/20260515_093149_user.wav b/data/recordings/20260515_093149_user.wav new file mode 100644 index 0000000..5baada5 Binary files /dev/null and b/data/recordings/20260515_093149_user.wav differ diff --git a/data/recordings/20260515_093151_robot.wav b/data/recordings/20260515_093151_robot.wav new file mode 100644 index 0000000..9cf98e8 Binary files /dev/null and b/data/recordings/20260515_093151_robot.wav differ diff --git a/data/recordings/20260515_093151_user.wav b/data/recordings/20260515_093151_user.wav new file mode 100644 index 0000000..0f078fd Binary files /dev/null and b/data/recordings/20260515_093151_user.wav differ diff --git a/data/recordings/20260515_093152_user.wav b/data/recordings/20260515_093152_user.wav new file mode 100644 index 0000000..8c9a49b Binary files /dev/null and b/data/recordings/20260515_093152_user.wav differ diff --git a/data/recordings/20260515_093154_robot.wav b/data/recordings/20260515_093154_robot.wav new file mode 100644 index 0000000..1ead35b Binary files /dev/null and b/data/recordings/20260515_093154_robot.wav differ diff --git a/data/recordings/20260515_093154_user.wav b/data/recordings/20260515_093154_user.wav new file mode 100644 index 0000000..7039382 Binary files /dev/null and b/data/recordings/20260515_093154_user.wav differ diff --git a/data/recordings/20260515_093157_robot.wav b/data/recordings/20260515_093157_robot.wav new file mode 100644 index 0000000..8c1a78c Binary files /dev/null and b/data/recordings/20260515_093157_robot.wav differ diff --git a/data/recordings/20260515_093157_user.wav b/data/recordings/20260515_093157_user.wav new file mode 100644 index 0000000..bd69077 Binary files /dev/null and b/data/recordings/20260515_093157_user.wav differ diff --git a/data/recordings/20260515_093204_robot.wav b/data/recordings/20260515_093204_robot.wav new file mode 100644 index 0000000..6d07e2a Binary files /dev/null and b/data/recordings/20260515_093204_robot.wav differ diff --git a/data/recordings/20260515_093204_user.wav b/data/recordings/20260515_093204_user.wav new file mode 100644 index 0000000..3f1dbfc Binary files /dev/null and b/data/recordings/20260515_093204_user.wav differ diff --git a/data/recordings/20260515_093215_robot.wav b/data/recordings/20260515_093215_robot.wav new file mode 100644 index 0000000..0e2720a Binary files /dev/null and b/data/recordings/20260515_093215_robot.wav differ diff --git a/data/recordings/20260515_093215_user.wav b/data/recordings/20260515_093215_user.wav new file mode 100644 index 0000000..3f7694c Binary files /dev/null and b/data/recordings/20260515_093215_user.wav differ diff --git a/data/recordings/20260515_093225_user.wav b/data/recordings/20260515_093225_user.wav new file mode 100644 index 0000000..064ea31 Binary files /dev/null and b/data/recordings/20260515_093225_user.wav differ diff --git a/data/recordings/20260515_093226_robot.wav b/data/recordings/20260515_093226_robot.wav new file mode 100644 index 0000000..5e4b977 Binary files /dev/null and b/data/recordings/20260515_093226_robot.wav differ diff --git a/data/recordings/20260515_093226_user.wav b/data/recordings/20260515_093226_user.wav new file mode 100644 index 0000000..cc584a9 Binary files /dev/null and b/data/recordings/20260515_093226_user.wav differ diff --git a/data/recordings/20260515_093234_user.wav b/data/recordings/20260515_093234_user.wav new file mode 100644 index 0000000..fbc68a7 Binary files /dev/null and b/data/recordings/20260515_093234_user.wav differ diff --git a/data/recordings/20260515_093236_user.wav b/data/recordings/20260515_093236_user.wav new file mode 100644 index 0000000..534f150 Binary files /dev/null and b/data/recordings/20260515_093236_user.wav differ diff --git a/data/recordings/20260515_093239_robot.wav b/data/recordings/20260515_093239_robot.wav new file mode 100644 index 0000000..582efba Binary files /dev/null and b/data/recordings/20260515_093239_robot.wav differ diff --git a/data/recordings/20260515_093239_user.wav b/data/recordings/20260515_093239_user.wav new file mode 100644 index 0000000..ec7f091 Binary files /dev/null and b/data/recordings/20260515_093239_user.wav differ diff --git a/data/recordings/20260515_093245_user.wav b/data/recordings/20260515_093245_user.wav new file mode 100644 index 0000000..3e67579 Binary files /dev/null and b/data/recordings/20260515_093245_user.wav differ diff --git a/data/recordings/20260515_093246_user.wav b/data/recordings/20260515_093246_user.wav new file mode 100644 index 0000000..ce255bc Binary files /dev/null and b/data/recordings/20260515_093246_user.wav differ diff --git a/data/recordings/20260515_093258_robot.wav b/data/recordings/20260515_093258_robot.wav new file mode 100644 index 0000000..69a4734 Binary files /dev/null and b/data/recordings/20260515_093258_robot.wav differ diff --git a/data/recordings/20260515_093258_user.wav b/data/recordings/20260515_093258_user.wav new file mode 100644 index 0000000..dae978d Binary files /dev/null and b/data/recordings/20260515_093258_user.wav differ diff --git a/data/recordings/20260515_093303_user.wav b/data/recordings/20260515_093303_user.wav new file mode 100644 index 0000000..e9bd655 Binary files /dev/null and b/data/recordings/20260515_093303_user.wav differ diff --git a/data/recordings/20260515_093304_user.wav b/data/recordings/20260515_093304_user.wav new file mode 100644 index 0000000..f40fb5d Binary files /dev/null and b/data/recordings/20260515_093304_user.wav differ diff --git a/data/recordings/20260515_093307_user.wav b/data/recordings/20260515_093307_user.wav new file mode 100644 index 0000000..e2ab1af Binary files /dev/null and b/data/recordings/20260515_093307_user.wav differ diff --git a/data/recordings/20260515_093311_user.wav b/data/recordings/20260515_093311_user.wav new file mode 100644 index 0000000..7816a43 Binary files /dev/null and b/data/recordings/20260515_093311_user.wav differ diff --git a/data/recordings/20260515_093314_robot.wav b/data/recordings/20260515_093314_robot.wav new file mode 100644 index 0000000..048c0b2 Binary files /dev/null and b/data/recordings/20260515_093314_robot.wav differ diff --git a/data/recordings/20260515_093314_user.wav b/data/recordings/20260515_093314_user.wav new file mode 100644 index 0000000..b07cac5 Binary files /dev/null and b/data/recordings/20260515_093314_user.wav differ diff --git a/data/recordings/20260515_093322_robot.wav b/data/recordings/20260515_093322_robot.wav new file mode 100644 index 0000000..19f969b Binary files /dev/null and b/data/recordings/20260515_093322_robot.wav differ diff --git a/data/recordings/20260515_093322_user.wav b/data/recordings/20260515_093322_user.wav new file mode 100644 index 0000000..982e731 Binary files /dev/null and b/data/recordings/20260515_093322_user.wav differ diff --git a/data/recordings/20260515_093330_robot.wav b/data/recordings/20260515_093330_robot.wav new file mode 100644 index 0000000..398a52e Binary files /dev/null and b/data/recordings/20260515_093330_robot.wav differ diff --git a/data/recordings/20260515_093330_user.wav b/data/recordings/20260515_093330_user.wav new file mode 100644 index 0000000..7478780 Binary files /dev/null and b/data/recordings/20260515_093330_user.wav differ diff --git a/data/recordings/20260515_093332_user.wav b/data/recordings/20260515_093332_user.wav new file mode 100644 index 0000000..8a9f8a6 Binary files /dev/null and b/data/recordings/20260515_093332_user.wav differ diff --git a/data/recordings/20260515_093334_robot.wav b/data/recordings/20260515_093334_robot.wav new file mode 100644 index 0000000..9b1633b Binary files /dev/null and b/data/recordings/20260515_093334_robot.wav differ diff --git a/data/recordings/20260515_093334_user.wav b/data/recordings/20260515_093334_user.wav new file mode 100644 index 0000000..1ef2944 Binary files /dev/null and b/data/recordings/20260515_093334_user.wav differ diff --git a/data/recordings/20260515_093357_robot.wav b/data/recordings/20260515_093357_robot.wav new file mode 100644 index 0000000..3f203f8 Binary files /dev/null and b/data/recordings/20260515_093357_robot.wav differ diff --git a/data/recordings/20260515_093357_user.wav b/data/recordings/20260515_093357_user.wav new file mode 100644 index 0000000..c81702b Binary files /dev/null and b/data/recordings/20260515_093357_user.wav differ diff --git a/data/recordings/20260515_093402_user.wav b/data/recordings/20260515_093402_user.wav new file mode 100644 index 0000000..3276336 Binary files /dev/null and b/data/recordings/20260515_093402_user.wav differ diff --git a/data/recordings/20260515_093410_user.wav b/data/recordings/20260515_093410_user.wav new file mode 100644 index 0000000..cf75ebc Binary files /dev/null and b/data/recordings/20260515_093410_user.wav differ diff --git a/data/recordings/20260515_093413_user.wav b/data/recordings/20260515_093413_user.wav new file mode 100644 index 0000000..36f95aa Binary files /dev/null and b/data/recordings/20260515_093413_user.wav differ diff --git a/data/recordings/20260515_093415_user.wav b/data/recordings/20260515_093415_user.wav new file mode 100644 index 0000000..3ee5c96 Binary files /dev/null and b/data/recordings/20260515_093415_user.wav differ diff --git a/data/recordings/20260515_093422_robot.wav b/data/recordings/20260515_093422_robot.wav new file mode 100644 index 0000000..589484a Binary files /dev/null and b/data/recordings/20260515_093422_robot.wav differ diff --git a/data/recordings/20260515_093422_user.wav b/data/recordings/20260515_093422_user.wav new file mode 100644 index 0000000..2b73f23 Binary files /dev/null and b/data/recordings/20260515_093422_user.wav differ diff --git a/data/recordings/20260515_093429_user.wav b/data/recordings/20260515_093429_user.wav new file mode 100644 index 0000000..f9be38a Binary files /dev/null and b/data/recordings/20260515_093429_user.wav differ diff --git a/data/recordings/20260515_093435_user.wav b/data/recordings/20260515_093435_user.wav new file mode 100644 index 0000000..6de230a Binary files /dev/null and b/data/recordings/20260515_093435_user.wav differ diff --git a/data/recordings/20260515_093442_user.wav b/data/recordings/20260515_093442_user.wav new file mode 100644 index 0000000..0c6e9d5 Binary files /dev/null and b/data/recordings/20260515_093442_user.wav differ diff --git a/data/recordings/20260515_093450_robot.wav b/data/recordings/20260515_093450_robot.wav new file mode 100644 index 0000000..81ea3d3 Binary files /dev/null and b/data/recordings/20260515_093450_robot.wav differ diff --git a/data/recordings/20260515_093450_user.wav b/data/recordings/20260515_093450_user.wav new file mode 100644 index 0000000..9c50bc1 Binary files /dev/null and b/data/recordings/20260515_093450_user.wav differ diff --git a/data/recordings/20260515_093454_user.wav b/data/recordings/20260515_093454_user.wav new file mode 100644 index 0000000..7f4d0ba Binary files /dev/null and b/data/recordings/20260515_093454_user.wav differ diff --git a/data/recordings/20260515_093456_user.wav b/data/recordings/20260515_093456_user.wav new file mode 100644 index 0000000..54d1f7c Binary files /dev/null and b/data/recordings/20260515_093456_user.wav differ diff --git a/data/recordings/20260515_093519_robot.wav b/data/recordings/20260515_093519_robot.wav new file mode 100644 index 0000000..04b4649 Binary files /dev/null and b/data/recordings/20260515_093519_robot.wav differ diff --git a/data/recordings/20260515_093519_user.wav b/data/recordings/20260515_093519_user.wav new file mode 100644 index 0000000..a63dfb5 Binary files /dev/null and b/data/recordings/20260515_093519_user.wav differ diff --git a/data/recordings/20260515_093525_user.wav b/data/recordings/20260515_093525_user.wav new file mode 100644 index 0000000..b69a548 Binary files /dev/null and b/data/recordings/20260515_093525_user.wav differ diff --git a/data/recordings/20260515_093526_user.wav b/data/recordings/20260515_093526_user.wav new file mode 100644 index 0000000..887d9af Binary files /dev/null and b/data/recordings/20260515_093526_user.wav differ diff --git a/data/recordings/20260515_093537_user.wav b/data/recordings/20260515_093537_user.wav new file mode 100644 index 0000000..1187b15 Binary files /dev/null and b/data/recordings/20260515_093537_user.wav differ diff --git a/data/recordings/20260515_093542_user.wav b/data/recordings/20260515_093542_user.wav new file mode 100644 index 0000000..cdbe402 Binary files /dev/null and b/data/recordings/20260515_093542_user.wav differ diff --git a/data/recordings/20260515_093551_user.wav b/data/recordings/20260515_093551_user.wav new file mode 100644 index 0000000..82a29c3 Binary files /dev/null and b/data/recordings/20260515_093551_user.wav differ diff --git a/data/recordings/20260515_093554_robot.wav b/data/recordings/20260515_093554_robot.wav new file mode 100644 index 0000000..429b526 Binary files /dev/null and b/data/recordings/20260515_093554_robot.wav differ diff --git a/data/recordings/20260515_093554_user.wav b/data/recordings/20260515_093554_user.wav new file mode 100644 index 0000000..37b9484 Binary files /dev/null and b/data/recordings/20260515_093554_user.wav differ diff --git a/data/recordings/20260515_093601_robot.wav b/data/recordings/20260515_093601_robot.wav new file mode 100644 index 0000000..b0caf52 Binary files /dev/null and b/data/recordings/20260515_093601_robot.wav differ diff --git a/data/recordings/20260515_093601_user.wav b/data/recordings/20260515_093601_user.wav new file mode 100644 index 0000000..0a1fb6b Binary files /dev/null and b/data/recordings/20260515_093601_user.wav differ diff --git a/data/recordings/20260515_093602_robot.wav b/data/recordings/20260515_093602_robot.wav new file mode 100644 index 0000000..3fb955c Binary files /dev/null and b/data/recordings/20260515_093602_robot.wav differ diff --git a/data/recordings/20260515_093602_user.wav b/data/recordings/20260515_093602_user.wav new file mode 100644 index 0000000..d73bf00 Binary files /dev/null and b/data/recordings/20260515_093602_user.wav differ diff --git a/data/recordings/20260515_093603_user.wav b/data/recordings/20260515_093603_user.wav new file mode 100644 index 0000000..d720ea0 Binary files /dev/null and b/data/recordings/20260515_093603_user.wav differ diff --git a/data/recordings/20260515_093607_robot.wav b/data/recordings/20260515_093607_robot.wav new file mode 100644 index 0000000..a2a903f Binary files /dev/null and b/data/recordings/20260515_093607_robot.wav differ diff --git a/data/recordings/20260515_093607_user.wav b/data/recordings/20260515_093607_user.wav new file mode 100644 index 0000000..6e89433 Binary files /dev/null and b/data/recordings/20260515_093607_user.wav differ diff --git a/data/recordings/20260515_093618_user.wav b/data/recordings/20260515_093618_user.wav new file mode 100644 index 0000000..7326f59 Binary files /dev/null and b/data/recordings/20260515_093618_user.wav differ diff --git a/data/recordings/20260515_093620_user.wav b/data/recordings/20260515_093620_user.wav new file mode 100644 index 0000000..746369f Binary files /dev/null and b/data/recordings/20260515_093620_user.wav differ diff --git a/data/recordings/20260515_093703_robot.wav b/data/recordings/20260515_093703_robot.wav new file mode 100644 index 0000000..40bc4da Binary files /dev/null and b/data/recordings/20260515_093703_robot.wav differ diff --git a/data/recordings/20260515_093703_user.wav b/data/recordings/20260515_093703_user.wav new file mode 100644 index 0000000..13aa386 Binary files /dev/null and b/data/recordings/20260515_093703_user.wav differ diff --git a/data/recordings/20260515_093707_robot.wav b/data/recordings/20260515_093707_robot.wav new file mode 100644 index 0000000..9420900 Binary files /dev/null and b/data/recordings/20260515_093707_robot.wav differ diff --git a/data/recordings/20260515_093707_user.wav b/data/recordings/20260515_093707_user.wav new file mode 100644 index 0000000..10d2b31 Binary files /dev/null and b/data/recordings/20260515_093707_user.wav differ diff --git a/data/recordings/20260515_093711_robot.wav b/data/recordings/20260515_093711_robot.wav new file mode 100644 index 0000000..5033dac Binary files /dev/null and b/data/recordings/20260515_093711_robot.wav differ diff --git a/data/recordings/20260515_093711_user.wav b/data/recordings/20260515_093711_user.wav new file mode 100644 index 0000000..9b41ca1 Binary files /dev/null and b/data/recordings/20260515_093711_user.wav differ diff --git a/data/recordings/20260515_093715_user.wav b/data/recordings/20260515_093715_user.wav new file mode 100644 index 0000000..0e1ed5f Binary files /dev/null and b/data/recordings/20260515_093715_user.wav differ diff --git a/data/recordings/20260515_093729_robot.wav b/data/recordings/20260515_093729_robot.wav new file mode 100644 index 0000000..e41353e Binary files /dev/null and b/data/recordings/20260515_093729_robot.wav differ diff --git a/data/recordings/20260515_093729_user.wav b/data/recordings/20260515_093729_user.wav new file mode 100644 index 0000000..f830ab1 Binary files /dev/null and b/data/recordings/20260515_093729_user.wav differ diff --git a/data/recordings/20260515_093735_robot.wav b/data/recordings/20260515_093735_robot.wav new file mode 100644 index 0000000..f535ddd Binary files /dev/null and b/data/recordings/20260515_093735_robot.wav differ diff --git a/data/recordings/20260515_093735_user.wav b/data/recordings/20260515_093735_user.wav new file mode 100644 index 0000000..8699de5 Binary files /dev/null and b/data/recordings/20260515_093735_user.wav differ diff --git a/data/recordings/20260515_093808_robot.wav b/data/recordings/20260515_093808_robot.wav new file mode 100644 index 0000000..6eb5ff2 Binary files /dev/null and b/data/recordings/20260515_093808_robot.wav differ diff --git a/data/recordings/20260515_093808_user.wav b/data/recordings/20260515_093808_user.wav new file mode 100644 index 0000000..5ac915a Binary files /dev/null and b/data/recordings/20260515_093808_user.wav differ diff --git a/data/recordings/20260515_093812_robot.wav b/data/recordings/20260515_093812_robot.wav new file mode 100644 index 0000000..5a5e12b Binary files /dev/null and b/data/recordings/20260515_093812_robot.wav differ diff --git a/data/recordings/20260515_093812_user.wav b/data/recordings/20260515_093812_user.wav new file mode 100644 index 0000000..993d3e8 Binary files /dev/null and b/data/recordings/20260515_093812_user.wav differ diff --git a/data/recordings/20260515_093820_user.wav b/data/recordings/20260515_093820_user.wav new file mode 100644 index 0000000..74d7995 Binary files /dev/null and b/data/recordings/20260515_093820_user.wav differ diff --git a/data/recordings/20260515_093833_robot.wav b/data/recordings/20260515_093833_robot.wav new file mode 100644 index 0000000..5c11c7f Binary files /dev/null and b/data/recordings/20260515_093833_robot.wav differ diff --git a/data/recordings/20260515_093833_user.wav b/data/recordings/20260515_093833_user.wav new file mode 100644 index 0000000..11b871c Binary files /dev/null and b/data/recordings/20260515_093833_user.wav differ diff --git a/data/recordings/20260515_093838_robot.wav b/data/recordings/20260515_093838_robot.wav new file mode 100644 index 0000000..4eb8928 Binary files /dev/null and b/data/recordings/20260515_093838_robot.wav differ diff --git a/data/recordings/20260515_093838_user.wav b/data/recordings/20260515_093838_user.wav new file mode 100644 index 0000000..d63e136 Binary files /dev/null and b/data/recordings/20260515_093838_user.wav differ diff --git a/data/recordings/20260515_093841_user.wav b/data/recordings/20260515_093841_user.wav new file mode 100644 index 0000000..68a225e Binary files /dev/null and b/data/recordings/20260515_093841_user.wav differ diff --git a/data/recordings/20260515_093853_robot.wav b/data/recordings/20260515_093853_robot.wav new file mode 100644 index 0000000..a2b8007 Binary files /dev/null and b/data/recordings/20260515_093853_robot.wav differ diff --git a/data/recordings/20260515_093853_user.wav b/data/recordings/20260515_093853_user.wav new file mode 100644 index 0000000..3e9b7f6 Binary files /dev/null and b/data/recordings/20260515_093853_user.wav differ diff --git a/data/recordings/20260515_093856_user.wav b/data/recordings/20260515_093856_user.wav new file mode 100644 index 0000000..034500f Binary files /dev/null and b/data/recordings/20260515_093856_user.wav differ diff --git a/data/recordings/20260515_093858_robot.wav b/data/recordings/20260515_093858_robot.wav new file mode 100644 index 0000000..9b6a56b Binary files /dev/null and b/data/recordings/20260515_093858_robot.wav differ diff --git a/data/recordings/20260515_093858_user.wav b/data/recordings/20260515_093858_user.wav new file mode 100644 index 0000000..e557414 Binary files /dev/null and b/data/recordings/20260515_093858_user.wav differ diff --git a/data/recordings/20260515_093901_robot.wav b/data/recordings/20260515_093901_robot.wav new file mode 100644 index 0000000..3c43431 Binary files /dev/null and b/data/recordings/20260515_093901_robot.wav differ diff --git a/data/recordings/20260515_093901_user.wav b/data/recordings/20260515_093901_user.wav new file mode 100644 index 0000000..1b5bd68 Binary files /dev/null and b/data/recordings/20260515_093901_user.wav differ diff --git a/data/recordings/20260515_093907_robot.wav b/data/recordings/20260515_093907_robot.wav new file mode 100644 index 0000000..7df80a2 Binary files /dev/null and b/data/recordings/20260515_093907_robot.wav differ diff --git a/data/recordings/20260515_093907_user.wav b/data/recordings/20260515_093907_user.wav new file mode 100644 index 0000000..eaef7a0 Binary files /dev/null and b/data/recordings/20260515_093907_user.wav differ diff --git a/data/recordings/20260515_094006_robot.wav b/data/recordings/20260515_094006_robot.wav new file mode 100644 index 0000000..6a5bb60 Binary files /dev/null and b/data/recordings/20260515_094006_robot.wav differ diff --git a/data/recordings/20260515_094006_user.wav b/data/recordings/20260515_094006_user.wav new file mode 100644 index 0000000..fd628f3 Binary files /dev/null and b/data/recordings/20260515_094006_user.wav differ diff --git a/data/recordings/20260515_094013_robot.wav b/data/recordings/20260515_094013_robot.wav new file mode 100644 index 0000000..da3be67 Binary files /dev/null and b/data/recordings/20260515_094013_robot.wav differ diff --git a/data/recordings/20260515_094013_user.wav b/data/recordings/20260515_094013_user.wav new file mode 100644 index 0000000..748f5dc Binary files /dev/null and b/data/recordings/20260515_094013_user.wav differ diff --git a/data/recordings/20260515_094024_robot.wav b/data/recordings/20260515_094024_robot.wav new file mode 100644 index 0000000..949158f Binary files /dev/null and b/data/recordings/20260515_094024_robot.wav differ diff --git a/data/recordings/20260515_094024_user.wav b/data/recordings/20260515_094024_user.wav new file mode 100644 index 0000000..5c6c09f Binary files /dev/null and b/data/recordings/20260515_094024_user.wav differ diff --git a/data/recordings/20260515_094030_robot.wav b/data/recordings/20260515_094030_robot.wav new file mode 100644 index 0000000..cbd4311 Binary files /dev/null and b/data/recordings/20260515_094030_robot.wav differ diff --git a/data/recordings/20260515_094030_user.wav b/data/recordings/20260515_094030_user.wav new file mode 100644 index 0000000..4549d0e Binary files /dev/null and b/data/recordings/20260515_094030_user.wav differ diff --git a/data/recordings/20260515_094033_user.wav b/data/recordings/20260515_094033_user.wav new file mode 100644 index 0000000..3eb6440 Binary files /dev/null and b/data/recordings/20260515_094033_user.wav differ diff --git a/data/recordings/20260515_094034_user.wav b/data/recordings/20260515_094034_user.wav new file mode 100644 index 0000000..657b3f2 Binary files /dev/null and b/data/recordings/20260515_094034_user.wav differ diff --git a/data/recordings/20260515_094035_user.wav b/data/recordings/20260515_094035_user.wav new file mode 100644 index 0000000..9a00160 Binary files /dev/null and b/data/recordings/20260515_094035_user.wav differ diff --git a/data/recordings/20260515_094037_user.wav b/data/recordings/20260515_094037_user.wav new file mode 100644 index 0000000..6765f25 Binary files /dev/null and b/data/recordings/20260515_094037_user.wav differ diff --git a/data/recordings/20260515_094041_robot.wav b/data/recordings/20260515_094041_robot.wav new file mode 100644 index 0000000..a66232b Binary files /dev/null and b/data/recordings/20260515_094041_robot.wav differ diff --git a/data/recordings/20260515_094041_user.wav b/data/recordings/20260515_094041_user.wav new file mode 100644 index 0000000..86ad36b Binary files /dev/null and b/data/recordings/20260515_094041_user.wav differ diff --git a/data/recordings/20260515_094048_user.wav b/data/recordings/20260515_094048_user.wav new file mode 100644 index 0000000..e54aa91 Binary files /dev/null and b/data/recordings/20260515_094048_user.wav differ diff --git a/data/recordings/20260515_094049_robot.wav b/data/recordings/20260515_094049_robot.wav new file mode 100644 index 0000000..1e6eec9 Binary files /dev/null and b/data/recordings/20260515_094049_robot.wav differ diff --git a/data/recordings/20260515_094049_user.wav b/data/recordings/20260515_094049_user.wav new file mode 100644 index 0000000..4d2b468 Binary files /dev/null and b/data/recordings/20260515_094049_user.wav differ diff --git a/data/recordings/20260515_094058_user.wav b/data/recordings/20260515_094058_user.wav new file mode 100644 index 0000000..6014726 Binary files /dev/null and b/data/recordings/20260515_094058_user.wav differ diff --git a/data/recordings/20260515_094059_user.wav b/data/recordings/20260515_094059_user.wav new file mode 100644 index 0000000..902a655 Binary files /dev/null and b/data/recordings/20260515_094059_user.wav differ diff --git a/data/recordings/20260515_094120_robot.wav b/data/recordings/20260515_094120_robot.wav new file mode 100644 index 0000000..83f3990 Binary files /dev/null and b/data/recordings/20260515_094120_robot.wav differ diff --git a/data/recordings/20260515_094120_user.wav b/data/recordings/20260515_094120_user.wav new file mode 100644 index 0000000..ae962d4 Binary files /dev/null and b/data/recordings/20260515_094120_user.wav differ diff --git a/data/recordings/20260515_094127_robot.wav b/data/recordings/20260515_094127_robot.wav new file mode 100644 index 0000000..04d3b44 Binary files /dev/null and b/data/recordings/20260515_094127_robot.wav differ diff --git a/data/recordings/20260515_094127_user.wav b/data/recordings/20260515_094127_user.wav new file mode 100644 index 0000000..2c89df4 Binary files /dev/null and b/data/recordings/20260515_094127_user.wav differ diff --git a/data/recordings/20260515_094132_user.wav b/data/recordings/20260515_094132_user.wav new file mode 100644 index 0000000..97a69b9 Binary files /dev/null and b/data/recordings/20260515_094132_user.wav differ diff --git a/data/recordings/20260515_094134_robot.wav b/data/recordings/20260515_094134_robot.wav new file mode 100644 index 0000000..027e643 Binary files /dev/null and b/data/recordings/20260515_094134_robot.wav differ diff --git a/data/recordings/20260515_094134_user.wav b/data/recordings/20260515_094134_user.wav new file mode 100644 index 0000000..4c944d9 Binary files /dev/null and b/data/recordings/20260515_094134_user.wav differ diff --git a/data/recordings/20260515_094153_robot.wav b/data/recordings/20260515_094153_robot.wav new file mode 100644 index 0000000..cef1231 Binary files /dev/null and b/data/recordings/20260515_094153_robot.wav differ diff --git a/data/recordings/20260515_094153_user.wav b/data/recordings/20260515_094153_user.wav new file mode 100644 index 0000000..3b72525 Binary files /dev/null and b/data/recordings/20260515_094153_user.wav differ diff --git a/data/recordings/20260515_094154_robot.wav b/data/recordings/20260515_094154_robot.wav new file mode 100644 index 0000000..dfc2199 Binary files /dev/null and b/data/recordings/20260515_094154_robot.wav differ diff --git a/data/recordings/20260515_094154_user.wav b/data/recordings/20260515_094154_user.wav new file mode 100644 index 0000000..1345d0f Binary files /dev/null and b/data/recordings/20260515_094154_user.wav differ diff --git a/data/recordings/20260515_094159_user.wav b/data/recordings/20260515_094159_user.wav new file mode 100644 index 0000000..910c682 Binary files /dev/null and b/data/recordings/20260515_094159_user.wav differ diff --git a/data/recordings/20260515_094209_robot.wav b/data/recordings/20260515_094209_robot.wav new file mode 100644 index 0000000..be7276a Binary files /dev/null and b/data/recordings/20260515_094209_robot.wav differ diff --git a/data/recordings/20260515_094209_user.wav b/data/recordings/20260515_094209_user.wav new file mode 100644 index 0000000..9c2bd9d Binary files /dev/null and b/data/recordings/20260515_094209_user.wav differ diff --git a/data/recordings/20260515_094216_user.wav b/data/recordings/20260515_094216_user.wav new file mode 100644 index 0000000..449049a Binary files /dev/null and b/data/recordings/20260515_094216_user.wav differ diff --git a/data/recordings/20260515_094230_user.wav b/data/recordings/20260515_094230_user.wav new file mode 100644 index 0000000..d640429 Binary files /dev/null and b/data/recordings/20260515_094230_user.wav differ diff --git a/data/recordings/20260515_094234_user.wav b/data/recordings/20260515_094234_user.wav new file mode 100644 index 0000000..ab41365 Binary files /dev/null and b/data/recordings/20260515_094234_user.wav differ diff --git a/data/recordings/20260515_094236_user.wav b/data/recordings/20260515_094236_user.wav new file mode 100644 index 0000000..d23563d Binary files /dev/null and b/data/recordings/20260515_094236_user.wav differ diff --git a/data/recordings/20260515_094244_robot.wav b/data/recordings/20260515_094244_robot.wav new file mode 100644 index 0000000..c24d0bf Binary files /dev/null and b/data/recordings/20260515_094244_robot.wav differ diff --git a/data/recordings/20260515_094244_user.wav b/data/recordings/20260515_094244_user.wav new file mode 100644 index 0000000..ba458cb Binary files /dev/null and b/data/recordings/20260515_094244_user.wav differ diff --git a/data/recordings/20260515_094256_user.wav b/data/recordings/20260515_094256_user.wav new file mode 100644 index 0000000..aa16666 Binary files /dev/null and b/data/recordings/20260515_094256_user.wav differ diff --git a/data/recordings/20260515_094324_user.wav b/data/recordings/20260515_094324_user.wav new file mode 100644 index 0000000..6d861ef Binary files /dev/null and b/data/recordings/20260515_094324_user.wav differ diff --git a/data/recordings/20260515_094402_user.wav b/data/recordings/20260515_094402_user.wav new file mode 100644 index 0000000..8cd479c Binary files /dev/null and b/data/recordings/20260515_094402_user.wav differ diff --git a/data/recordings/20260515_094416_robot.wav b/data/recordings/20260515_094416_robot.wav new file mode 100644 index 0000000..a4f6603 Binary files /dev/null and b/data/recordings/20260515_094416_robot.wav differ diff --git a/data/recordings/20260515_094416_user.wav b/data/recordings/20260515_094416_user.wav new file mode 100644 index 0000000..a276180 Binary files /dev/null and b/data/recordings/20260515_094416_user.wav differ diff --git a/data/recordings/20260515_094421_robot.wav b/data/recordings/20260515_094421_robot.wav new file mode 100644 index 0000000..3e8ee7e Binary files /dev/null and b/data/recordings/20260515_094421_robot.wav differ diff --git a/data/recordings/20260515_094421_user.wav b/data/recordings/20260515_094421_user.wav new file mode 100644 index 0000000..712d6cd Binary files /dev/null and b/data/recordings/20260515_094421_user.wav differ diff --git a/data/recordings/20260515_094423_user.wav b/data/recordings/20260515_094423_user.wav new file mode 100644 index 0000000..fe8e857 Binary files /dev/null and b/data/recordings/20260515_094423_user.wav differ diff --git a/data/recordings/20260515_094424_user.wav b/data/recordings/20260515_094424_user.wav new file mode 100644 index 0000000..68e95ca Binary files /dev/null and b/data/recordings/20260515_094424_user.wav differ diff --git a/data/recordings/20260515_094457_user.wav b/data/recordings/20260515_094457_user.wav new file mode 100644 index 0000000..de06326 Binary files /dev/null and b/data/recordings/20260515_094457_user.wav differ diff --git a/data/recordings/20260515_094504_robot.wav b/data/recordings/20260515_094504_robot.wav new file mode 100644 index 0000000..5d3e048 Binary files /dev/null and b/data/recordings/20260515_094504_robot.wav differ diff --git a/data/recordings/20260515_094504_user.wav b/data/recordings/20260515_094504_user.wav new file mode 100644 index 0000000..8b186ae Binary files /dev/null and b/data/recordings/20260515_094504_user.wav differ diff --git a/data/recordings/20260515_094526_robot.wav b/data/recordings/20260515_094526_robot.wav new file mode 100644 index 0000000..de9e13a Binary files /dev/null and b/data/recordings/20260515_094526_robot.wav differ diff --git a/data/recordings/20260515_094526_user.wav b/data/recordings/20260515_094526_user.wav new file mode 100644 index 0000000..733a5d7 Binary files /dev/null and b/data/recordings/20260515_094526_user.wav differ diff --git a/data/recordings/20260515_094529_robot.wav b/data/recordings/20260515_094529_robot.wav new file mode 100644 index 0000000..d8dc483 Binary files /dev/null and b/data/recordings/20260515_094529_robot.wav differ diff --git a/data/recordings/20260515_094529_user.wav b/data/recordings/20260515_094529_user.wav new file mode 100644 index 0000000..eb3d0b8 Binary files /dev/null and b/data/recordings/20260515_094529_user.wav differ diff --git a/data/recordings/20260515_094532_user.wav b/data/recordings/20260515_094532_user.wav new file mode 100644 index 0000000..3213de3 Binary files /dev/null and b/data/recordings/20260515_094532_user.wav differ diff --git a/data/recordings/20260515_094616_robot.wav b/data/recordings/20260515_094616_robot.wav new file mode 100644 index 0000000..122dc76 Binary files /dev/null and b/data/recordings/20260515_094616_robot.wav differ diff --git a/data/recordings/20260515_094616_user.wav b/data/recordings/20260515_094616_user.wav new file mode 100644 index 0000000..073a1b7 Binary files /dev/null and b/data/recordings/20260515_094616_user.wav differ diff --git a/data/recordings/20260515_094627_user.wav b/data/recordings/20260515_094627_user.wav new file mode 100644 index 0000000..47ec685 Binary files /dev/null and b/data/recordings/20260515_094627_user.wav differ diff --git a/data/recordings/20260515_094637_robot.wav b/data/recordings/20260515_094637_robot.wav new file mode 100644 index 0000000..a0d234d Binary files /dev/null and b/data/recordings/20260515_094637_robot.wav differ diff --git a/data/recordings/20260515_094637_user.wav b/data/recordings/20260515_094637_user.wav new file mode 100644 index 0000000..45497fc Binary files /dev/null and b/data/recordings/20260515_094637_user.wav differ diff --git a/data/recordings/20260515_094647_user.wav b/data/recordings/20260515_094647_user.wav new file mode 100644 index 0000000..fc2cf3a Binary files /dev/null and b/data/recordings/20260515_094647_user.wav differ diff --git a/data/recordings/20260515_094650_user.wav b/data/recordings/20260515_094650_user.wav new file mode 100644 index 0000000..b5e2e46 Binary files /dev/null and b/data/recordings/20260515_094650_user.wav differ diff --git a/data/recordings/20260515_094713_user.wav b/data/recordings/20260515_094713_user.wav new file mode 100644 index 0000000..3d06f01 Binary files /dev/null and b/data/recordings/20260515_094713_user.wav differ diff --git a/data/recordings/20260515_094714_robot.wav b/data/recordings/20260515_094714_robot.wav new file mode 100644 index 0000000..3c8b743 Binary files /dev/null and b/data/recordings/20260515_094714_robot.wav differ diff --git a/data/recordings/20260515_094714_user.wav b/data/recordings/20260515_094714_user.wav new file mode 100644 index 0000000..fee24a1 Binary files /dev/null and b/data/recordings/20260515_094714_user.wav differ diff --git a/data/recordings/20260515_094724_user.wav b/data/recordings/20260515_094724_user.wav new file mode 100644 index 0000000..49f4eb6 Binary files /dev/null and b/data/recordings/20260515_094724_user.wav differ diff --git a/data/recordings/20260515_094725_robot.wav b/data/recordings/20260515_094725_robot.wav new file mode 100644 index 0000000..ead095e Binary files /dev/null and b/data/recordings/20260515_094725_robot.wav differ diff --git a/data/recordings/20260515_094725_user.wav b/data/recordings/20260515_094725_user.wav new file mode 100644 index 0000000..907ad8c Binary files /dev/null and b/data/recordings/20260515_094725_user.wav differ diff --git a/data/recordings/20260515_094738_user.wav b/data/recordings/20260515_094738_user.wav new file mode 100644 index 0000000..f09fb00 Binary files /dev/null and b/data/recordings/20260515_094738_user.wav differ diff --git a/data/recordings/20260515_094739_user.wav b/data/recordings/20260515_094739_user.wav new file mode 100644 index 0000000..decb3b5 Binary files /dev/null and b/data/recordings/20260515_094739_user.wav differ diff --git a/data/recordings/20260515_094751_robot.wav b/data/recordings/20260515_094751_robot.wav new file mode 100644 index 0000000..c0fa2bd Binary files /dev/null and b/data/recordings/20260515_094751_robot.wav differ diff --git a/data/recordings/20260515_094751_user.wav b/data/recordings/20260515_094751_user.wav new file mode 100644 index 0000000..e3c8f67 Binary files /dev/null and b/data/recordings/20260515_094751_user.wav differ diff --git a/data/recordings/20260515_094755_robot.wav b/data/recordings/20260515_094755_robot.wav new file mode 100644 index 0000000..3289488 Binary files /dev/null and b/data/recordings/20260515_094755_robot.wav differ diff --git a/data/recordings/20260515_094755_user.wav b/data/recordings/20260515_094755_user.wav new file mode 100644 index 0000000..e4bad1a Binary files /dev/null and b/data/recordings/20260515_094755_user.wav differ diff --git a/data/recordings/20260515_094757_user.wav b/data/recordings/20260515_094757_user.wav new file mode 100644 index 0000000..f727ad3 Binary files /dev/null and b/data/recordings/20260515_094757_user.wav differ diff --git a/data/recordings/20260515_094758_user.wav b/data/recordings/20260515_094758_user.wav new file mode 100644 index 0000000..28f21f3 Binary files /dev/null and b/data/recordings/20260515_094758_user.wav differ diff --git a/data/recordings/20260515_094831_user.wav b/data/recordings/20260515_094831_user.wav new file mode 100644 index 0000000..bc44e95 Binary files /dev/null and b/data/recordings/20260515_094831_user.wav differ diff --git a/data/recordings/20260515_094834_user.wav b/data/recordings/20260515_094834_user.wav new file mode 100644 index 0000000..fa08f15 Binary files /dev/null and b/data/recordings/20260515_094834_user.wav differ diff --git a/data/recordings/20260515_094839_robot.wav b/data/recordings/20260515_094839_robot.wav new file mode 100644 index 0000000..af3e61a Binary files /dev/null and b/data/recordings/20260515_094839_robot.wav differ diff --git a/data/recordings/20260515_094839_user.wav b/data/recordings/20260515_094839_user.wav new file mode 100644 index 0000000..959a7d5 Binary files /dev/null and b/data/recordings/20260515_094839_user.wav differ diff --git a/data/recordings/20260515_094843_robot.wav b/data/recordings/20260515_094843_robot.wav new file mode 100644 index 0000000..7c95e60 Binary files /dev/null and b/data/recordings/20260515_094843_robot.wav differ diff --git a/data/recordings/20260515_094843_user.wav b/data/recordings/20260515_094843_user.wav new file mode 100644 index 0000000..69169e3 Binary files /dev/null and b/data/recordings/20260515_094843_user.wav differ diff --git a/data/recordings/20260515_094844_user.wav b/data/recordings/20260515_094844_user.wav new file mode 100644 index 0000000..bbcedd6 Binary files /dev/null and b/data/recordings/20260515_094844_user.wav differ diff --git a/data/recordings/20260515_094848_user.wav b/data/recordings/20260515_094848_user.wav new file mode 100644 index 0000000..8694783 Binary files /dev/null and b/data/recordings/20260515_094848_user.wav differ diff --git a/data/recordings/20260515_094855_user.wav b/data/recordings/20260515_094855_user.wav new file mode 100644 index 0000000..a759608 Binary files /dev/null and b/data/recordings/20260515_094855_user.wav differ diff --git a/data/recordings/20260515_094856_user.wav b/data/recordings/20260515_094856_user.wav new file mode 100644 index 0000000..76a7a48 Binary files /dev/null and b/data/recordings/20260515_094856_user.wav differ diff --git a/data/recordings/20260515_094900_user.wav b/data/recordings/20260515_094900_user.wav new file mode 100644 index 0000000..8e7eb0c Binary files /dev/null and b/data/recordings/20260515_094900_user.wav differ diff --git a/data/recordings/20260515_094907_user.wav b/data/recordings/20260515_094907_user.wav new file mode 100644 index 0000000..29db47a Binary files /dev/null and b/data/recordings/20260515_094907_user.wav differ diff --git a/data/recordings/20260515_094913_user.wav b/data/recordings/20260515_094913_user.wav new file mode 100644 index 0000000..9d0874e Binary files /dev/null and b/data/recordings/20260515_094913_user.wav differ diff --git a/data/recordings/20260515_094923_robot.wav b/data/recordings/20260515_094923_robot.wav new file mode 100644 index 0000000..cd30f1b Binary files /dev/null and b/data/recordings/20260515_094923_robot.wav differ diff --git a/data/recordings/20260515_094923_user.wav b/data/recordings/20260515_094923_user.wav new file mode 100644 index 0000000..9494b38 Binary files /dev/null and b/data/recordings/20260515_094923_user.wav differ diff --git a/data/recordings/20260515_094934_robot.wav b/data/recordings/20260515_094934_robot.wav new file mode 100644 index 0000000..5cf02fd Binary files /dev/null and b/data/recordings/20260515_094934_robot.wav differ diff --git a/data/recordings/20260515_094934_user.wav b/data/recordings/20260515_094934_user.wav new file mode 100644 index 0000000..9ecee0b Binary files /dev/null and b/data/recordings/20260515_094934_user.wav differ diff --git a/data/recordings/20260515_094935_user.wav b/data/recordings/20260515_094935_user.wav new file mode 100644 index 0000000..b270982 Binary files /dev/null and b/data/recordings/20260515_094935_user.wav differ diff --git a/data/recordings/20260515_094938_robot.wav b/data/recordings/20260515_094938_robot.wav new file mode 100644 index 0000000..a2d50f2 Binary files /dev/null and b/data/recordings/20260515_094938_robot.wav differ diff --git a/data/recordings/20260515_094938_user.wav b/data/recordings/20260515_094938_user.wav new file mode 100644 index 0000000..7cbc967 Binary files /dev/null and b/data/recordings/20260515_094938_user.wav differ diff --git a/data/recordings/20260515_094955_robot.wav b/data/recordings/20260515_094955_robot.wav new file mode 100644 index 0000000..c7addc6 Binary files /dev/null and b/data/recordings/20260515_094955_robot.wav differ diff --git a/data/recordings/20260515_094955_user.wav b/data/recordings/20260515_094955_user.wav new file mode 100644 index 0000000..5d502f6 Binary files /dev/null and b/data/recordings/20260515_094955_user.wav differ diff --git a/data/recordings/20260515_095004_robot.wav b/data/recordings/20260515_095004_robot.wav new file mode 100644 index 0000000..4e76d38 Binary files /dev/null and b/data/recordings/20260515_095004_robot.wav differ diff --git a/data/recordings/20260515_095004_user.wav b/data/recordings/20260515_095004_user.wav new file mode 100644 index 0000000..3729fd1 Binary files /dev/null and b/data/recordings/20260515_095004_user.wav differ diff --git a/data/recordings/20260515_095005_user.wav b/data/recordings/20260515_095005_user.wav new file mode 100644 index 0000000..a4cb9aa Binary files /dev/null and b/data/recordings/20260515_095005_user.wav differ diff --git a/data/recordings/20260515_095006_user.wav b/data/recordings/20260515_095006_user.wav new file mode 100644 index 0000000..a51c093 Binary files /dev/null and b/data/recordings/20260515_095006_user.wav differ diff --git a/data/recordings/20260515_095018_user.wav b/data/recordings/20260515_095018_user.wav new file mode 100644 index 0000000..fa17db8 Binary files /dev/null and b/data/recordings/20260515_095018_user.wav differ diff --git a/data/recordings/20260515_095020_robot.wav b/data/recordings/20260515_095020_robot.wav new file mode 100644 index 0000000..102850a Binary files /dev/null and b/data/recordings/20260515_095020_robot.wav differ diff --git a/data/recordings/20260515_095020_user.wav b/data/recordings/20260515_095020_user.wav new file mode 100644 index 0000000..03635a2 Binary files /dev/null and b/data/recordings/20260515_095020_user.wav differ diff --git a/data/recordings/20260515_095028_robot.wav b/data/recordings/20260515_095028_robot.wav new file mode 100644 index 0000000..565f52a Binary files /dev/null and b/data/recordings/20260515_095028_robot.wav differ diff --git a/data/recordings/20260515_095028_user.wav b/data/recordings/20260515_095028_user.wav new file mode 100644 index 0000000..791eb4f Binary files /dev/null and b/data/recordings/20260515_095028_user.wav differ diff --git a/data/recordings/20260515_095029_user.wav b/data/recordings/20260515_095029_user.wav new file mode 100644 index 0000000..b0c4c1f Binary files /dev/null and b/data/recordings/20260515_095029_user.wav differ diff --git a/data/recordings/20260515_095031_user.wav b/data/recordings/20260515_095031_user.wav new file mode 100644 index 0000000..48bbbac Binary files /dev/null and b/data/recordings/20260515_095031_user.wav differ diff --git a/data/recordings/20260515_095042_robot.wav b/data/recordings/20260515_095042_robot.wav new file mode 100644 index 0000000..4d960a6 Binary files /dev/null and b/data/recordings/20260515_095042_robot.wav differ diff --git a/data/recordings/20260515_095042_user.wav b/data/recordings/20260515_095042_user.wav new file mode 100644 index 0000000..2075b69 Binary files /dev/null and b/data/recordings/20260515_095042_user.wav differ diff --git a/data/recordings/20260515_095047_robot.wav b/data/recordings/20260515_095047_robot.wav new file mode 100644 index 0000000..e82151a Binary files /dev/null and b/data/recordings/20260515_095047_robot.wav differ diff --git a/data/recordings/20260515_095047_user.wav b/data/recordings/20260515_095047_user.wav new file mode 100644 index 0000000..7c1a192 Binary files /dev/null and b/data/recordings/20260515_095047_user.wav differ diff --git a/data/recordings/20260515_095050_robot.wav b/data/recordings/20260515_095050_robot.wav new file mode 100644 index 0000000..fe5d006 Binary files /dev/null and b/data/recordings/20260515_095050_robot.wav differ diff --git a/data/recordings/20260515_095050_user.wav b/data/recordings/20260515_095050_user.wav new file mode 100644 index 0000000..1604bec Binary files /dev/null and b/data/recordings/20260515_095050_user.wav differ diff --git a/data/recordings/20260515_095107_robot.wav b/data/recordings/20260515_095107_robot.wav new file mode 100644 index 0000000..f91ba83 Binary files /dev/null and b/data/recordings/20260515_095107_robot.wav differ diff --git a/data/recordings/20260515_095107_user.wav b/data/recordings/20260515_095107_user.wav new file mode 100644 index 0000000..7dea71f Binary files /dev/null and b/data/recordings/20260515_095107_user.wav differ diff --git a/data/recordings/20260515_095108_robot.wav b/data/recordings/20260515_095108_robot.wav new file mode 100644 index 0000000..788654d Binary files /dev/null and b/data/recordings/20260515_095108_robot.wav differ diff --git a/data/recordings/20260515_095108_user.wav b/data/recordings/20260515_095108_user.wav new file mode 100644 index 0000000..263ad0e Binary files /dev/null and b/data/recordings/20260515_095108_user.wav differ diff --git a/data/recordings/20260515_095112_robot.wav b/data/recordings/20260515_095112_robot.wav new file mode 100644 index 0000000..7f9a2d2 Binary files /dev/null and b/data/recordings/20260515_095112_robot.wav differ diff --git a/data/recordings/20260515_095112_user.wav b/data/recordings/20260515_095112_user.wav new file mode 100644 index 0000000..e95f89e Binary files /dev/null and b/data/recordings/20260515_095112_user.wav differ diff --git a/data/recordings/20260515_095121_robot.wav b/data/recordings/20260515_095121_robot.wav new file mode 100644 index 0000000..7cde510 Binary files /dev/null and b/data/recordings/20260515_095121_robot.wav differ diff --git a/data/recordings/20260515_095121_user.wav b/data/recordings/20260515_095121_user.wav new file mode 100644 index 0000000..c728b26 Binary files /dev/null and b/data/recordings/20260515_095121_user.wav differ diff --git a/data/recordings/20260515_095123_user.wav b/data/recordings/20260515_095123_user.wav new file mode 100644 index 0000000..c3b7984 Binary files /dev/null and b/data/recordings/20260515_095123_user.wav differ diff --git a/data/recordings/20260515_095124_user.wav b/data/recordings/20260515_095124_user.wav new file mode 100644 index 0000000..bff07dc Binary files /dev/null and b/data/recordings/20260515_095124_user.wav differ diff --git a/data/recordings/20260515_095135_robot.wav b/data/recordings/20260515_095135_robot.wav new file mode 100644 index 0000000..8bc99eb Binary files /dev/null and b/data/recordings/20260515_095135_robot.wav differ diff --git a/data/recordings/20260515_095135_user.wav b/data/recordings/20260515_095135_user.wav new file mode 100644 index 0000000..eb64f4b Binary files /dev/null and b/data/recordings/20260515_095135_user.wav differ diff --git a/data/recordings/20260515_095148_user.wav b/data/recordings/20260515_095148_user.wav new file mode 100644 index 0000000..e0b9577 Binary files /dev/null and b/data/recordings/20260515_095148_user.wav differ diff --git a/data/recordings/20260515_095150_user.wav b/data/recordings/20260515_095150_user.wav new file mode 100644 index 0000000..49e46ef Binary files /dev/null and b/data/recordings/20260515_095150_user.wav differ diff --git a/data/recordings/20260515_095217_user.wav b/data/recordings/20260515_095217_user.wav new file mode 100644 index 0000000..42d19b5 Binary files /dev/null and b/data/recordings/20260515_095217_user.wav differ diff --git a/data/recordings/20260515_095223_user.wav b/data/recordings/20260515_095223_user.wav new file mode 100644 index 0000000..acd3027 Binary files /dev/null and b/data/recordings/20260515_095223_user.wav differ diff --git a/data/recordings/20260515_095227_user.wav b/data/recordings/20260515_095227_user.wav new file mode 100644 index 0000000..81cbf60 Binary files /dev/null and b/data/recordings/20260515_095227_user.wav differ diff --git a/data/recordings/20260515_095228_robot.wav b/data/recordings/20260515_095228_robot.wav new file mode 100644 index 0000000..a182a64 Binary files /dev/null and b/data/recordings/20260515_095228_robot.wav differ diff --git a/data/recordings/20260515_095228_user.wav b/data/recordings/20260515_095228_user.wav new file mode 100644 index 0000000..59c601d Binary files /dev/null and b/data/recordings/20260515_095228_user.wav differ diff --git a/data/recordings/20260515_095240_robot.wav b/data/recordings/20260515_095240_robot.wav new file mode 100644 index 0000000..6ffa9db Binary files /dev/null and b/data/recordings/20260515_095240_robot.wav differ diff --git a/data/recordings/20260515_095240_user.wav b/data/recordings/20260515_095240_user.wav new file mode 100644 index 0000000..65a1fe2 Binary files /dev/null and b/data/recordings/20260515_095240_user.wav differ diff --git a/data/recordings/20260515_095245_robot.wav b/data/recordings/20260515_095245_robot.wav new file mode 100644 index 0000000..da44be3 Binary files /dev/null and b/data/recordings/20260515_095245_robot.wav differ diff --git a/data/recordings/20260515_095245_user.wav b/data/recordings/20260515_095245_user.wav new file mode 100644 index 0000000..b5693d3 Binary files /dev/null and b/data/recordings/20260515_095245_user.wav differ diff --git a/data/recordings/20260515_095246_user.wav b/data/recordings/20260515_095246_user.wav new file mode 100644 index 0000000..06353a7 Binary files /dev/null and b/data/recordings/20260515_095246_user.wav differ diff --git a/data/recordings/20260515_095250_user.wav b/data/recordings/20260515_095250_user.wav new file mode 100644 index 0000000..8224e4d Binary files /dev/null and b/data/recordings/20260515_095250_user.wav differ diff --git a/data/recordings/20260515_095252_robot.wav b/data/recordings/20260515_095252_robot.wav new file mode 100644 index 0000000..9ebf39f Binary files /dev/null and b/data/recordings/20260515_095252_robot.wav differ diff --git a/data/recordings/20260515_095252_user.wav b/data/recordings/20260515_095252_user.wav new file mode 100644 index 0000000..1658cce Binary files /dev/null and b/data/recordings/20260515_095252_user.wav differ diff --git a/data/recordings/20260515_095303_user.wav b/data/recordings/20260515_095303_user.wav new file mode 100644 index 0000000..1b7ba34 Binary files /dev/null and b/data/recordings/20260515_095303_user.wav differ diff --git a/data/recordings/20260515_095311_user.wav b/data/recordings/20260515_095311_user.wav new file mode 100644 index 0000000..15168cb Binary files /dev/null and b/data/recordings/20260515_095311_user.wav differ diff --git a/data/recordings/20260515_095312_user.wav b/data/recordings/20260515_095312_user.wav new file mode 100644 index 0000000..88d17c7 Binary files /dev/null and b/data/recordings/20260515_095312_user.wav differ diff --git a/data/recordings/20260515_095319_user.wav b/data/recordings/20260515_095319_user.wav new file mode 100644 index 0000000..51d6f25 Binary files /dev/null and b/data/recordings/20260515_095319_user.wav differ diff --git a/data/recordings/20260515_095323_user.wav b/data/recordings/20260515_095323_user.wav new file mode 100644 index 0000000..b347d1d Binary files /dev/null and b/data/recordings/20260515_095323_user.wav differ diff --git a/data/recordings/20260515_095327_user.wav b/data/recordings/20260515_095327_user.wav new file mode 100644 index 0000000..62c5616 Binary files /dev/null and b/data/recordings/20260515_095327_user.wav differ diff --git a/data/recordings/20260515_095329_user.wav b/data/recordings/20260515_095329_user.wav new file mode 100644 index 0000000..6c600ef Binary files /dev/null and b/data/recordings/20260515_095329_user.wav differ diff --git a/data/recordings/20260515_095331_user.wav b/data/recordings/20260515_095331_user.wav new file mode 100644 index 0000000..6992897 Binary files /dev/null and b/data/recordings/20260515_095331_user.wav differ diff --git a/data/recordings/20260515_095335_user.wav b/data/recordings/20260515_095335_user.wav new file mode 100644 index 0000000..4332e00 Binary files /dev/null and b/data/recordings/20260515_095335_user.wav differ diff --git a/data/recordings/20260515_095338_user.wav b/data/recordings/20260515_095338_user.wav new file mode 100644 index 0000000..660137b Binary files /dev/null and b/data/recordings/20260515_095338_user.wav differ diff --git a/data/recordings/20260515_095348_user.wav b/data/recordings/20260515_095348_user.wav new file mode 100644 index 0000000..f5fb732 Binary files /dev/null and b/data/recordings/20260515_095348_user.wav differ diff --git a/data/recordings/20260515_095349_user.wav b/data/recordings/20260515_095349_user.wav new file mode 100644 index 0000000..345d94b Binary files /dev/null and b/data/recordings/20260515_095349_user.wav differ diff --git a/data/recordings/20260515_095355_robot.wav b/data/recordings/20260515_095355_robot.wav new file mode 100644 index 0000000..68591e8 Binary files /dev/null and b/data/recordings/20260515_095355_robot.wav differ diff --git a/data/recordings/20260515_095355_user.wav b/data/recordings/20260515_095355_user.wav new file mode 100644 index 0000000..1a9305e Binary files /dev/null and b/data/recordings/20260515_095355_user.wav differ diff --git a/data/recordings/20260515_095358_robot.wav b/data/recordings/20260515_095358_robot.wav new file mode 100644 index 0000000..c38b3d6 Binary files /dev/null and b/data/recordings/20260515_095358_robot.wav differ diff --git a/data/recordings/20260515_095358_user.wav b/data/recordings/20260515_095358_user.wav new file mode 100644 index 0000000..0c4d27e Binary files /dev/null and b/data/recordings/20260515_095358_user.wav differ diff --git a/data/recordings/20260515_095404_user.wav b/data/recordings/20260515_095404_user.wav new file mode 100644 index 0000000..5ae5882 Binary files /dev/null and b/data/recordings/20260515_095404_user.wav differ diff --git a/data/recordings/20260515_095429_user.wav b/data/recordings/20260515_095429_user.wav new file mode 100644 index 0000000..aa949b5 Binary files /dev/null and b/data/recordings/20260515_095429_user.wav differ diff --git a/data/recordings/20260515_095452_robot.wav b/data/recordings/20260515_095452_robot.wav new file mode 100644 index 0000000..2cab77a Binary files /dev/null and b/data/recordings/20260515_095452_robot.wav differ diff --git a/data/recordings/20260515_095452_user.wav b/data/recordings/20260515_095452_user.wav new file mode 100644 index 0000000..6278c52 Binary files /dev/null and b/data/recordings/20260515_095452_user.wav differ diff --git a/data/recordings/20260515_095504_robot.wav b/data/recordings/20260515_095504_robot.wav new file mode 100644 index 0000000..943fe3a Binary files /dev/null and b/data/recordings/20260515_095504_robot.wav differ diff --git a/data/recordings/20260515_095504_user.wav b/data/recordings/20260515_095504_user.wav new file mode 100644 index 0000000..e150ddb Binary files /dev/null and b/data/recordings/20260515_095504_user.wav differ diff --git a/data/recordings/20260515_095516_robot.wav b/data/recordings/20260515_095516_robot.wav new file mode 100644 index 0000000..35ef363 Binary files /dev/null and b/data/recordings/20260515_095516_robot.wav differ diff --git a/data/recordings/20260515_095516_user.wav b/data/recordings/20260515_095516_user.wav new file mode 100644 index 0000000..2ac5cc0 Binary files /dev/null and b/data/recordings/20260515_095516_user.wav differ diff --git a/data/recordings/20260515_095522_robot.wav b/data/recordings/20260515_095522_robot.wav new file mode 100644 index 0000000..fe21b15 Binary files /dev/null and b/data/recordings/20260515_095522_robot.wav differ diff --git a/data/recordings/20260515_095522_user.wav b/data/recordings/20260515_095522_user.wav new file mode 100644 index 0000000..66a2fb8 Binary files /dev/null and b/data/recordings/20260515_095522_user.wav differ diff --git a/data/recordings/20260515_095524_robot.wav b/data/recordings/20260515_095524_robot.wav new file mode 100644 index 0000000..86f0df4 Binary files /dev/null and b/data/recordings/20260515_095524_robot.wav differ diff --git a/data/recordings/20260515_095524_user.wav b/data/recordings/20260515_095524_user.wav new file mode 100644 index 0000000..bf7d3c6 Binary files /dev/null and b/data/recordings/20260515_095524_user.wav differ diff --git a/data/recordings/20260515_095528_user.wav b/data/recordings/20260515_095528_user.wav new file mode 100644 index 0000000..c18ec97 Binary files /dev/null and b/data/recordings/20260515_095528_user.wav differ diff --git a/data/recordings/20260515_095530_user.wav b/data/recordings/20260515_095530_user.wav new file mode 100644 index 0000000..abd0fb5 Binary files /dev/null and b/data/recordings/20260515_095530_user.wav differ diff --git a/data/recordings/20260515_095750_user.wav b/data/recordings/20260515_095750_user.wav new file mode 100644 index 0000000..fee9882 Binary files /dev/null and b/data/recordings/20260515_095750_user.wav differ diff --git a/data/recordings/20260515_100406_user.wav b/data/recordings/20260515_100406_user.wav new file mode 100644 index 0000000..f7d7fa9 Binary files /dev/null and b/data/recordings/20260515_100406_user.wav differ diff --git a/data/recordings/20260515_100407_user.wav b/data/recordings/20260515_100407_user.wav new file mode 100644 index 0000000..cd33f72 Binary files /dev/null and b/data/recordings/20260515_100407_user.wav differ diff --git a/data/recordings/20260515_100408_user.wav b/data/recordings/20260515_100408_user.wav new file mode 100644 index 0000000..3fc351c Binary files /dev/null and b/data/recordings/20260515_100408_user.wav differ diff --git a/data/recordings/20260515_102425_robot.wav b/data/recordings/20260515_102425_robot.wav new file mode 100644 index 0000000..6adfe9c Binary files /dev/null and b/data/recordings/20260515_102425_robot.wav differ diff --git a/data/recordings/20260515_102425_user.wav b/data/recordings/20260515_102425_user.wav new file mode 100644 index 0000000..4945b1c Binary files /dev/null and b/data/recordings/20260515_102425_user.wav differ diff --git a/data/recordings/20260515_102430_robot.wav b/data/recordings/20260515_102430_robot.wav new file mode 100644 index 0000000..bf9b927 Binary files /dev/null and b/data/recordings/20260515_102430_robot.wav differ diff --git a/data/recordings/20260515_102430_user.wav b/data/recordings/20260515_102430_user.wav new file mode 100644 index 0000000..b2d22f6 Binary files /dev/null and b/data/recordings/20260515_102430_user.wav differ diff --git a/data/recordings/20260515_102431_robot.wav b/data/recordings/20260515_102431_robot.wav new file mode 100644 index 0000000..4dbd57d Binary files /dev/null and b/data/recordings/20260515_102431_robot.wav differ diff --git a/data/recordings/20260515_102431_user.wav b/data/recordings/20260515_102431_user.wav new file mode 100644 index 0000000..11d7fb5 Binary files /dev/null and b/data/recordings/20260515_102431_user.wav differ diff --git a/data/recordings/20260515_102435_user.wav b/data/recordings/20260515_102435_user.wav new file mode 100644 index 0000000..2d50c53 Binary files /dev/null and b/data/recordings/20260515_102435_user.wav differ diff --git a/data/recordings/20260515_102438_user.wav b/data/recordings/20260515_102438_user.wav new file mode 100644 index 0000000..8396226 Binary files /dev/null and b/data/recordings/20260515_102438_user.wav differ diff --git a/data/recordings/20260515_105540_robot.wav b/data/recordings/20260515_105540_robot.wav new file mode 100644 index 0000000..b4db206 Binary files /dev/null and b/data/recordings/20260515_105540_robot.wav differ diff --git a/data/recordings/20260515_105543_user.wav b/data/recordings/20260515_105543_user.wav new file mode 100644 index 0000000..860ba62 Binary files /dev/null and b/data/recordings/20260515_105543_user.wav differ diff --git a/data/recordings/20260515_105544_robot.wav b/data/recordings/20260515_105544_robot.wav new file mode 100644 index 0000000..f3531b6 Binary files /dev/null and b/data/recordings/20260515_105544_robot.wav differ diff --git a/data/recordings/20260515_105544_user.wav b/data/recordings/20260515_105544_user.wav new file mode 100644 index 0000000..f56b2e7 Binary files /dev/null and b/data/recordings/20260515_105544_user.wav differ diff --git a/data/recordings/20260515_105548_robot.wav b/data/recordings/20260515_105548_robot.wav new file mode 100644 index 0000000..9446c48 Binary files /dev/null and b/data/recordings/20260515_105548_robot.wav differ diff --git a/data/recordings/20260515_105548_user.wav b/data/recordings/20260515_105548_user.wav new file mode 100644 index 0000000..690ad34 Binary files /dev/null and b/data/recordings/20260515_105548_user.wav differ diff --git a/data/recordings/20260515_105553_user.wav b/data/recordings/20260515_105553_user.wav new file mode 100644 index 0000000..61f8967 Binary files /dev/null and b/data/recordings/20260515_105553_user.wav differ diff --git a/data/recordings/20260515_105559_user.wav b/data/recordings/20260515_105559_user.wav new file mode 100644 index 0000000..c3639c0 Binary files /dev/null and b/data/recordings/20260515_105559_user.wav differ diff --git a/data/recordings/20260515_105602_user.wav b/data/recordings/20260515_105602_user.wav new file mode 100644 index 0000000..d1ddb43 Binary files /dev/null and b/data/recordings/20260515_105602_user.wav differ diff --git a/data/recordings/20260515_105606_user.wav b/data/recordings/20260515_105606_user.wav new file mode 100644 index 0000000..dad83b7 Binary files /dev/null and b/data/recordings/20260515_105606_user.wav differ diff --git a/data/recordings/20260515_105610_robot.wav b/data/recordings/20260515_105610_robot.wav new file mode 100644 index 0000000..03808df Binary files /dev/null and b/data/recordings/20260515_105610_robot.wav differ diff --git a/data/recordings/20260515_105610_user.wav b/data/recordings/20260515_105610_user.wav new file mode 100644 index 0000000..7bcd3f4 Binary files /dev/null and b/data/recordings/20260515_105610_user.wav differ diff --git a/data/recordings/20260515_105615_robot.wav b/data/recordings/20260515_105615_robot.wav new file mode 100644 index 0000000..7d6b879 Binary files /dev/null and b/data/recordings/20260515_105615_robot.wav differ diff --git a/data/recordings/20260515_105615_user.wav b/data/recordings/20260515_105615_user.wav new file mode 100644 index 0000000..1c86754 Binary files /dev/null and b/data/recordings/20260515_105615_user.wav differ diff --git a/data/recordings/20260515_105622_user.wav b/data/recordings/20260515_105622_user.wav new file mode 100644 index 0000000..fa29b07 Binary files /dev/null and b/data/recordings/20260515_105622_user.wav differ diff --git a/data/recordings/20260515_105635_user.wav b/data/recordings/20260515_105635_user.wav new file mode 100644 index 0000000..97d4908 Binary files /dev/null and b/data/recordings/20260515_105635_user.wav differ diff --git a/data/recordings/20260515_105637_user.wav b/data/recordings/20260515_105637_user.wav new file mode 100644 index 0000000..20072ef Binary files /dev/null and b/data/recordings/20260515_105637_user.wav differ diff --git a/data/recordings/20260515_105643_user.wav b/data/recordings/20260515_105643_user.wav new file mode 100644 index 0000000..0e3c7a5 Binary files /dev/null and b/data/recordings/20260515_105643_user.wav differ diff --git a/data/recordings/20260515_105647_user.wav b/data/recordings/20260515_105647_user.wav new file mode 100644 index 0000000..d14d55f Binary files /dev/null and b/data/recordings/20260515_105647_user.wav differ diff --git a/data/recordings/20260515_105652_user.wav b/data/recordings/20260515_105652_user.wav new file mode 100644 index 0000000..7588f87 Binary files /dev/null and b/data/recordings/20260515_105652_user.wav differ diff --git a/data/recordings/20260515_105655_robot.wav b/data/recordings/20260515_105655_robot.wav new file mode 100644 index 0000000..81586d2 Binary files /dev/null and b/data/recordings/20260515_105655_robot.wav differ diff --git a/data/recordings/20260515_105655_user.wav b/data/recordings/20260515_105655_user.wav new file mode 100644 index 0000000..54585a5 Binary files /dev/null and b/data/recordings/20260515_105655_user.wav differ diff --git a/data/recordings/20260515_105700_robot.wav b/data/recordings/20260515_105700_robot.wav new file mode 100644 index 0000000..1d7d439 Binary files /dev/null and b/data/recordings/20260515_105700_robot.wav differ diff --git a/data/recordings/20260515_105700_user.wav b/data/recordings/20260515_105700_user.wav new file mode 100644 index 0000000..ea3b991 Binary files /dev/null and b/data/recordings/20260515_105700_user.wav differ diff --git a/data/recordings/20260515_105701_user.wav b/data/recordings/20260515_105701_user.wav new file mode 100644 index 0000000..ebf3e04 Binary files /dev/null and b/data/recordings/20260515_105701_user.wav differ diff --git a/data/recordings/20260515_105702_robot.wav b/data/recordings/20260515_105702_robot.wav new file mode 100644 index 0000000..7af4317 Binary files /dev/null and b/data/recordings/20260515_105702_robot.wav differ diff --git a/data/recordings/20260515_105702_user.wav b/data/recordings/20260515_105702_user.wav new file mode 100644 index 0000000..0f3fd8d Binary files /dev/null and b/data/recordings/20260515_105702_user.wav differ diff --git a/data/recordings/20260515_105709_robot.wav b/data/recordings/20260515_105709_robot.wav new file mode 100644 index 0000000..c476613 Binary files /dev/null and b/data/recordings/20260515_105709_robot.wav differ diff --git a/data/recordings/20260515_105709_user.wav b/data/recordings/20260515_105709_user.wav new file mode 100644 index 0000000..4239325 Binary files /dev/null and b/data/recordings/20260515_105709_user.wav differ diff --git a/data/recordings/20260515_105746_user.wav b/data/recordings/20260515_105746_user.wav new file mode 100644 index 0000000..f56b872 Binary files /dev/null and b/data/recordings/20260515_105746_user.wav differ diff --git a/data/recordings/20260515_105747_robot.wav b/data/recordings/20260515_105747_robot.wav new file mode 100644 index 0000000..fff72f8 Binary files /dev/null and b/data/recordings/20260515_105747_robot.wav differ diff --git a/data/recordings/20260515_105747_user.wav b/data/recordings/20260515_105747_user.wav new file mode 100644 index 0000000..eada8fd Binary files /dev/null and b/data/recordings/20260515_105747_user.wav differ diff --git a/data/recordings/20260515_105756_robot.wav b/data/recordings/20260515_105756_robot.wav new file mode 100644 index 0000000..72c8f0b Binary files /dev/null and b/data/recordings/20260515_105756_robot.wav differ diff --git a/data/recordings/20260515_105756_user.wav b/data/recordings/20260515_105756_user.wav new file mode 100644 index 0000000..889e02c Binary files /dev/null and b/data/recordings/20260515_105756_user.wav differ diff --git a/data/recordings/20260515_105802_user.wav b/data/recordings/20260515_105802_user.wav new file mode 100644 index 0000000..b676a2e Binary files /dev/null and b/data/recordings/20260515_105802_user.wav differ diff --git a/data/recordings/20260516_112009_robot.wav b/data/recordings/20260516_112009_robot.wav new file mode 100644 index 0000000..a2f122b Binary files /dev/null and b/data/recordings/20260516_112009_robot.wav differ diff --git a/data/recordings/20260516_112010_user.wav b/data/recordings/20260516_112010_user.wav new file mode 100644 index 0000000..fe54df4 Binary files /dev/null and b/data/recordings/20260516_112010_user.wav differ diff --git a/data/recordings/20260519_082138_robot.wav b/data/recordings/20260519_082138_robot.wav new file mode 100644 index 0000000..b132fb1 Binary files /dev/null and b/data/recordings/20260519_082138_robot.wav differ diff --git a/data/recordings/20260519_082138_user.wav b/data/recordings/20260519_082138_user.wav new file mode 100644 index 0000000..0d47512 Binary files /dev/null and b/data/recordings/20260519_082138_user.wav differ diff --git a/data/recordings/20260519_082148_user.wav b/data/recordings/20260519_082148_user.wav new file mode 100644 index 0000000..f24f0c6 Binary files /dev/null and b/data/recordings/20260519_082148_user.wav differ diff --git a/data/recordings/20260519_082158_robot.wav b/data/recordings/20260519_082158_robot.wav new file mode 100644 index 0000000..7bdc254 Binary files /dev/null and b/data/recordings/20260519_082158_robot.wav differ diff --git a/data/recordings/20260519_082158_user.wav b/data/recordings/20260519_082158_user.wav new file mode 100644 index 0000000..39079b6 Binary files /dev/null and b/data/recordings/20260519_082158_user.wav differ diff --git a/data/recordings/20260519_084918_robot.wav b/data/recordings/20260519_084918_robot.wav new file mode 100644 index 0000000..6d0d569 Binary files /dev/null and b/data/recordings/20260519_084918_robot.wav differ diff --git a/data/recordings/20260519_084918_user.wav b/data/recordings/20260519_084918_user.wav new file mode 100644 index 0000000..d1f5510 Binary files /dev/null and b/data/recordings/20260519_084918_user.wav differ diff --git a/data/recordings/20260519_084923_robot.wav b/data/recordings/20260519_084923_robot.wav new file mode 100644 index 0000000..eaa7e3f Binary files /dev/null and b/data/recordings/20260519_084923_robot.wav differ diff --git a/data/recordings/20260519_084923_user.wav b/data/recordings/20260519_084923_user.wav new file mode 100644 index 0000000..913c6bb Binary files /dev/null and b/data/recordings/20260519_084923_user.wav differ diff --git a/data/recordings/20260519_084927_user.wav b/data/recordings/20260519_084927_user.wav new file mode 100644 index 0000000..8f37aad Binary files /dev/null and b/data/recordings/20260519_084927_user.wav differ diff --git a/data/recordings/20260519_115018_robot.wav b/data/recordings/20260519_115018_robot.wav new file mode 100644 index 0000000..ebfb98d Binary files /dev/null and b/data/recordings/20260519_115018_robot.wav differ diff --git a/data/recordings/20260519_115018_user.wav b/data/recordings/20260519_115018_user.wav new file mode 100644 index 0000000..4fb4567 Binary files /dev/null and b/data/recordings/20260519_115018_user.wav differ diff --git a/data/recordings/20260519_115025_robot.wav b/data/recordings/20260519_115025_robot.wav new file mode 100644 index 0000000..73ef4d3 Binary files /dev/null and b/data/recordings/20260519_115025_robot.wav differ diff --git a/data/recordings/20260519_115025_user.wav b/data/recordings/20260519_115025_user.wav new file mode 100644 index 0000000..0b285c8 Binary files /dev/null and b/data/recordings/20260519_115025_user.wav differ diff --git a/data/recordings/20260519_115036_robot.wav b/data/recordings/20260519_115036_robot.wav new file mode 100644 index 0000000..804fe3a Binary files /dev/null and b/data/recordings/20260519_115036_robot.wav differ diff --git a/data/recordings/20260519_115036_user.wav b/data/recordings/20260519_115036_user.wav new file mode 100644 index 0000000..7f70b67 Binary files /dev/null and b/data/recordings/20260519_115036_user.wav differ diff --git a/data/recordings/20260519_115039_user.wav b/data/recordings/20260519_115039_user.wav new file mode 100644 index 0000000..18113f5 Binary files /dev/null and b/data/recordings/20260519_115039_user.wav differ diff --git a/data/recordings/20260519_115041_user.wav b/data/recordings/20260519_115041_user.wav new file mode 100644 index 0000000..4c70f8d Binary files /dev/null and b/data/recordings/20260519_115041_user.wav differ diff --git a/data/recordings/20260519_115049_robot.wav b/data/recordings/20260519_115049_robot.wav new file mode 100644 index 0000000..d0bf7af Binary files /dev/null and b/data/recordings/20260519_115049_robot.wav differ diff --git a/data/recordings/20260519_115049_user.wav b/data/recordings/20260519_115049_user.wav new file mode 100644 index 0000000..8396788 Binary files /dev/null and b/data/recordings/20260519_115049_user.wav differ diff --git a/data/recordings/20260519_115102_robot.wav b/data/recordings/20260519_115102_robot.wav new file mode 100644 index 0000000..0314714 Binary files /dev/null and b/data/recordings/20260519_115102_robot.wav differ diff --git a/data/recordings/20260519_115109_robot.wav b/data/recordings/20260519_115109_robot.wav new file mode 100644 index 0000000..8b12197 Binary files /dev/null and b/data/recordings/20260519_115109_robot.wav differ diff --git a/data/recordings/20260519_115109_user.wav b/data/recordings/20260519_115109_user.wav new file mode 100644 index 0000000..a9858ba Binary files /dev/null and b/data/recordings/20260519_115109_user.wav differ diff --git a/data/recordings/20260519_115113_robot.wav b/data/recordings/20260519_115113_robot.wav new file mode 100644 index 0000000..17e73fa Binary files /dev/null and b/data/recordings/20260519_115113_robot.wav differ diff --git a/data/recordings/20260519_115125_robot.wav b/data/recordings/20260519_115125_robot.wav new file mode 100644 index 0000000..dc3d783 Binary files /dev/null and b/data/recordings/20260519_115125_robot.wav differ diff --git a/data/recordings/20260519_115125_user.wav b/data/recordings/20260519_115125_user.wav new file mode 100644 index 0000000..90b32b6 Binary files /dev/null and b/data/recordings/20260519_115125_user.wav differ diff --git a/data/recordings/20260519_115131_robot.wav b/data/recordings/20260519_115131_robot.wav new file mode 100644 index 0000000..57f52e8 Binary files /dev/null and b/data/recordings/20260519_115131_robot.wav differ diff --git a/data/recordings/20260519_115131_user.wav b/data/recordings/20260519_115131_user.wav new file mode 100644 index 0000000..82f7bc2 Binary files /dev/null and b/data/recordings/20260519_115131_user.wav differ diff --git a/data/recordings/20260519_115138_user.wav b/data/recordings/20260519_115138_user.wav new file mode 100644 index 0000000..a139650 Binary files /dev/null and b/data/recordings/20260519_115138_user.wav differ diff --git a/data/recordings/20260519_130234_robot.wav b/data/recordings/20260519_130234_robot.wav new file mode 100644 index 0000000..accead2 Binary files /dev/null and b/data/recordings/20260519_130234_robot.wav differ diff --git a/data/recordings/20260519_130239_user.wav b/data/recordings/20260519_130239_user.wav new file mode 100644 index 0000000..513a018 Binary files /dev/null and b/data/recordings/20260519_130239_user.wav differ diff --git a/data/recordings/20260519_154619_user.wav b/data/recordings/20260519_154619_user.wav new file mode 100644 index 0000000..10d67ea Binary files /dev/null and b/data/recordings/20260519_154619_user.wav differ diff --git a/data/recordings/20260519_154620_user.wav b/data/recordings/20260519_154620_user.wav new file mode 100644 index 0000000..f844f24 Binary files /dev/null and b/data/recordings/20260519_154620_user.wav differ diff --git a/data/recordings/20260519_154621_robot.wav b/data/recordings/20260519_154621_robot.wav new file mode 100644 index 0000000..c8a8b3e Binary files /dev/null and b/data/recordings/20260519_154621_robot.wav differ diff --git a/data/recordings/20260519_154621_user.wav b/data/recordings/20260519_154621_user.wav new file mode 100644 index 0000000..ab6c04d Binary files /dev/null and b/data/recordings/20260519_154621_user.wav differ diff --git a/data/recordings/20260519_154632_robot.wav b/data/recordings/20260519_154632_robot.wav new file mode 100644 index 0000000..832f8fd Binary files /dev/null and b/data/recordings/20260519_154632_robot.wav differ diff --git a/data/recordings/20260519_154632_user.wav b/data/recordings/20260519_154632_user.wav new file mode 100644 index 0000000..b8b4835 Binary files /dev/null and b/data/recordings/20260519_154632_user.wav differ diff --git a/data/recordings/20260519_154633_robot.wav b/data/recordings/20260519_154633_robot.wav new file mode 100644 index 0000000..f935826 Binary files /dev/null and b/data/recordings/20260519_154633_robot.wav differ diff --git a/data/recordings/20260519_154633_user.wav b/data/recordings/20260519_154633_user.wav new file mode 100644 index 0000000..db33ab1 Binary files /dev/null and b/data/recordings/20260519_154633_user.wav differ diff --git a/data/recordings/20260519_154636_robot.wav b/data/recordings/20260519_154636_robot.wav new file mode 100644 index 0000000..6ea531d Binary files /dev/null and b/data/recordings/20260519_154636_robot.wav differ diff --git a/data/recordings/20260519_154636_user.wav b/data/recordings/20260519_154636_user.wav new file mode 100644 index 0000000..efae78f Binary files /dev/null and b/data/recordings/20260519_154636_user.wav differ diff --git a/data/recordings/20260519_154640_robot.wav b/data/recordings/20260519_154640_robot.wav new file mode 100644 index 0000000..c3b1b3f Binary files /dev/null and b/data/recordings/20260519_154640_robot.wav differ diff --git a/data/recordings/20260519_154640_user.wav b/data/recordings/20260519_154640_user.wav new file mode 100644 index 0000000..a23a003 Binary files /dev/null and b/data/recordings/20260519_154640_user.wav differ diff --git a/data/recordings/20260519_154703_user.wav b/data/recordings/20260519_154703_user.wav new file mode 100644 index 0000000..2752de6 Binary files /dev/null and b/data/recordings/20260519_154703_user.wav differ diff --git a/data/recordings/20260519_154705_robot.wav b/data/recordings/20260519_154705_robot.wav new file mode 100644 index 0000000..8c2ebd2 Binary files /dev/null and b/data/recordings/20260519_154705_robot.wav differ diff --git a/data/recordings/20260519_154705_user.wav b/data/recordings/20260519_154705_user.wav new file mode 100644 index 0000000..b95a9e4 Binary files /dev/null and b/data/recordings/20260519_154705_user.wav differ diff --git a/data/recordings/20260519_154711_user.wav b/data/recordings/20260519_154711_user.wav new file mode 100644 index 0000000..da16568 Binary files /dev/null and b/data/recordings/20260519_154711_user.wav differ diff --git a/data/recordings/20260519_154712_robot.wav b/data/recordings/20260519_154712_robot.wav new file mode 100644 index 0000000..3f73b93 Binary files /dev/null and b/data/recordings/20260519_154712_robot.wav differ diff --git a/data/recordings/20260519_154712_user.wav b/data/recordings/20260519_154712_user.wav new file mode 100644 index 0000000..9eb0e2e Binary files /dev/null and b/data/recordings/20260519_154712_user.wav differ diff --git a/data/recordings/20260519_154720_robot.wav b/data/recordings/20260519_154720_robot.wav new file mode 100644 index 0000000..389c01d Binary files /dev/null and b/data/recordings/20260519_154720_robot.wav differ diff --git a/data/recordings/20260519_154720_user.wav b/data/recordings/20260519_154720_user.wav new file mode 100644 index 0000000..56b4f87 Binary files /dev/null and b/data/recordings/20260519_154720_user.wav differ diff --git a/data/recordings/20260519_154727_robot.wav b/data/recordings/20260519_154727_robot.wav new file mode 100644 index 0000000..7d4a6d7 Binary files /dev/null and b/data/recordings/20260519_154727_robot.wav differ diff --git a/data/recordings/20260519_154727_user.wav b/data/recordings/20260519_154727_user.wav new file mode 100644 index 0000000..57ed818 Binary files /dev/null and b/data/recordings/20260519_154727_user.wav differ diff --git a/data/recordings/20260519_154733_robot.wav b/data/recordings/20260519_154733_robot.wav new file mode 100644 index 0000000..dc3ac89 Binary files /dev/null and b/data/recordings/20260519_154733_robot.wav differ diff --git a/data/recordings/20260519_154733_user.wav b/data/recordings/20260519_154733_user.wav new file mode 100644 index 0000000..0949d2d Binary files /dev/null and b/data/recordings/20260519_154733_user.wav differ diff --git a/data/recordings/20260519_154737_robot.wav b/data/recordings/20260519_154737_robot.wav new file mode 100644 index 0000000..109877d Binary files /dev/null and b/data/recordings/20260519_154737_robot.wav differ diff --git a/data/recordings/20260519_154737_user.wav b/data/recordings/20260519_154737_user.wav new file mode 100644 index 0000000..3185ff3 Binary files /dev/null and b/data/recordings/20260519_154737_user.wav differ diff --git a/data/recordings/20260519_154740_robot.wav b/data/recordings/20260519_154740_robot.wav new file mode 100644 index 0000000..b64ea83 Binary files /dev/null and b/data/recordings/20260519_154740_robot.wav differ diff --git a/data/recordings/20260519_154740_user.wav b/data/recordings/20260519_154740_user.wav new file mode 100644 index 0000000..f8a04af Binary files /dev/null and b/data/recordings/20260519_154740_user.wav differ diff --git a/data/recordings/20260519_154742_user.wav b/data/recordings/20260519_154742_user.wav new file mode 100644 index 0000000..5bb72af Binary files /dev/null and b/data/recordings/20260519_154742_user.wav differ diff --git a/data/recordings/20260519_154752_robot.wav b/data/recordings/20260519_154752_robot.wav new file mode 100644 index 0000000..6787590 Binary files /dev/null and b/data/recordings/20260519_154752_robot.wav differ diff --git a/data/recordings/20260519_154752_user.wav b/data/recordings/20260519_154752_user.wav new file mode 100644 index 0000000..614ca52 Binary files /dev/null and b/data/recordings/20260519_154752_user.wav differ diff --git a/data/recordings/20260519_154804_robot.wav b/data/recordings/20260519_154804_robot.wav new file mode 100644 index 0000000..caf44f7 Binary files /dev/null and b/data/recordings/20260519_154804_robot.wav differ diff --git a/data/recordings/20260519_154804_user.wav b/data/recordings/20260519_154804_user.wav new file mode 100644 index 0000000..824c9ed Binary files /dev/null and b/data/recordings/20260519_154804_user.wav differ diff --git a/data/recordings/20260519_154832_user.wav b/data/recordings/20260519_154832_user.wav new file mode 100644 index 0000000..818992f Binary files /dev/null and b/data/recordings/20260519_154832_user.wav differ diff --git a/data/recordings/20260519_154833_robot.wav b/data/recordings/20260519_154833_robot.wav new file mode 100644 index 0000000..fc0a819 Binary files /dev/null and b/data/recordings/20260519_154833_robot.wav differ diff --git a/data/recordings/20260519_154833_user.wav b/data/recordings/20260519_154833_user.wav new file mode 100644 index 0000000..1da3192 Binary files /dev/null and b/data/recordings/20260519_154833_user.wav differ diff --git a/data/recordings/20260519_154851_user.wav b/data/recordings/20260519_154851_user.wav new file mode 100644 index 0000000..61437b1 Binary files /dev/null and b/data/recordings/20260519_154851_user.wav differ diff --git a/data/recordings/20260519_154903_robot.wav b/data/recordings/20260519_154903_robot.wav new file mode 100644 index 0000000..c2d96f1 Binary files /dev/null and b/data/recordings/20260519_154903_robot.wav differ diff --git a/data/recordings/20260519_154903_user.wav b/data/recordings/20260519_154903_user.wav new file mode 100644 index 0000000..113196a Binary files /dev/null and b/data/recordings/20260519_154903_user.wav differ diff --git a/data/recordings/20260519_154918_user.wav b/data/recordings/20260519_154918_user.wav new file mode 100644 index 0000000..8c320ab Binary files /dev/null and b/data/recordings/20260519_154918_user.wav differ diff --git a/data/recordings/20260519_154949_user.wav b/data/recordings/20260519_154949_user.wav new file mode 100644 index 0000000..3d78a90 Binary files /dev/null and b/data/recordings/20260519_154949_user.wav differ diff --git a/data/recordings/20260519_154955_robot.wav b/data/recordings/20260519_154955_robot.wav new file mode 100644 index 0000000..4f494b9 Binary files /dev/null and b/data/recordings/20260519_154955_robot.wav differ diff --git a/data/recordings/20260519_154955_user.wav b/data/recordings/20260519_154955_user.wav new file mode 100644 index 0000000..f56b210 Binary files /dev/null and b/data/recordings/20260519_154955_user.wav differ diff --git a/data/recordings/20260519_155008_robot.wav b/data/recordings/20260519_155008_robot.wav new file mode 100644 index 0000000..286e435 Binary files /dev/null and b/data/recordings/20260519_155008_robot.wav differ diff --git a/data/recordings/20260519_155008_user.wav b/data/recordings/20260519_155008_user.wav new file mode 100644 index 0000000..b7993e1 Binary files /dev/null and b/data/recordings/20260519_155008_user.wav differ diff --git a/data/recordings/20260519_155015_robot.wav b/data/recordings/20260519_155015_robot.wav new file mode 100644 index 0000000..9c5baa2 Binary files /dev/null and b/data/recordings/20260519_155015_robot.wav differ diff --git a/data/recordings/20260519_155015_user.wav b/data/recordings/20260519_155015_user.wav new file mode 100644 index 0000000..bd579a2 Binary files /dev/null and b/data/recordings/20260519_155015_user.wav differ diff --git a/data/recordings/20260519_155026_robot.wav b/data/recordings/20260519_155026_robot.wav new file mode 100644 index 0000000..f765eb1 Binary files /dev/null and b/data/recordings/20260519_155026_robot.wav differ diff --git a/data/recordings/20260519_155026_user.wav b/data/recordings/20260519_155026_user.wav new file mode 100644 index 0000000..a8651b7 Binary files /dev/null and b/data/recordings/20260519_155026_user.wav differ diff --git a/data/recordings/20260519_155029_robot.wav b/data/recordings/20260519_155029_robot.wav new file mode 100644 index 0000000..6fe0bdd Binary files /dev/null and b/data/recordings/20260519_155029_robot.wav differ diff --git a/data/recordings/20260519_155029_user.wav b/data/recordings/20260519_155029_user.wav new file mode 100644 index 0000000..52c218f Binary files /dev/null and b/data/recordings/20260519_155029_user.wav differ diff --git a/data/recordings/20260519_155101_user.wav b/data/recordings/20260519_155101_user.wav new file mode 100644 index 0000000..ef0e69f Binary files /dev/null and b/data/recordings/20260519_155101_user.wav differ diff --git a/data/recordings/20260519_155251_user.wav b/data/recordings/20260519_155251_user.wav new file mode 100644 index 0000000..3be713c Binary files /dev/null and b/data/recordings/20260519_155251_user.wav differ diff --git a/data/recordings/20260519_155317_robot.wav b/data/recordings/20260519_155317_robot.wav new file mode 100644 index 0000000..5f8df05 Binary files /dev/null and b/data/recordings/20260519_155317_robot.wav differ diff --git a/data/recordings/20260519_155317_user.wav b/data/recordings/20260519_155317_user.wav new file mode 100644 index 0000000..acf4221 Binary files /dev/null and b/data/recordings/20260519_155317_user.wav differ diff --git a/data/recordings/20260519_155328_user.wav b/data/recordings/20260519_155328_user.wav new file mode 100644 index 0000000..47ed111 Binary files /dev/null and b/data/recordings/20260519_155328_user.wav differ diff --git a/data/recordings/20260519_155616_robot.wav b/data/recordings/20260519_155616_robot.wav new file mode 100644 index 0000000..64e9c75 Binary files /dev/null and b/data/recordings/20260519_155616_robot.wav differ diff --git a/data/recordings/20260519_155616_user.wav b/data/recordings/20260519_155616_user.wav new file mode 100644 index 0000000..7ef7f75 Binary files /dev/null and b/data/recordings/20260519_155616_user.wav differ diff --git a/data/recordings/20260519_155622_user.wav b/data/recordings/20260519_155622_user.wav new file mode 100644 index 0000000..cb955cc Binary files /dev/null and b/data/recordings/20260519_155622_user.wav differ diff --git a/data/recordings/20260519_155625_robot.wav b/data/recordings/20260519_155625_robot.wav new file mode 100644 index 0000000..76f29fc Binary files /dev/null and b/data/recordings/20260519_155625_robot.wav differ diff --git a/data/recordings/20260519_155625_user.wav b/data/recordings/20260519_155625_user.wav new file mode 100644 index 0000000..2648df1 Binary files /dev/null and b/data/recordings/20260519_155625_user.wav differ diff --git a/data/recordings/20260519_155641_robot.wav b/data/recordings/20260519_155641_robot.wav new file mode 100644 index 0000000..19dff72 Binary files /dev/null and b/data/recordings/20260519_155641_robot.wav differ diff --git a/data/recordings/20260519_155641_user.wav b/data/recordings/20260519_155641_user.wav new file mode 100644 index 0000000..6c32742 Binary files /dev/null and b/data/recordings/20260519_155641_user.wav differ diff --git a/data/recordings/20260520_113905_robot.wav b/data/recordings/20260520_113905_robot.wav new file mode 100644 index 0000000..ea85923 Binary files /dev/null and b/data/recordings/20260520_113905_robot.wav differ diff --git a/data/recordings/20260520_113914_robot.wav b/data/recordings/20260520_113914_robot.wav new file mode 100644 index 0000000..c5483e5 Binary files /dev/null and b/data/recordings/20260520_113914_robot.wav differ diff --git a/data/recordings/20260520_113914_user.wav b/data/recordings/20260520_113914_user.wav new file mode 100644 index 0000000..a0b5d82 Binary files /dev/null and b/data/recordings/20260520_113914_user.wav differ diff --git a/data/recordings/20260520_121411_user.wav b/data/recordings/20260520_121411_user.wav new file mode 100644 index 0000000..16ce812 Binary files /dev/null and b/data/recordings/20260520_121411_user.wav differ diff --git a/data/recordings/20260520_121412_robot.wav b/data/recordings/20260520_121412_robot.wav new file mode 100644 index 0000000..1786ffc Binary files /dev/null and b/data/recordings/20260520_121412_robot.wav differ diff --git a/data/recordings/20260520_121412_user.wav b/data/recordings/20260520_121412_user.wav new file mode 100644 index 0000000..9d1e7cb Binary files /dev/null and b/data/recordings/20260520_121412_user.wav differ diff --git a/data/recordings/20260528_164730_robot.wav b/data/recordings/20260528_164730_robot.wav new file mode 100644 index 0000000..9a514fb Binary files /dev/null and b/data/recordings/20260528_164730_robot.wav differ diff --git a/data/recordings/20260528_164732_robot.wav b/data/recordings/20260528_164732_robot.wav new file mode 100644 index 0000000..b760c4e Binary files /dev/null and b/data/recordings/20260528_164732_robot.wav differ diff --git a/data/recordings/20260528_164732_user.wav b/data/recordings/20260528_164732_user.wav new file mode 100644 index 0000000..f89abab Binary files /dev/null and b/data/recordings/20260528_164732_user.wav differ diff --git a/data/recordings/20260528_174103_robot.wav b/data/recordings/20260528_174103_robot.wav new file mode 100644 index 0000000..487686f Binary files /dev/null and b/data/recordings/20260528_174103_robot.wav differ diff --git a/data/recordings/20260528_174103_user.wav b/data/recordings/20260528_174103_user.wav new file mode 100644 index 0000000..a49ddbc Binary files /dev/null and b/data/recordings/20260528_174103_user.wav differ diff --git a/data/recordings/index.json b/data/recordings/index.json new file mode 100644 index 0000000..b9e7f8e --- /dev/null +++ b/data/recordings/index.json @@ -0,0 +1,3859 @@ +{ + "records": [ + { + "timestamp": "20260514_174629", + "started_at": 1778751989.0267973, + "user_text": "", + "robot_text": "مرحبا، يا خوي! شخبارك؟ أنا موجود هني عشان أساعدك.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_174629_robot.wav", + "robot_duration_sec": 4.0 + }, + { + "timestamp": "20260514_174700", + "started_at": 1778752020.5914102, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_174700_user.wav", + "user_duration_sec": 0.32 + }, + { + "timestamp": "20260514_174701", + "started_at": 1778752021.0626216, + "user_text": "", + "robot_text": "مرحبا، يا خوي. شحالك اليوم؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_174701_user.wav", + "user_duration_sec": 0.128, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_174701_robot.wav", + "robot_duration_sec": 2.4 + }, + { + "timestamp": "20260514_174714", + "started_at": 1778752034.3399298, + "user_text": "", + "robot_text": "يا خوي، الكاميرا انطفت عندي وما أقدر أشوفك الحين. وعقب ما بقدر أميز الوجوه بعد.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_174714_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_174714_robot.wav", + "robot_duration_sec": 5.76 + }, + { + "timestamp": "20260514_174723", + "started_at": 1778752043.9763718, + "user_text": "", + "robot_text": "هلا، الحين أشوفك زين.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_174723_user.wav", + "user_duration_sec": 0.928, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_174723_robot.wav", + "robot_duration_sec": 1.8 + }, + { + "timestamp": "20260514_175127", + "started_at": 1778752287.5203393, + "user_text": "", + "robot_text": "تمام، الحين بقدر أميز الوجوه اللي أعرفها.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175127_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175127_robot.wav", + "robot_duration_sec": 3.4 + }, + { + "timestamp": "20260514_175134", + "started_at": 1778752294.0328631, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175134_user.wav", + "user_duration_sec": 0.512 + }, + { + "timestamp": "20260514_175139", + "started_at": 1778752299.8385606, + "user_text": "อัน นั้น มี ", + "robot_text": "هلا والله يا كسام، شحالك؟", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175139_robot.wav", + "robot_duration_sec": 2.08 + }, + { + "timestamp": "20260514_175147", + "started_at": 1778752307.3065, + "user_text": "هذا مين بتعرفه؟", + "robot_text": "هذا قسام! مهندس الروبوتات، صح؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175147_user.wav", + "user_duration_sec": 0.992, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175147_robot.wav", + "robot_duration_sec": 2.84 + }, + { + "timestamp": "20260514_175200", + "started_at": 1778752320.2166772, + "user_text": "هذا مين؟", + "robot_text": "هذا همام! مدير المشاريع.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175200_user.wav", + "user_duration_sec": 0.512, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175200_robot.wav", + "robot_duration_sec": 2.2 + }, + { + "timestamp": "20260514_175213", + "started_at": 1778752333.0210764, + "user_text": "وهذا مين؟", + "robot_text": "السموحة، الصورة ما واضحة كفاية عندي. منو هذا؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175213_user.wav", + "user_duration_sec": 0.704, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175213_robot.wav", + "robot_duration_sec": 3.28 + }, + { + "timestamp": "20260514_175221", + "started_at": 1778752341.1267893, + "user_text": "هذا منهم", + "robot_text": "أوه، هذا حازم! المدير العام في شركة ys lootah tech.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175221_user.wav", + "user_duration_sec": 0.544, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175221_robot.wav", + "robot_duration_sec": 4.48 + }, + { + "timestamp": "20260514_175231", + "started_at": 1778752351.7411609, + "user_text": "شو بتعرف عنه؟", + "robot_text": "عرف إنه المدير العام لشركة ys lootah tech. فيه شيء ثاني تبغي تعرفه؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175231_user.wav", + "user_duration_sec": 0.896, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175231_robot.wav", + "robot_duration_sec": 5.44 + }, + { + "timestamp": "20260514_175244", + "started_at": 1778752364.2121518, + "user_text": "", + "robot_text": "يا خوي، الكاميرا طفت عندي وما أقدر أشوفك الحين، وماراح أقدر أميز الوجوه بعد.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260514_175244_user.wav", + "user_duration_sec": 0.096, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260514_175244_robot.wav", + "robot_duration_sec": 5.44 + }, + { + "timestamp": "20260515_080052", + "started_at": 1778803252.418725, + "user_text": "", + "robot_text": "مرحبا بك، يا خوي. آمِرني بأي شيء تحتاجه.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080052_robot.wav", + "robot_duration_sec": 3.48 + }, + { + "timestamp": "20260515_080056", + "started_at": 1778803256.8518295, + "user_text": "", + "robot_text": "هلا، الحين أشوفك زين. كيف حالك اليوم؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080056_user.wav", + "user_duration_sec": 0.16, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080056_robot.wav", + "robot_duration_sec": 3.12 + }, + { + "timestamp": "20260515_080201", + "started_at": 1778803321.2681544, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080201_user.wav", + "user_duration_sec": 0.48 + }, + { + "timestamp": "20260515_080205", + "started_at": 1778803325.0990653, + "user_text": "", + "robot_text": "تمام، الحين أقدر أميز الأشخاص اللي أعرفهم.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080205_robot.wav", + "robot_duration_sec": 3.52 + }, + { + "timestamp": "20260515_080209", + "started_at": 1778803329.921542, + "user_text": "", + "robot_text": "تمام، مسجلين عندي. أي شيء ثاني؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080209_user.wav", + "user_duration_sec": 0.128, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080209_robot.wav", + "robot_duration_sec": 3.08 + }, + { + "timestamp": "20260515_080221", + "started_at": 1778803341.6103332, + "user_text": "", + "robot_text": "...", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080221_user.wav", + "user_duration_sec": 1.088 + }, + { + "timestamp": "20260515_080234", + "started_at": 1778803354.989505, + "user_text": "هذا مين؟ ", + "robot_text": "هلا والله يا صامت، شحالك؟ مصورنا المبدع، عساك بخير؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080234_user.wav", + "user_duration_sec": 1.408, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080234_robot.wav", + "robot_duration_sec": 4.6 + }, + { + "timestamp": "20260515_080256", + "started_at": 1778803376.1388574, + "user_text": "شو بسوي لك بالعربي؟", + "robot_text": "أنا جاهز لأي مساعدة تحتاجها، سواء معلومات أو ترجمة أو أي شيء ثاني.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080256_user.wav", + "user_duration_sec": 0.96, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080256_robot.wav", + "robot_duration_sec": 5.48 + }, + { + "timestamp": "20260515_080302", + "started_at": 1778803382.7340183, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080302_user.wav", + "user_duration_sec": 1.76 + }, + { + "timestamp": "20260515_080305", + "started_at": 1778803385.8291006, + "user_text": "شو بس وي لك بال عاد ه مين هون", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080305_user.wav", + "user_duration_sec": 1.952 + }, + { + "timestamp": "20260515_080309", + "started_at": 1778803389.0532322, + "user_text": "ชื่อ แบงค์ ", + "robot_text": "صامت مصورنا اللي يصورك ويحب يمزح وياك شرات ما ذكرت لي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080309_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080309_robot.wav", + "robot_duration_sec": 4.6 + }, + { + "timestamp": "20260515_080319", + "started_at": 1778803399.403645, + "user_text": "بن كهه عليك شيء؟", + "robot_text": "ههههه، أكيد! صامت يحب يضيف جو مرح للتصوير. شو بغيت أسويلك الحين؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080319_user.wav", + "user_duration_sec": 0.928, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080319_robot.wav", + "robot_duration_sec": 6.0 + }, + { + "timestamp": "20260515_080329", + "started_at": 1778803409.5110688, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080329_user.wav", + "user_duration_sec": 2.496 + }, + { + "timestamp": "20260515_080337", + "started_at": 1778803417.7394204, + "user_text": "집어 달라고 그래요.", + "robot_text": "شايف اللوحة اللي فوق الباب؟ مكتوب", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080337_user.wav", + "user_duration_sec": 1.024, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080337_robot.wav", + "robot_duration_sec": 2.24 + }, + { + "timestamp": "20260515_080341", + "started_at": 1778803421.0030382, + "user_text": "", + "robot_text": "عليها \"IT MANAGER\". يعني صامت ممكن يكون", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080341_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080341_robot.wav", + "robot_duration_sec": 3.64 + }, + { + "timestamp": "20260515_080342", + "started_at": 1778803422.1499975, + "user_text": "", + "robot_text": "مدير قسم التكنولوجيا عندكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080342_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080342_robot.wav", + "robot_duration_sec": 2.36 + }, + { + "timestamp": "20260515_080345", + "started_at": 1778803425.9800315, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080345_user.wav", + "user_duration_sec": 1.184 + }, + { + "timestamp": "20260515_080348", + "started_at": 1778803428.9489615, + "user_text": "", + "robot_text": "ايه، هو هذا صامت. في شيء ثاني بالك منه؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080348_user.wav", + "user_duration_sec": 1.92, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080348_robot.wav", + "robot_duration_sec": 3.32 + }, + { + "timestamp": "20260515_080401", + "started_at": 1778803441.7404523, + "user_text": "", + "robot_text": "تم، الحين ما بقدر أميز الوجوه مرة ثانية.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080401_user.wav", + "user_duration_sec": 1.088, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080401_robot.wav", + "robot_duration_sec": 3.12 + }, + { + "timestamp": "20260515_080433", + "started_at": 1778803473.8520813, + "user_text": "", + "robot_text": "يا خوي، الكاميرا طفت، ما أقدر أشوف أي شيء الحين.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080433_robot.wav", + "robot_duration_sec": 3.56 + }, + { + "timestamp": "20260515_080438", + "started_at": 1778803478.29367, + "user_text": "ఆ", + "robot_text": "ما أقدر أشوف المكتب يا خوي، لازم تشغل الكاميرا من لوحة التحكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080438_user.wav", + "user_duration_sec": 7.936, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080438_robot.wav", + "robot_duration_sec": 4.44 + }, + { + "timestamp": "20260515_080519", + "started_at": 1778803519.1297877, + "user_text": ".", + "robot_text": "مثل ما خبرتك، الكاميرا مبندة، لو سمحت شغلها من لوحة التحكم عشان أقدر أساعدك.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080519_user.wav", + "user_duration_sec": 7.04, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080519_robot.wav", + "robot_duration_sec": 6.4 + }, + { + "timestamp": "20260515_080555", + "started_at": 1778803555.5243697, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080555_user.wav", + "user_duration_sec": 0.032 + }, + { + "timestamp": "20260515_080555", + "started_at": 1778803555.6336243, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080555_user.wav", + "user_duration_sec": 6.624 + }, + { + "timestamp": "20260515_080615", + "started_at": 1778803575.420792, + "user_text": "خير", + "robot_text": "أكيد أذكر حازم،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080615_user.wav", + "user_duration_sec": 5.824, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080615_robot.wav", + "robot_duration_sec": 1.08 + }, + { + "timestamp": "20260515_080626", + "started_at": 1778803586.774901, + "user_text": ".", + "robot_text": "بس مثل ما خبرتك، ما أقدر أشوفه الحين لأن الكاميرا", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080626_user.wav", + "user_duration_sec": 1.472, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080626_robot.wav", + "robot_duration_sec": 3.08 + }, + { + "timestamp": "20260515_080633", + "started_at": 1778803593.1564424, + "user_text": "", + "robot_text": "مبندة، ومعرفتي", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080633_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080633_robot.wav", + "robot_duration_sec": 0.84 + }, + { + "timestamp": "20260515_080633", + "started_at": 1778803593.604888, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_080633_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_080633_robot.wav", + "robot_duration_sec": 0.32 + }, + { + "timestamp": "20260515_081625", + "started_at": 1778804185.8479338, + "user_text": "", + "robot_text": "مرحبا يا خوي، يبشر بالخير. أوقات الصلاة في دبي اليوم، الجمعة 15 مايو 2026: الفجر على الساعة 4:15 دقيقة الصبح، الظهر 12:30 دقيقة، العصر 4:05 دقائق، المغرب 6:55 دقيقة، والعشاء 8:05 دقائق.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081625_robot.wav", + "robot_duration_sec": 21.0 + }, + { + "timestamp": "20260515_081915", + "started_at": 1778804355.0008562, + "user_text": "", + "robot_text": "مرحبا يا خوي، شحالك؟ آمِرني، في شي تباه؟", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081915_robot.wav", + "robot_duration_sec": 3.4 + }, + { + "timestamp": "20260515_081919", + "started_at": 1778804359.6887746, + "user_text": "", + "robot_text": "هلا والله، الحين أشوفك عدل يا خوي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_081919_user.wav", + "user_duration_sec": 0.096, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081919_robot.wav", + "robot_duration_sec": 2.52 + }, + { + "timestamp": "20260515_081926", + "started_at": 1778804366.1183736, + "user_text": "", + "robot_text": "تمام، الحين أقدر أميّز الناس اللي أعرفهم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_081926_user.wav", + "user_duration_sec": 0.224, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081926_robot.wav", + "robot_duration_sec": 2.88 + }, + { + "timestamp": "20260515_081931", + "started_at": 1778804371.6329637, + "user_text": "", + "robot_text": "الجو في المكتب حلو اليوم، مرتب وهادي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_081931_user.wav", + "user_duration_sec": 0.224, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081931_robot.wav", + "robot_duration_sec": 2.88 + }, + { + "timestamp": "20260515_081937", + "started_at": 1778804377.4027572, + "user_text": "ആദരണീയ", + "robot_text": "ما شاء الله، مكتب مرتب ونظيف.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_081937_user.wav", + "user_duration_sec": 0.512, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081937_robot.wav", + "robot_duration_sec": 2.64 + }, + { + "timestamp": "20260515_081949", + "started_at": 1778804389.00869, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_081949_user.wav", + "user_duration_sec": 0.032 + }, + { + "timestamp": "20260515_081949", + "started_at": 1778804389.138754, + "user_text": "هذا هذا صوره مين؟", + "robot_text": "هذا أستاذ يوسف سعيد لوتاه، المؤسس والرئيس التنفيذي لشركة لوتاه للوقود الحيوي. بعد مدير في شركات الذكاء الاصطناعي والتكنولوجيا.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_081949_user.wav", + "user_duration_sec": 1.28, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_081949_robot.wav", + "robot_duration_sec": 10.24 + }, + { + "timestamp": "20260515_082011", + "started_at": 1778804411.4102132, + "user_text": "కనీ", + "robot_text": "هلا والله يا قسام، شحالك؟ علومك؟ بعدك منورنا بمشاريع الروبوتات؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_082011_user.wav", + "user_duration_sec": 0.384, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_082011_robot.wav", + "robot_duration_sec": 5.24 + }, + { + "timestamp": "20260515_082024", + "started_at": 1778804424.196041, + "user_text": "", + "robot_text": "عفواً، يا خوي، الكاميرا انطفت وما أقدر أشوف الحين، وبعد ما بقدر أميّز الوجوه.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_082024_user.wav", + "user_duration_sec": 1.056, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_082024_robot.wav", + "robot_duration_sec": 5.6 + }, + { + "timestamp": "20260515_083239", + "started_at": 1778805159.6130798, + "user_text": "", + "robot_text": "مرحبا بك يا خوي. كيف حالك اليوم؟ آمِرْني وش تحتاج؟", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083239_robot.wav", + "robot_duration_sec": 4.24 + }, + { + "timestamp": "20260515_083245", + "started_at": 1778805165.2778418, + "user_text": "", + "robot_text": "هلا، الحين أشوفك زين. تفضل، وش بغيت؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083245_user.wav", + "user_duration_sec": 0.32, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083245_robot.wav", + "robot_duration_sec": 3.68 + }, + { + "timestamp": "20260515_083253", + "started_at": 1778805173.7585948, + "user_text": "", + "robot_text": "تمام، الحين أقدر أتعرف على الأشخاص.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083253_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083253_robot.wav", + "robot_duration_sec": 2.92 + }, + { + "timestamp": "20260515_083300", + "started_at": 1778805180.9703467, + "user_text": ".", + "robot_text": "هلا والله يا همام، شحالك؟ عساك بخير. مشروعك كيف ماشي؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083300_user.wav", + "user_duration_sec": 1.792, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083300_robot.wav", + "robot_duration_sec": 4.6 + }, + { + "timestamp": "20260515_083318", + "started_at": 1778805198.9531856, + "user_text": "الحمد لل ه ما شي عا ود", + "robot_text": "تمام الحمد لله. وياك بعد نفس الشيء، الأمور طيبة. منور المكان اليوم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083318_user.wav", + "user_duration_sec": 2.144, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083318_robot.wav", + "robot_duration_sec": 6.28 + }, + { + "timestamp": "20260515_083329", + "started_at": 1778805209.3082454, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083329_user.wav", + "user_duration_sec": 0.224 + }, + { + "timestamp": "20260515_083330", + "started_at": 1778805210.2653205, + "user_text": ".", + "robot_text": "العفو يا همام، ماسوينا شيء. إذا احتجت أي شيء أنا موجود.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083330_user.wav", + "user_duration_sec": 0.832, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083330_robot.wav", + "robot_duration_sec": 4.84 + }, + { + "timestamp": "20260515_083338", + "started_at": 1778805218.2573886, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083338_user.wav", + "user_duration_sec": 0.256 + }, + { + "timestamp": "20260515_083338", + "started_at": 1778805218.6545627, + "user_text": "انا انا مين انا مين انا فين؟", + "robot_text": "أنت همام، مدير المشاريع. ومثل ما أشوف، أنت الحين في المكتب.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083338_user.wav", + "user_duration_sec": 0.96, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083338_robot.wav", + "robot_duration_sec": 4.88 + }, + { + "timestamp": "20260515_083345", + "started_at": 1778805225.5045438, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083345_user.wav", + "user_duration_sec": 1.824 + }, + { + "timestamp": "20260515_083348", + "started_at": 1778805228.5722678, + "user_text": ". Min min kjø re min er du den rapper?", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083348_user.wav", + "user_duration_sec": 2.976 + }, + { + "timestamp": "20260515_083417", + "started_at": 1778805257.6116493, + "user_text": " అనవమి", + "robot_text": "هلا والله يا قسام، شحالك؟ مهندس الروبوتات منورنا. منو اللي وياك؟", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083417_robot.wav", + "robot_duration_sec": 5.68 + }, + { + "timestamp": "20260515_083421", + "started_at": 1778805261.4056509, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083421_user.wav", + "user_duration_sec": 0.256 + }, + { + "timestamp": "20260515_083421", + "started_at": 1778805261.8769617, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083421_user.wav", + "user_duration_sec": 2.592 + }, + { + "timestamp": "20260515_083434", + "started_at": 1778805274.1959803, + "user_text": "", + "robot_text": "هذا همام", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083434_user.wav", + "user_duration_sec": 0.448 + }, + { + "timestamp": "20260515_083435", + "started_at": 1778805275.610895, + "user_text": "", + "robot_text": "لا هذا قسام يا همام، مهندس الروبوتات. هلا بك والله يا قسام، شعلومك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083435_user.wav", + "user_duration_sec": 0.32, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083435_robot.wav", + "robot_duration_sec": 5.76 + }, + { + "timestamp": "20260515_083446", + "started_at": 1778805286.5930977, + "user_text": "", + "robot_text": "تمام، الحين ما بقدر أتعرف على الوجوه. مرحبا بالضيف اللي وياك بعد.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083446_user.wav", + "user_duration_sec": 0.288, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083446_robot.wav", + "robot_duration_sec": 4.92 + }, + { + "timestamp": "20260515_083454", + "started_at": 1778805294.0147464, + "user_text": "", + "robot_text": "disillusioned وياي، يا خوي. الكاميرا انطفت الحين وما أقدر أشوف شيء.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_083454_user.wav", + "user_duration_sec": 0.16, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_083454_robot.wav", + "robot_duration_sec": 4.6 + }, + { + "timestamp": "20260515_084552", + "started_at": 1778805952.7000039, + "user_text": "", + "robot_text": "مرحبا! عشان أقولك كيف الجو، وين مكانك بالضبط؟ وعن المشي، عادة أحسن وقت الصبح أول ما تنش أو متأخر في الليل يوم الجو يبرد شوي.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_084552_robot.wav", + "robot_duration_sec": 10.36 + }, + { + "timestamp": "20260515_092655", + "started_at": 1778808415.8625035, + "user_text": "", + "robot_text": "مرحبا يا خوي. تمام، أنا", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092655_user.wav", + "user_duration_sec": 0.384, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092655_robot.wav", + "robot_duration_sec": 1.76 + }, + { + "timestamp": "20260515_092658", + "started_at": 1778808418.7591166, + "user_text": "", + "robot_text": "جاهز. كيف أقدر أساعدك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092658_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092658_robot.wav", + "robot_duration_sec": 2.24 + }, + { + "timestamp": "20260515_092659", + "started_at": 1778808419.2552266, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092659_user.wav", + "user_duration_sec": 1.152 + }, + { + "timestamp": "20260515_092702", + "started_at": 1778808422.378509, + "user_text": "", + "robot_text": "هلا، الحين أشوفك زين. أمورنا طيبة.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092702_user.wav", + "user_duration_sec": 1.088, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092702_robot.wav", + "robot_duration_sec": 3.04 + }, + { + "timestamp": "20260515_092713", + "started_at": 1778808433.4802406, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092713_user.wav", + "user_duration_sec": 0.192 + }, + { + "timestamp": "20260515_092713", + "started_at": 1778808433.9190485, + "user_text": "う う う 。", + "robot_text": "تمام، الحين أقدر أميز الأشخاص اللي أعرفهم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092713_user.wav", + "user_duration_sec": 1.152, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092713_robot.wav", + "robot_duration_sec": 3.0 + }, + { + "timestamp": "20260515_092725", + "started_at": 1778808445.449916, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092725_user.wav", + "user_duration_sec": 0.192 + }, + { + "timestamp": "20260515_092727", + "started_at": 1778808447.2239919, + "user_text": "Так.", + "robot_text": "أقدر أميز الأشخاص اللي أعرفهم. بس حالياً ما أشوف حد واضح قدامي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092727_user.wav", + "user_duration_sec": 0.48, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092727_robot.wav", + "robot_duration_sec": 4.68 + }, + { + "timestamp": "20260515_092732", + "started_at": 1778808452.5665224, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092732_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092732_robot.wav", + "robot_duration_sec": 0.48 + }, + { + "timestamp": "20260515_092733", + "started_at": 1778808453.7777894, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092733_user.wav", + "user_duration_sec": 1.216 + }, + { + "timestamp": "20260515_092741", + "started_at": 1778808461.4609826, + "user_text": "1 2 3 4", + "robot_text": "مرحبا بك يا ضيفنا، شرفتنا.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092741_user.wav", + "user_duration_sec": 0.8, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092741_robot.wav", + "robot_duration_sec": 2.36 + }, + { + "timestamp": "20260515_092752", + "started_at": 1778808472.1780024, + "user_text": "", + "robot_text": "هلا والله يا قصي، شحالك؟ ويا مرحبا بالضيفة الكريمة.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092752_user.wav", + "user_duration_sec": 4.8, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092752_robot.wav", + "robot_duration_sec": 3.92 + }, + { + "timestamp": "20260515_092806", + "started_at": 1778808486.6427715, + "user_text": "speak in English.", + "robot_text": "Sure thing! How can I help you both today?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092806_user.wav", + "user_duration_sec": 1.568, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092806_robot.wav", + "robot_duration_sec": 2.64 + }, + { + "timestamp": "20260515_092814", + "started_at": 1778808494.9139514, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092814_user.wav", + "user_duration_sec": 7.392 + }, + { + "timestamp": "20260515_092840", + "started_at": 1778808520.1893198, + "user_text": "?", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092840_user.wav", + "user_duration_sec": 3.264 + }, + { + "timestamp": "20260515_092846", + "started_at": 1778808526.4298105, + "user_text": "O kay , yeah , ye ah , the pic ture , yeah.", + "robot_text": "I'm sorry,", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092846_user.wav", + "user_duration_sec": 2.752 + }, + { + "timestamp": "20260515_092853", + "started_at": 1778808533.2171433, + "user_text": "I don' t know if you can re mind by di fferen t pic tures for one person. I did it.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092853_user.wav", + "user_duration_sec": 4.256 + }, + { + "timestamp": "20260515_092900", + "started_at": 1778808540.3006194, + "user_text": "", + "robot_text": "Yes, I can recognize people based on different pictures now. Is there someone you'd like me to verify?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092900_user.wav", + "user_duration_sec": 0.256, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092900_robot.wav", + "robot_duration_sec": 6.4 + }, + { + "timestamp": "20260515_092910", + "started_at": 1778808550.8778107, + "user_text": "Gre at, you are amazing.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092910_user.wav", + "user_duration_sec": 1.44 + }, + { + "timestamp": "20260515_092915", + "started_at": 1778808555.6748326, + "user_text": "What can you see now?", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092915_user.wav", + "user_duration_sec": 1.408 + }, + { + "timestamp": "20260515_092919", + "started_at": 1778808559.5810645, + "user_text": "باز نفر ", + "robot_text": "I can see you now. It looks like you're in an office environment, with a frosted glass door that says \"IT MANAGER\" on it.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092919_user.wav", + "user_duration_sec": 0.352, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092919_robot.wav", + "robot_duration_sec": 7.84 + }, + { + "timestamp": "20260515_092931", + "started_at": 1778808571.7623181, + "user_text": "", + "robot_text": "هلا والله يا قصي، شحالك؟ ومرحباً بالضيفة الكريمة.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092931_user.wav", + "user_duration_sec": 1.344, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092931_robot.wav", + "robot_duration_sec": 3.96 + }, + { + "timestamp": "20260515_092942", + "started_at": 1778808582.4596295, + "user_text": "Let's do that for finish.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092942_user.wav", + "user_duration_sec": 1.152 + }, + { + "timestamp": "20260515_092945", + "started_at": 1778808585.406496, + "user_text": "Can you see her fa ce? I can defy her face.", + "robot_text": "أكيد، أقدر أشوف وجهها. بس مرحباً بك يا ضيفتنا، ما عندي معلومات عنك في الوقت الحالي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092945_user.wav", + "user_duration_sec": 3.104, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_092945_robot.wav", + "robot_duration_sec": 6.04 + }, + { + "timestamp": "20260515_092958", + "started_at": 1778808598.3650396, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_092958_user.wav", + "user_duration_sec": 0.8 + }, + { + "timestamp": "20260515_093001", + "started_at": 1778808601.9152248, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093001_user.wav", + "user_duration_sec": 0.544 + }, + { + "timestamp": "20260515_093002", + "started_at": 1778808602.7251916, + "user_text": "Speaking of", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093002_user.wav", + "user_duration_sec": 0.512 + }, + { + "timestamp": "20260515_093006", + "started_at": 1778808606.3671908, + "user_text": "", + "robot_text": "هلا، الحين أقدر أشوفك تمام وأميز", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093006_user.wav", + "user_duration_sec": 0.896, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093006_robot.wav", + "robot_duration_sec": 2.08 + }, + { + "timestamp": "20260515_093008", + "started_at": 1778808608.083727, + "user_text": "", + "robot_text": "الأشخاص.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093008_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093008_robot.wav", + "robot_duration_sec": 1.04 + }, + { + "timestamp": "20260515_093010", + "started_at": 1778808610.8150465, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093010_user.wav", + "user_duration_sec": 0.032 + }, + { + "timestamp": "20260515_093011", + "started_at": 1778808611.0065467, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093011_user.wav", + "user_duration_sec": 0.48 + }, + { + "timestamp": "20260515_093012", + "started_at": 1778808612.4403443, + "user_text": "Now what do you see?", + "robot_text": "ما زلت أشوفك والباب اللي خلفك، بس ما فيه أشخاص ثانيين واضحين جدامي الحين.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093012_user.wav", + "user_duration_sec": 0.384, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093012_robot.wav", + "robot_duration_sec": 5.16 + }, + { + "timestamp": "20260515_093019", + "started_at": 1778808619.2790315, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093019_user.wav", + "user_duration_sec": 1.28 + }, + { + "timestamp": "20260515_093021", + "started_at": 1778808621.231028, + "user_text": "", + "robot_text": "ما أقدر أميز الشخص اللي واقف نالحين. هل تباني أساعدك في شيء ثاني؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093021_user.wav", + "user_duration_sec": 1.344, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093021_robot.wav", + "robot_duration_sec": 4.76 + }, + { + "timestamp": "20260515_093029", + "started_at": 1778808629.9927347, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093029_user.wav", + "user_duration_sec": 0.288 + }, + { + "timestamp": "20260515_093030", + "started_at": 1778808630.5072486, + "user_text": "English please.", + "robot_text": "Sure. There is a person walking past in the background.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093030_user.wav", + "user_duration_sec": 1.472, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093030_robot.wav", + "robot_duration_sec": 3.8 + }, + { + "timestamp": "20260515_093040", + "started_at": 1778808640.8527079, + "user_text": "ป่วย ทําไม คะ", + "robot_text": "I'm sorry, I didn't catch that. Could you please clarify?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093040_user.wav", + "user_duration_sec": 3.68, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093040_robot.wav", + "robot_duration_sec": 3.76 + }, + { + "timestamp": "20260515_093106", + "started_at": 1778808666.1763866, + "user_text": "", + "robot_text": "مرحبا بك! شحالك يا خوي؟ نورت المكان.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093106_user.wav", + "user_duration_sec": 1.024, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093106_robot.wav", + "robot_duration_sec": 3.28 + }, + { + "timestamp": "20260515_093114", + "started_at": 1778808674.8401976, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093114_user.wav", + "user_duration_sec": 2.592 + }, + { + "timestamp": "20260515_093120", + "started_at": 1778808680.6628096, + "user_text": "Ska du klicka sedan?", + "robot_text": "أشوف حرمة تلبس قبعة وتطالع تلفونها، وخلفها في مكتب.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093120_user.wav", + "user_duration_sec": 0.16, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093120_robot.wav", + "robot_duration_sec": 4.68 + }, + { + "timestamp": "20260515_093126", + "started_at": 1778808686.3805344, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093126_user.wav", + "user_duration_sec": 0.32 + }, + { + "timestamp": "20260515_093126", + "started_at": 1778808686.9214926, + "user_text": "Yes yes. Good sheep.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093126_user.wav", + "user_duration_sec": 1.568 + }, + { + "timestamp": "20260515_093129", + "started_at": 1778808689.7912679, + "user_text": "세 번 하나요? 벌써 시작.", + "robot_text": "هذي TT WANG، مديرة المبيعات", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093129_user.wav", + "user_duration_sec": 0.832, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093129_robot.wav", + "robot_duration_sec": 1.92 + }, + { + "timestamp": "20260515_093133", + "started_at": 1778808693.2217903, + "user_text": "", + "robot_text": "في Wisemate. هلا والله يا TT، شحالج؟ عساج بخير؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093133_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093133_robot.wav", + "robot_duration_sec": 4.68 + }, + { + "timestamp": "20260515_093137", + "started_at": 1778808697.4870217, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093137_user.wav", + "user_duration_sec": 1.376 + }, + { + "timestamp": "20260515_093139", + "started_at": 1778808699.471395, + "user_text": "Oh , hi . Nice to meet you.", + "robot_text": "تسرك العلوم يا بنت الحلال، منورة الدنيا.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093139_user.wav", + "user_duration_sec": 2.304, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093139_robot.wav", + "robot_duration_sec": 3.12 + }, + { + "timestamp": "20260515_093144", + "started_at": 1778808704.3541894, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093144_user.wav", + "user_duration_sec": 1.44 + }, + { + "timestamp": "20260515_093146", + "started_at": 1778808706.4134073, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093146_user.wav", + "user_duration_sec": 1.216 + }, + { + "timestamp": "20260515_093149", + "started_at": 1778808709.1484604, + "user_text": "", + "robot_text": "Sure thing! How can I help you today?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093149_user.wav", + "user_duration_sec": 0.256, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093149_robot.wav", + "robot_duration_sec": 1.92 + }, + { + "timestamp": "20260515_093151", + "started_at": 1778808711.835948, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093151_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093151_robot.wav", + "robot_duration_sec": 0.4 + }, + { + "timestamp": "20260515_093152", + "started_at": 1778808712.2246015, + "user_text": "Yeah, mystery.", + "robot_text": "It's great", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093152_user.wav", + "user_duration_sec": 0.8 + }, + { + "timestamp": "20260515_093154", + "started_at": 1778808714.7327406, + "user_text": "โอ ไป นั่ง ทํา", + "robot_text": "It's great to see", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093154_user.wav", + "user_duration_sec": 0.96, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093154_robot.wav", + "robot_duration_sec": 1.08 + }, + { + "timestamp": "20260515_093157", + "started_at": 1778808717.5985093, + "user_text": "", + "robot_text": "to see you again, TT! Is", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093157_user.wav", + "user_duration_sec": 2.688, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093157_robot.wav", + "robot_duration_sec": 2.44 + }, + { + "timestamp": "20260515_093204", + "started_at": 1778808724.0087736, + "user_text": "", + "robot_text": "everything going well?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093204_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093204_robot.wav", + "robot_duration_sec": 1.48 + }, + { + "timestamp": "20260515_093204", + "started_at": 1778808724.8495002, + "user_text": "and say my name from. Hello.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093204_user.wav", + "user_duration_sec": 3.84 + }, + { + "timestamp": "20260515_093215", + "started_at": 1778808735.3556094, + "user_text": "เดี๋ยว นะ มี", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093215_user.wav", + "user_duration_sec": 0.224 + }, + { + "timestamp": "20260515_093215", + "started_at": 1778808735.8533013, + "user_text": ".", + "robot_text": "هلا والله ليام! شحالك يا مدير المبيعات في Wisemate؟ منورنا!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093215_user.wav", + "user_duration_sec": 1.952, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093215_robot.wav", + "robot_duration_sec": 4.52 + }, + { + "timestamp": "20260515_093225", + "started_at": 1778808745.4753637, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093225_user.wav", + "user_duration_sec": 0.32 + }, + { + "timestamp": "20260515_093226", + "started_at": 1778808746.1022458, + "user_text": "E nglish Eng lish", + "robot_text": "Ah, Liam!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093226_user.wav", + "user_duration_sec": 1.248, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093226_robot.wav", + "robot_duration_sec": 0.96 + }, + { + "timestamp": "20260515_093234", + "started_at": 1778808754.4977877, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093234_user.wav", + "user_duration_sec": 1.376 + }, + { + "timestamp": "20260515_093236", + "started_at": 1778808756.7329848, + "user_text": "guys guys guys", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093236_user.wav", + "user_duration_sec": 1.216 + }, + { + "timestamp": "20260515_093239", + "started_at": 1778808759.427639, + "user_text": "他的電子。 ", + "robot_text": "This looks like an office environment, with desks and partitions. I also see a sign that says \"IT MANAGER\".", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093239_user.wav", + "user_duration_sec": 0.224, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093239_robot.wav", + "robot_duration_sec": 7.8 + }, + { + "timestamp": "20260515_093245", + "started_at": 1778808765.9777915, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093245_user.wav", + "user_duration_sec": 0.32 + }, + { + "timestamp": "20260515_093246", + "started_at": 1778808766.5645595, + "user_text": "sta nding next to you . What is", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093246_user.wav", + "user_duration_sec": 3.264 + }, + { + "timestamp": "20260515_093258", + "started_at": 1778808778.2300367, + "user_text": ".", + "robot_text": "The lady is just standing there, looking at her phone most likely. Is there anything specific you'd like to know?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093258_user.wav", + "user_duration_sec": 0.256, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093258_robot.wav", + "robot_duration_sec": 6.8 + }, + { + "timestamp": "20260515_093303", + "started_at": 1778808783.7272348, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093303_user.wav", + "user_duration_sec": 0.608 + }, + { + "timestamp": "20260515_093304", + "started_at": 1778808784.760625, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093304_user.wav", + "user_duration_sec": 1.28 + }, + { + "timestamp": "20260515_093307", + "started_at": 1778808787.5414374, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093307_user.wav", + "user_duration_sec": 2.816 + }, + { + "timestamp": "20260515_093311", + "started_at": 1778808791.693397, + "user_text": "Okay . Now do you see Liam?", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093311_user.wav", + "user_duration_sec": 0.96 + }, + { + "timestamp": "20260515_093314", + "started_at": 1778808794.2517202, + "user_text": "Can you see it?", + "robot_text": "Yes, I can see Liam now! Hello again, Liam! How are things today?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093314_user.wav", + "user_duration_sec": 0.192, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093314_robot.wav", + "robot_duration_sec": 4.48 + }, + { + "timestamp": "20260515_093322", + "started_at": 1778808802.0323343, + "user_text": "", + "robot_text": "That is Liam again, the Sales", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093322_user.wav", + "user_duration_sec": 3.744, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093322_robot.wav", + "robot_duration_sec": 1.64 + }, + { + "timestamp": "20260515_093330", + "started_at": 1778808810.0503805, + "user_text": "", + "robot_text": "Director from Wisemate. We just greeted him!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093330_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093330_robot.wav", + "robot_duration_sec": 3.28 + }, + { + "timestamp": "20260515_093332", + "started_at": 1778808812.4769826, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093332_user.wav", + "user_duration_sec": 1.76 + }, + { + "timestamp": "20260515_093334", + "started_at": 1778808814.7510154, + "user_text": "", + "robot_text": "هلا حازم، شحالك يا مدير عام", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093334_user.wav", + "user_duration_sec": 7.072, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093334_robot.wav", + "robot_duration_sec": 1.88 + }, + { + "timestamp": "20260515_093357", + "started_at": 1778808837.309134, + "user_text": "", + "robot_text": "في ys lootah tech؟ منور المكان اليوم!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093357_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093357_robot.wav", + "robot_duration_sec": 3.16 + }, + { + "timestamp": "20260515_093402", + "started_at": 1778808842.713585, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093402_user.wav", + "user_duration_sec": 2.24 + }, + { + "timestamp": "20260515_093410", + "started_at": 1778808850.5478997, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093410_user.wav", + "user_duration_sec": 1.536 + }, + { + "timestamp": "20260515_093413", + "started_at": 1778808853.1925826, + "user_text": "es pe cially for the language", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093413_user.wav", + "user_duration_sec": 1.248 + }, + { + "timestamp": "20260515_093415", + "started_at": 1778808855.822921, + "user_text": "", + "robot_text": "Welcome, Hazem!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093415_user.wav", + "user_duration_sec": 3.168 + }, + { + "timestamp": "20260515_093422", + "started_at": 1778808862.8056698, + "user_text": "", + "robot_text": "You haven't been in a while! How's the General Manager position going at ys lootah tech?", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093422_user.wav", + "user_duration_sec": 1.056, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093422_robot.wav", + "robot_duration_sec": 5.2 + }, + { + "timestamp": "20260515_093429", + "started_at": 1778808869.3247516, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093429_user.wav", + "user_duration_sec": 0.384 + }, + { + "timestamp": "20260515_093429", + "started_at": 1778808869.9292102, + "user_text": "to like what to mean.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093429_user.wav", + "user_duration_sec": 2.176 + }, + { + "timestamp": "20260515_093435", + "started_at": 1778808875.4767356, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093435_user.wav", + "user_duration_sec": 0.288 + }, + { + "timestamp": "20260515_093435", + "started_at": 1778808875.9400072, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093435_user.wav", + "user_duration_sec": 4.544 + }, + { + "timestamp": "20260515_093442", + "started_at": 1778808882.6966286, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093442_user.wav", + "user_duration_sec": 4.608 + }, + { + "timestamp": "20260515_093450", + "started_at": 1778808890.574166, + "user_text": "طب ما كت سموز.", + "robot_text": "Sounds good! Is there anything", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093450_user.wav", + "user_duration_sec": 2.464, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093450_robot.wav", + "robot_duration_sec": 1.84 + }, + { + "timestamp": "20260515_093454", + "started_at": 1778808894.9215496, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093454_user.wav", + "user_duration_sec": 0.576 + }, + { + "timestamp": "20260515_093456", + "started_at": 1778808896.5344315, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093456_user.wav", + "user_duration_sec": 13.44 + }, + { + "timestamp": "20260515_093519", + "started_at": 1778808919.7640326, + "user_text": "", + "robot_text": "هلا والله يا قسام، شحالك؟ عسى أمورك كلها طيبة؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093519_user.wav", + "user_duration_sec": 1.376, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093519_robot.wav", + "robot_duration_sec": 3.6 + }, + { + "timestamp": "20260515_093525", + "started_at": 1778808925.0223677, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093525_user.wav", + "user_duration_sec": 0.928 + }, + { + "timestamp": "20260515_093526", + "started_at": 1778808926.753946, + "user_text": ". I'm", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093526_user.wav", + "user_duration_sec": 6.784 + }, + { + "timestamp": "20260515_093537", + "started_at": 1778808937.1660523, + "user_text": "", + "robot_text": "هلا حازم", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093537_user.wav", + "user_duration_sec": 3.328 + }, + { + "timestamp": "20260515_093542", + "started_at": 1778808942.0090811, + "user_text": "for two week s . I just dri ve to uh pi ck up my frien d s in the . I took that.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093542_user.wav", + "user_duration_sec": 6.432 + }, + { + "timestamp": "20260515_093551", + "started_at": 1778808951.3076293, + "user_text": "So that 's when the situation still", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093551_user.wav", + "user_duration_sec": 2.048 + }, + { + "timestamp": "20260515_093554", + "started_at": 1778808954.6460414, + "user_text": "", + "robot_text": "منور حازم، شحالك يا مدير في ys", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093554_user.wav", + "user_duration_sec": 3.296, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093554_robot.wav", + "robot_duration_sec": 2.12 + }, + { + "timestamp": "20260515_093601", + "started_at": 1778808961.6352406, + "user_text": "", + "robot_text": "lootah tech؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093601_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093601_robot.wav", + "robot_duration_sec": 1.08 + }, + { + "timestamp": "20260515_093602", + "started_at": 1778808962.0792303, + "user_text": "", + "robot_text": "وياك ليام بعد،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093602_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093602_robot.wav", + "robot_duration_sec": 1.6 + }, + { + "timestamp": "20260515_093602", + "started_at": 1778808962.6601222, + "user_text": "", + "robot_text": "هلا والله!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093602_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093602_robot.wav", + "robot_duration_sec": 1.0 + }, + { + "timestamp": "20260515_093603", + "started_at": 1778808963.6594806, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093603_user.wav", + "user_duration_sec": 2.176 + }, + { + "timestamp": "20260515_093607", + "started_at": 1778808967.0852342, + "user_text": " Posso ir para trás?", + "robot_text": "العفو، حياكم الله! أي خدمة ثانية تحتاجونها؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093607_user.wav", + "user_duration_sec": 4.448, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093607_robot.wav", + "robot_duration_sec": 3.8 + }, + { + "timestamp": "20260515_093618", + "started_at": 1778808978.5019813, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093618_user.wav", + "user_duration_sec": 0.704 + }, + { + "timestamp": "20260515_093620", + "started_at": 1778808980.1916487, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093620_user.wav", + "user_duration_sec": 14.88 + }, + { + "timestamp": "20260515_093703", + "started_at": 1778809023.6500554, + "user_text": "", + "robot_text": "العفو، بالتوفيق", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093703_user.wav", + "user_duration_sec": 1.824, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093703_robot.wav", + "robot_duration_sec": 1.0 + }, + { + "timestamp": "20260515_093707", + "started_at": 1778809027.8983266, + "user_text": "Let me have you.", + "robot_text": "يا هلا بـ TT WANG! منورة شركة Wisemate،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093707_user.wav", + "user_duration_sec": 1.984, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093707_robot.wav", + "robot_duration_sec": 2.72 + }, + { + "timestamp": "20260515_093711", + "started_at": 1778809031.0617394, + "user_text": "", + "robot_text": "شحالج؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093711_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093711_robot.wav", + "robot_duration_sec": 1.72 + }, + { + "timestamp": "20260515_093715", + "started_at": 1778809035.325638, + "user_text": "他 不 是 如 果 要 狗 嗎 ?我 說 送 你 個 價。", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093715_user.wav", + "user_duration_sec": 7.072 + }, + { + "timestamp": "20260515_093729", + "started_at": 1778809049.0277343, + "user_text": "", + "robot_text": "بالتوفيق ان شاء الله!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093729_user.wav", + "user_duration_sec": 1.184, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093729_robot.wav", + "robot_duration_sec": 1.4 + }, + { + "timestamp": "20260515_093735", + "started_at": 1778809055.755901, + "user_text": "Ja.", + "robot_text": "مع السلامة وكل التوفيق ان شاء شاء الله.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093735_user.wav", + "user_duration_sec": 6.496, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093735_robot.wav", + "robot_duration_sec": 2.16 + }, + { + "timestamp": "20260515_093808", + "started_at": 1778809088.0410483, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093808_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093808_robot.wav", + "robot_duration_sec": 0.24 + }, + { + "timestamp": "20260515_093808", + "started_at": 1778809088.7037902, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093808_user.wav", + "user_duration_sec": 1.28 + }, + { + "timestamp": "20260515_093812", + "started_at": 1778809092.058754, + "user_text": "ตอบ หรือ ยัง เฮ้ย", + "robot_text": "ربي يحفظكم، في أمان الله.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093812_user.wav", + "user_duration_sec": 0.992, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093812_robot.wav", + "robot_duration_sec": 2.08 + }, + { + "timestamp": "20260515_093820", + "started_at": 1778809100.1867275, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093820_user.wav", + "user_duration_sec": 0.064 + }, + { + "timestamp": "20260515_093820", + "started_at": 1778809100.411773, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093820_user.wav", + "user_duration_sec": 7.584 + }, + { + "timestamp": "20260515_093833", + "started_at": 1778809113.6258783, + "user_text": ". Bun. Bun.", + "robot_text": "حياكم الله كلكم! شخباركم؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093833_user.wav", + "user_duration_sec": 2.592, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093833_robot.wav", + "robot_duration_sec": 2.0 + }, + { + "timestamp": "20260515_093838", + "started_at": 1778809118.4745846, + "user_text": "", + "robot_text": "عساكم بخير؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093838_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093838_robot.wav", + "robot_duration_sec": 1.44 + }, + { + "timestamp": "20260515_093838", + "started_at": 1778809118.8931139, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093838_user.wav", + "user_duration_sec": 1.792 + }, + { + "timestamp": "20260515_093841", + "started_at": 1778809121.7110543, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093841_user.wav", + "user_duration_sec": 5.6 + }, + { + "timestamp": "20260515_093853", + "started_at": 1778809133.1401792, + "user_text": "Havyes mm", + "robot_text": "تمام، أشوفكم على خير إن شاء الله.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093853_user.wav", + "user_duration_sec": 1.024, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093853_robot.wav", + "robot_duration_sec": 2.12 + }, + { + "timestamp": "20260515_093856", + "started_at": 1778809136.644915, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093856_user.wav", + "user_duration_sec": 1.024 + }, + { + "timestamp": "20260515_093858", + "started_at": 1778809138.8834007, + "user_text": "", + "robot_text": "بالتوفيق! مع السلامة.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093858_user.wav", + "user_duration_sec": 1.28, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093858_robot.wav", + "robot_duration_sec": 1.64 + }, + { + "timestamp": "20260515_093901", + "started_at": 1778809141.2846725, + "user_text": ".", + "robot_text": "حياكم الله!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093901_user.wav", + "user_duration_sec": 0.736, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093901_robot.wav", + "robot_duration_sec": 1.12 + }, + { + "timestamp": "20260515_093907", + "started_at": 1778809147.8680582, + "user_text": "", + "robot_text": "للأسف، الكاميرا انطفت وما بقدر أشوف كل اللي في الغرفة الحين، وعقب ما بقدر أميز الوجوه لو اشتغلت مرة ثانية.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_093907_user.wav", + "user_duration_sec": 8.384, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_093907_robot.wav", + "robot_duration_sec": 7.4 + }, + { + "timestamp": "20260515_094006", + "started_at": 1778809206.6012864, + "user_text": "Thank you so much.", + "robot_text": "العفو، أي وقت!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094006_user.wav", + "user_duration_sec": 0.896, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094006_robot.wav", + "robot_duration_sec": 1.6 + }, + { + "timestamp": "20260515_094013", + "started_at": 1778809213.5665903, + "user_text": "", + "robot_text": "في أمان الله.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094013_user.wav", + "user_duration_sec": 4.64, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094013_robot.wav", + "robot_duration_sec": 1.0 + }, + { + "timestamp": "20260515_094024", + "started_at": 1778809224.8354383, + "user_text": "", + "robot_text": "هلا، الحين أشوفك زين، ومبين", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094024_user.wav", + "user_duration_sec": 1.504, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094024_robot.wav", + "robot_duration_sec": 1.76 + }, + { + "timestamp": "20260515_094030", + "started_at": 1778809230.808465, + "user_text": "", + "robot_text": "عندي بعد منو وياك.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094030_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094030_robot.wav", + "robot_duration_sec": 2.28 + }, + { + "timestamp": "20260515_094033", + "started_at": 1778809233.5565565, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094033_user.wav", + "user_duration_sec": 0.768 + }, + { + "timestamp": "20260515_094034", + "started_at": 1778809234.5569935, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094034_user.wav", + "user_duration_sec": 0.192 + }, + { + "timestamp": "20260515_094035", + "started_at": 1778809235.6109018, + "user_text": "", + "robot_text": "تمام، في انتظار", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094035_user.wav", + "user_duration_sec": 1.152 + }, + { + "timestamp": "20260515_094037", + "started_at": 1778809237.8618693, + "user_text": "อืม", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094037_user.wav", + "user_duration_sec": 1.568 + }, + { + "timestamp": "20260515_094041", + "started_at": 1778809241.3059475, + "user_text": "", + "robot_text": "هلا والله بحازم، منور يا مدير! وشحالك اليوم؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094041_user.wav", + "user_duration_sec": 1.856, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094041_robot.wav", + "robot_duration_sec": 3.84 + }, + { + "timestamp": "20260515_094048", + "started_at": 1778809248.4348354, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094048_user.wav", + "user_duration_sec": 0.192 + }, + { + "timestamp": "20260515_094049", + "started_at": 1778809249.749145, + "user_text": "", + "robot_text": "هلا قسام، شخبارك؟ عساك بخير ومرتاح يا مهندس الروبوتات؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094049_user.wav", + "user_duration_sec": 1.344, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094049_robot.wav", + "robot_duration_sec": 4.8 + }, + { + "timestamp": "20260515_094058", + "started_at": 1778809258.7522726, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094058_user.wav", + "user_duration_sec": 0.48 + }, + { + "timestamp": "20260515_094059", + "started_at": 1778809259.6619916, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094059_user.wav", + "user_duration_sec": 14.048 + }, + { + "timestamp": "20260515_094120", + "started_at": 1778809280.8421795, + "user_text": "by the video. ", + "robot_text": "تمام، خذوا راحتكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094120_user.wav", + "user_duration_sec": 0.736, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094120_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260515_094127", + "started_at": 1778809287.1448166, + "user_text": "", + "robot_text": "بالتوفيق! ونورتوا المكان.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094127_user.wav", + "user_duration_sec": 2.08, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094127_robot.wav", + "robot_duration_sec": 2.24 + }, + { + "timestamp": "20260515_094132", + "started_at": 1778809292.3170307, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094132_user.wav", + "user_duration_sec": 0.512 + }, + { + "timestamp": "20260515_094134", + "started_at": 1778809294.2969973, + "user_text": ".", + "robot_text": "هلا تي تي WANG، شحالج؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094134_user.wav", + "user_duration_sec": 9.408, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094134_robot.wav", + "robot_duration_sec": 1.64 + }, + { + "timestamp": "20260515_094153", + "started_at": 1778809313.3011642, + "user_text": "", + "robot_text": "عسى كل أمورك طيبة", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094153_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094153_robot.wav", + "robot_duration_sec": 1.6 + }, + { + "timestamp": "20260515_094153", + "started_at": 1778809313.7676032, + "user_text": "", + "robot_text": "مديرة المبيعات في Wisemate؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094153_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094153_robot.wav", + "robot_duration_sec": 1.6 + }, + { + "timestamp": "20260515_094154", + "started_at": 1778809314.214365, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094154_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094154_robot.wav", + "robot_duration_sec": 0.52 + }, + { + "timestamp": "20260515_094154", + "started_at": 1778809314.6999533, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094154_user.wav", + "user_duration_sec": 1.568 + }, + { + "timestamp": "20260515_094159", + "started_at": 1778809319.1421852, + "user_text": ". Stop.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094159_user.wav", + "user_duration_sec": 4.192 + }, + { + "timestamp": "20260515_094209", + "started_at": 1778809329.7506063, + "user_text": "", + "robot_text": "بالتوفيق لكم كلكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094209_user.wav", + "user_duration_sec": 1.888, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094209_robot.wav", + "robot_duration_sec": 1.72 + }, + { + "timestamp": "20260515_094216", + "started_at": 1778809336.1121686, + "user_text": "So I will ask them to send word.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094216_user.wav", + "user_duration_sec": 6.048 + }, + { + "timestamp": "20260515_094230", + "started_at": 1778809350.217812, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094230_user.wav", + "user_duration_sec": 2.08 + }, + { + "timestamp": "20260515_094234", + "started_at": 1778809354.0501943, + "user_text": "Ну, ні, ні, ні, конечно, це", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094234_user.wav", + "user_duration_sec": 0.992 + }, + { + "timestamp": "20260515_094236", + "started_at": 1778809356.8670454, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094236_user.wav", + "user_duration_sec": 1.376 + }, + { + "timestamp": "20260515_094244", + "started_at": 1778809364.2052708, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094244_user.wav", + "user_duration_sec": 0.384 + }, + { + "timestamp": "20260515_094244", + "started_at": 1778809364.8692424, + "user_text": "Oh.", + "robot_text": "هلا والله يا قسام! ومرحباً تي تي وانغ، منورين! شحالكم اليوم؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094244_user.wav", + "user_duration_sec": 1.248, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094244_robot.wav", + "robot_duration_sec": 4.68 + }, + { + "timestamp": "20260515_094256", + "started_at": 1778809376.3023887, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094256_user.wav", + "user_duration_sec": 0.032 + }, + { + "timestamp": "20260515_094256", + "started_at": 1778809376.4119, + "user_text": ".", + "robot_text": ".", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094256_user.wav", + "user_duration_sec": 4.928 + }, + { + "timestamp": "20260515_094324", + "started_at": 1778809404.3362288, + "user_text": "ตัด ตัด ตรง นี้ เลย นะ", + "robot_text": "جو هادي في المكتب،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094324_user.wav", + "user_duration_sec": 3.456 + }, + { + "timestamp": "20260515_094402", + "started_at": 1778809442.621018, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094402_user.wav", + "user_duration_sec": 8.352 + }, + { + "timestamp": "20260515_094416", + "started_at": 1778809456.6309686, + "user_text": "", + "robot_text": "ما أشوف حد حالياً، بس المكتب مرتب", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094416_user.wav", + "user_duration_sec": 2.784, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094416_robot.wav", + "robot_duration_sec": 2.36 + }, + { + "timestamp": "20260515_094421", + "started_at": 1778809461.4685996, + "user_text": "", + "robot_text": "ورايق.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094421_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094421_robot.wav", + "robot_duration_sec": 1.2 + }, + { + "timestamp": "20260515_094423", + "started_at": 1778809463.4757385, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094423_user.wav", + "user_duration_sec": 0.608 + }, + { + "timestamp": "20260515_094424", + "started_at": 1778809464.9392118, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094424_user.wav", + "user_duration_sec": 21.472 + }, + { + "timestamp": "20260515_094457", + "started_at": 1778809497.9940362, + "user_text": "", + "robot_text": "ما في أي", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094457_user.wav", + "user_duration_sec": 2.72 + }, + { + "timestamp": "20260515_094504", + "started_at": 1778809504.0029688, + "user_text": "Gre at , gre at . We just di d I just nee d to sho w the video today and to show them o kay . al ready and then we can just know um lear n to the know ledge from to do that.", + "robot_text": "تمام، خذوا راحتكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094504_user.wav", + "user_duration_sec": 12.224, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094504_robot.wav", + "robot_duration_sec": 1.52 + }, + { + "timestamp": "20260515_094526", + "started_at": 1778809526.083124, + "user_text": "[noise]", + "robot_text": "اوكي، أنا موجود لو احتجتوا أي شيء.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094526_user.wav", + "user_duration_sec": 0.992, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094526_robot.wav", + "robot_duration_sec": 2.44 + }, + { + "timestamp": "20260515_094529", + "started_at": 1778809529.7873862, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094529_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094529_robot.wav", + "robot_duration_sec": 0.12 + }, + { + "timestamp": "20260515_094532", + "started_at": 1778809532.1946657, + "user_text": ". I like to.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094532_user.wav", + "user_duration_sec": 15.584 + }, + { + "timestamp": "20260515_094616", + "started_at": 1778809576.048343, + "user_text": "So that is like we can if this is there and le ft into one.", + "robot_text": "تمام، اللي يريحكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094616_user.wav", + "user_duration_sec": 5.088, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094616_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260515_094627", + "started_at": 1778809587.1029334, + "user_text": "აუ სიბიონით ხო?", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094627_user.wav", + "user_duration_sec": 5.568 + }, + { + "timestamp": "20260515_094637", + "started_at": 1778809597.710454, + "user_text": "Oh goo d, all good actua lly . So I 'm going to sho w view s to sho w functions.", + "robot_text": "شفت مهندس الروبوتات قسام مرة ثانية، شكله مشغول بشغله.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094637_user.wav", + "user_duration_sec": 5.504, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094637_robot.wav", + "robot_duration_sec": 4.24 + }, + { + "timestamp": "20260515_094647", + "started_at": 1778809607.3812702, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094647_user.wav", + "user_duration_sec": 1.44 + }, + { + "timestamp": "20260515_094650", + "started_at": 1778809610.885234, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094650_user.wav", + "user_duration_sec": 12.224 + }, + { + "timestamp": "20260515_094713", + "started_at": 1778809633.5971472, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094713_user.wav", + "user_duration_sec": 0.256 + }, + { + "timestamp": "20260515_094714", + "started_at": 1778809634.6655543, + "user_text": ".", + "robot_text": "هلا حازم، شحالك يا مدير؟ عساك بخير.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094714_user.wav", + "user_duration_sec": 0.608, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094714_robot.wav", + "robot_duration_sec": 3.48 + }, + { + "timestamp": "20260515_094724", + "started_at": 1778809644.413377, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094724_user.wav", + "user_duration_sec": 0.096 + }, + { + "timestamp": "20260515_094725", + "started_at": 1778809645.7589042, + "user_text": "You want to see me?", + "robot_text": "هلا ليام، منور الاجتماع!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094725_user.wav", + "user_duration_sec": 2.816, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094725_robot.wav", + "robot_duration_sec": 2.24 + }, + { + "timestamp": "20260515_094738", + "started_at": 1778809658.549823, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094738_user.wav", + "user_duration_sec": 0.352 + }, + { + "timestamp": "20260515_094739", + "started_at": 1778809659.0492153, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094739_user.wav", + "user_duration_sec": 3.392 + }, + { + "timestamp": "20260515_094751", + "started_at": 1778809671.5759125, + "user_text": "", + "robot_text": "بالتوفيق في اجتماعكم، يا حازم", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094751_user.wav", + "user_duration_sec": 0.864, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094751_robot.wav", + "robot_duration_sec": 2.08 + }, + { + "timestamp": "20260515_094755", + "started_at": 1778809675.6827207, + "user_text": "", + "robot_text": "وليام!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094755_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094755_robot.wav", + "robot_duration_sec": 0.84 + }, + { + "timestamp": "20260515_094757", + "started_at": 1778809677.7314005, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094757_user.wav", + "user_duration_sec": 0.352 + }, + { + "timestamp": "20260515_094758", + "started_at": 1778809678.3692307, + "user_text": "Shake hands. Shake hands.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094758_user.wav", + "user_duration_sec": 18.304 + }, + { + "timestamp": "20260515_094831", + "started_at": 1778809711.4078243, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094831_user.wav", + "user_duration_sec": 1.856 + }, + { + "timestamp": "20260515_094834", + "started_at": 1778809714.3034053, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094834_user.wav", + "user_duration_sec": 2.976 + }, + { + "timestamp": "20260515_094839", + "started_at": 1778809719.2034569, + "user_text": "", + "robot_text": "حيّاك الله يا حازم،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094839_user.wav", + "user_duration_sec": 2.4, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094839_robot.wav", + "robot_duration_sec": 1.64 + }, + { + "timestamp": "20260515_094843", + "started_at": 1778809723.0940247, + "user_text": "", + "robot_text": "ويا هلا بليام. كيف حالكم؟ عسى", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094843_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094843_robot.wav", + "robot_duration_sec": 2.52 + }, + { + "timestamp": "20260515_094843", + "started_at": 1778809723.990518, + "user_text": "", + "robot_text": "كل شيء طيب؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094843_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094843_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260515_094844", + "started_at": 1778809724.4064922, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094844_user.wav", + "user_duration_sec": 2.208 + }, + { + "timestamp": "20260515_094848", + "started_at": 1778809728.166173, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094848_user.wav", + "user_duration_sec": 1.28 + }, + { + "timestamp": "20260515_094855", + "started_at": 1778809735.4668992, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094855_user.wav", + "user_duration_sec": 0.352 + }, + { + "timestamp": "20260515_094856", + "started_at": 1778809736.0085597, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094856_user.wav", + "user_duration_sec": 2.624 + }, + { + "timestamp": "20260515_094900", + "started_at": 1778809740.05413, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094900_user.wav", + "user_duration_sec": 3.744 + }, + { + "timestamp": "20260515_094907", + "started_at": 1778809747.5636914, + "user_text": ".", + "robot_text": "مرحبا حازم،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094907_user.wav", + "user_duration_sec": 3.52 + }, + { + "timestamp": "20260515_094913", + "started_at": 1778809753.4751868, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094913_user.wav", + "user_duration_sec": 6.912 + }, + { + "timestamp": "20260515_094923", + "started_at": 1778809763.6009243, + "user_text": "수 풀 님 . 사 진 으로 하 면 서 شو عم شو", + "robot_text": "هلا والله حازم، شحالك يا مدير؟ وياك ليام", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094923_user.wav", + "user_duration_sec": 6.144, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094923_robot.wav", + "robot_duration_sec": 3.36 + }, + { + "timestamp": "20260515_094934", + "started_at": 1778809774.9002097, + "user_text": "", + "robot_text": "بعد منورين والله.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094934_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094934_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260515_094935", + "started_at": 1778809775.3861923, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094935_user.wav", + "user_duration_sec": 2.24 + }, + { + "timestamp": "20260515_094938", + "started_at": 1778809778.8386662, + "user_text": "", + "robot_text": "تنورونا دايماً! كيف الأمور عندكم؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094938_user.wav", + "user_duration_sec": 8.384, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094938_robot.wav", + "robot_duration_sec": 2.52 + }, + { + "timestamp": "20260515_094955", + "started_at": 1778809795.272099, + "user_text": "", + "robot_text": "منورين الجلسة والله، كل شيء تمام؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_094955_user.wav", + "user_duration_sec": 4.48, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_094955_robot.wav", + "robot_duration_sec": 2.0 + }, + { + "timestamp": "20260515_095004", + "started_at": 1778809804.2572916, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095004_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095004_robot.wav", + "robot_duration_sec": 0.4 + }, + { + "timestamp": "20260515_095005", + "started_at": 1778809805.201633, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095005_user.wav", + "user_duration_sec": 0.64 + }, + { + "timestamp": "20260515_095006", + "started_at": 1778809806.3229535, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095006_user.wav", + "user_duration_sec": 5.024 + }, + { + "timestamp": "20260515_095018", + "started_at": 1778809818.265587, + "user_text": "Okay. Also wir dürfen nicht.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095018_user.wav", + "user_duration_sec": 0.96 + }, + { + "timestamp": "20260515_095020", + "started_at": 1778809820.357888, + "user_text": "", + "robot_text": "حي الله ليام و تي تي شخباركم", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095020_user.wav", + "user_duration_sec": 4.768, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095020_robot.wav", + "robot_duration_sec": 2.16 + }, + { + "timestamp": "20260515_095028", + "started_at": 1778809828.7211938, + "user_text": "", + "robot_text": "عساكم بخير؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095028_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095028_robot.wav", + "robot_duration_sec": 1.4 + }, + { + "timestamp": "20260515_095029", + "started_at": 1778809829.1506586, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095029_user.wav", + "user_duration_sec": 1.76 + }, + { + "timestamp": "20260515_095031", + "started_at": 1778809831.8427076, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095031_user.wav", + "user_duration_sec": 5.376 + }, + { + "timestamp": "20260515_095042", + "started_at": 1778809842.6378486, + "user_text": "", + "robot_text": "جلسة موفقة ان شاء الله، بالتوفيق", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095042_user.wav", + "user_duration_sec": 1.984, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095042_robot.wav", + "robot_duration_sec": 1.52 + }, + { + "timestamp": "20260515_095047", + "started_at": 1778809847.50082, + "user_text": "", + "robot_text": "لكم جميعاً.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095047_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095047_robot.wav", + "robot_duration_sec": 1.52 + }, + { + "timestamp": "20260515_095047", + "started_at": 1778809847.920334, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095047_user.wav", + "user_duration_sec": 1.376 + }, + { + "timestamp": "20260515_095050", + "started_at": 1778809850.33551, + "user_text": ".", + "robot_text": "ما شاء الله، حازم و ليام", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095050_user.wav", + "user_duration_sec": 8.736, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095050_robot.wav", + "robot_duration_sec": 1.76 + }, + { + "timestamp": "20260515_095107", + "started_at": 1778809867.9139411, + "user_text": "", + "robot_text": "وتي تي كلهم موجودين! أكيد", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095107_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095107_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260515_095108", + "started_at": 1778809868.5132987, + "user_text": "", + "robot_text": "اجتماع مهم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095108_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095108_robot.wav", + "robot_duration_sec": 1.6 + }, + { + "timestamp": "20260515_095112", + "started_at": 1778809872.2557647, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095112_user.wav", + "user_duration_sec": 0.032 + }, + { + "timestamp": "20260515_095112", + "started_at": 1778809872.6511655, + "user_text": "", + "robot_text": "فالك طيب وياكم،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095112_user.wav", + "user_duration_sec": 5.152, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095112_robot.wav", + "robot_duration_sec": 1.64 + }, + { + "timestamp": "20260515_095121", + "started_at": 1778809881.0691955, + "user_text": "", + "robot_text": "أي خدمة", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095121_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095121_robot.wav", + "robot_duration_sec": 0.72 + }, + { + "timestamp": "20260515_095121", + "started_at": 1778809881.4970362, + "user_text": "", + "robot_text": "أنا جاهز.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095121_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095121_robot.wav", + "robot_duration_sec": 0.68 + }, + { + "timestamp": "20260515_095121", + "started_at": 1778809881.931356, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095121_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095121_robot.wav", + "robot_duration_sec": 0.24 + }, + { + "timestamp": "20260515_095123", + "started_at": 1778809883.673065, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095123_user.wav", + "user_duration_sec": 0.224 + }, + { + "timestamp": "20260515_095124", + "started_at": 1778809884.0138052, + "user_text": "沒 有 完 , 他 後 來 說 重 新 再 做 一個啊。 沒關係啊,我們", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095124_user.wav", + "user_duration_sec": 6.112 + }, + { + "timestamp": "20260515_095135", + "started_at": 1778809895.957714, + "user_text": "你 別 管 他 主 要 了 ,我 們 這 個 效 果 就是 他 有 這 個 支 護 能 講 ,今天介紹一個下一個。", + "robot_text": "تمام التمام، خذوا راحتكم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095135_user.wav", + "user_duration_sec": 7.872, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095135_robot.wav", + "robot_duration_sec": 2.12 + }, + { + "timestamp": "20260515_095148", + "started_at": 1778809908.3383746, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095148_user.wav", + "user_duration_sec": 0.864 + }, + { + "timestamp": "20260515_095150", + "started_at": 1778809910.1511078, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095150_user.wav", + "user_duration_sec": 8.288 + }, + { + "timestamp": "20260515_095217", + "started_at": 1778809937.7767408, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095217_user.wav", + "user_duration_sec": 0.704 + }, + { + "timestamp": "20260515_095223", + "started_at": 1778809943.5721247, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095223_user.wav", + "user_duration_sec": 1.312 + }, + { + "timestamp": "20260515_095227", + "started_at": 1778809947.2134564, + "user_text": " 阿西要 ", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095227_user.wav", + "user_duration_sec": 0.352 + }, + { + "timestamp": "20260515_095228", + "started_at": 1778809948.830938, + "user_text": "a + b介紹的。", + "robot_text": "شكلها بتكون جلسة مثمرة، بالتوفيق لكم!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095228_user.wav", + "user_duration_sec": 2.432, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095228_robot.wav", + "robot_duration_sec": 3.32 + }, + { + "timestamp": "20260515_095240", + "started_at": 1778809960.5091906, + "user_text": "", + "robot_text": "'Tفضلوا كملوا مدتكم، خذوا راحتكم للاخر.'", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095240_user.wav", + "user_duration_sec": 2.4, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095240_robot.wav", + "robot_duration_sec": 2.6 + }, + { + "timestamp": "20260515_095245", + "started_at": 1778809965.050899, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095245_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095245_robot.wav", + "robot_duration_sec": 0.28 + }, + { + "timestamp": "20260515_095246", + "started_at": 1778809966.0028684, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095246_user.wav", + "user_duration_sec": 0.48 + }, + { + "timestamp": "20260515_095246", + "started_at": 1778809966.7745104, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095246_user.wav", + "user_duration_sec": 2.08 + }, + { + "timestamp": "20260515_095250", + "started_at": 1778809970.2962544, + "user_text": "unglausi.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095250_user.wav", + "user_duration_sec": 0.48 + }, + { + "timestamp": "20260515_095252", + "started_at": 1778809972.4590547, + "user_text": "他其實當然面對。", + "robot_text": "بالتوفيق في اجتماعكم وموفقين دايم", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095252_user.wav", + "user_duration_sec": 0.672, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095252_robot.wav", + "robot_duration_sec": 3.08 + }, + { + "timestamp": "20260515_095303", + "started_at": 1778809983.983013, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095303_user.wav", + "user_duration_sec": 2.048 + }, + { + "timestamp": "20260515_095311", + "started_at": 1778809991.2751036, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095311_user.wav", + "user_duration_sec": 0.32 + }, + { + "timestamp": "20260515_095312", + "started_at": 1778809992.7278874, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095312_user.wav", + "user_duration_sec": 3.2 + }, + { + "timestamp": "20260515_095319", + "started_at": 1778809999.4196892, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095319_user.wav", + "user_duration_sec": 2.048 + }, + { + "timestamp": "20260515_095323", + "started_at": 1778810003.4062488, + "user_text": "7 0. Eh, visitamos a 100%.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095323_user.wav", + "user_duration_sec": 2.08 + }, + { + "timestamp": "20260515_095327", + "started_at": 1778810007.0185175, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095327_user.wav", + "user_duration_sec": 0.8 + }, + { + "timestamp": "20260515_095329", + "started_at": 1778810009.2331939, + "user_text": "او عادي حاله", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095329_user.wav", + "user_duration_sec": 0.64 + }, + { + "timestamp": "20260515_095331", + "started_at": 1778810011.081641, + "user_text": "والله ي سلم علي كم خير.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095331_user.wav", + "user_duration_sec": 2.336 + }, + { + "timestamp": "20260515_095335", + "started_at": 1778810015.4608798, + "user_text": "لو واحد اثنين جديد. ", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095335_user.wav", + "user_duration_sec": 1.92 + }, + { + "timestamp": "20260515_095338", + "started_at": 1778810018.7574284, + "user_text": "لو ات خ ن واحد ه", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095338_user.wav", + "user_duration_sec": 3.36 + }, + { + "timestamp": "20260515_095348", + "started_at": 1778810028.9448812, + "user_text": "", + "robot_text": "هلا والله", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095348_user.wav", + "user_duration_sec": 0.384 + }, + { + "timestamp": "20260515_095349", + "started_at": 1778810029.4919236, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095349_user.wav", + "user_duration_sec": 3.328 + }, + { + "timestamp": "20260515_095355", + "started_at": 1778810035.5984402, + "user_text": "Yes.", + "robot_text": "مرحبا ليام وتي تي، شحالكم؟ عساكم", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095355_user.wav", + "user_duration_sec": 0.256, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095355_robot.wav", + "robot_duration_sec": 2.8 + }, + { + "timestamp": "20260515_095358", + "started_at": 1778810038.7099447, + "user_text": "", + "robot_text": "بخير. مجهود طيب في عملكم في Wisemate.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095358_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095358_robot.wav", + "robot_duration_sec": 3.12 + }, + { + "timestamp": "20260515_095404", + "started_at": 1778810044.0742111, + "user_text": "但 是 把 這個 暴 力 為 一樣 , 那 這 次 兩 次 呢 ,他 就 per fect list , 然後 就 是 發 這樣 的 , 其實就是長這樣。", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095404_user.wav", + "user_duration_sec": 7.424 + }, + { + "timestamp": "20260515_095429", + "started_at": 1778810069.7691848, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095429_user.wav", + "user_duration_sec": 13.696 + }, + { + "timestamp": "20260515_095452", + "started_at": 1778810092.558289, + "user_text": ".", + "robot_text": "مع السلامة ليام، بالتوفيق في عملك!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095452_user.wav", + "user_duration_sec": 3.808, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095452_robot.wav", + "robot_duration_sec": 2.44 + }, + { + "timestamp": "20260515_095504", + "started_at": 1778810104.3199503, + "user_text": "Мама в новых видео.", + "robot_text": "في أمان الله تي تي، نشوفك على خير.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095504_user.wav", + "user_duration_sec": 2.944, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095504_robot.wav", + "robot_duration_sec": 2.28 + }, + { + "timestamp": "20260515_095516", + "started_at": 1778810116.1252255, + "user_text": "", + "robot_text": "بالعافية تي تي!", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095516_user.wav", + "user_duration_sec": 1.856, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095516_robot.wav", + "robot_duration_sec": 1.04 + }, + { + "timestamp": "20260515_095522", + "started_at": 1778810122.0784693, + "user_text": "ให้ ปลา อีก ล่ะ ครับ", + "robot_text": "تسلمين، أشوفج على خير.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095522_user.wav", + "user_duration_sec": 1.152, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095522_robot.wav", + "robot_duration_sec": 1.16 + }, + { + "timestamp": "20260515_095524", + "started_at": 1778810124.9922345, + "user_text": "", + "robot_text": "مع السلامة تي تي، درب السلامة.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095524_user.wav", + "user_duration_sec": 0.768, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_095524_robot.wav", + "robot_duration_sec": 2.32 + }, + { + "timestamp": "20260515_095528", + "started_at": 1778810128.147848, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095528_user.wav", + "user_duration_sec": 0.992 + }, + { + "timestamp": "20260515_095530", + "started_at": 1778810130.0458086, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095530_user.wav", + "user_duration_sec": 4.48 + }, + { + "timestamp": "20260515_095750", + "started_at": 1778810270.719531, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_095750_user.wav", + "user_duration_sec": 0.192 + }, + { + "timestamp": "20260515_100406", + "started_at": 1778810646.4413834, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_100406_user.wav", + "user_duration_sec": 0.064 + }, + { + "timestamp": "20260515_100407", + "started_at": 1778810647.9195557, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_100407_user.wav", + "user_duration_sec": 0.384 + }, + { + "timestamp": "20260515_100408", + "started_at": 1778810648.6200986, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_100408_user.wav", + "user_duration_sec": 0.832 + }, + { + "timestamp": "20260515_102425", + "started_at": 1778811865.2814493, + "user_text": "", + "robot_text": "مرحبا يا خوي، حالياً الكاميرا عندي مب", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_102425_user.wav", + "user_duration_sec": 3.36, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_102425_robot.wav", + "robot_duration_sec": 2.64 + }, + { + "timestamp": "20260515_102430", + "started_at": 1778811870.6020293, + "user_text": "", + "robot_text": "مفتوحة، والتعرف على الويه بعد مب شغال. يعني ما أقدر أشوف", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_102430_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_102430_robot.wav", + "robot_duration_sec": 4.12 + }, + { + "timestamp": "20260515_102431", + "started_at": 1778811871.3578074, + "user_text": "", + "robot_text": "أي شي أو أتعرف على حد بالفترة الحالية. لو احتجت شي، خبرني.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_102431_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_102431_robot.wav", + "robot_duration_sec": 4.56 + }, + { + "timestamp": "20260515_102435", + "started_at": 1778811875.9227085, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_102435_user.wav", + "user_duration_sec": 1.472 + }, + { + "timestamp": "20260515_102438", + "started_at": 1778811878.6716042, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_102438_user.wav", + "user_duration_sec": 0.224 + }, + { + "timestamp": "20260515_105540", + "started_at": 1778813740.1552172, + "user_text": "", + "robot_text": "مرحبا بك، يا خوي. أنا سند، كيف أقدر أساعدك اليوم؟", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105540_robot.wav", + "robot_duration_sec": 4.28 + }, + { + "timestamp": "20260515_105543", + "started_at": 1778813743.2126505, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105543_user.wav", + "user_duration_sec": 0.8 + }, + { + "timestamp": "20260515_105544", + "started_at": 1778813744.2409508, + "user_text": "", + "robot_text": "تمام، يا خوي. الحين صار بإمكاني أتعرف", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105544_user.wav", + "user_duration_sec": 0.864, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105544_robot.wav", + "robot_duration_sec": 2.92 + }, + { + "timestamp": "20260515_105548", + "started_at": 1778813748.3176389, + "user_text": "", + "robot_text": "على الأشخاص اللي أعرفهم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105548_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105548_robot.wav", + "robot_duration_sec": 1.68 + }, + { + "timestamp": "20260515_105553", + "started_at": 1778813753.512742, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105553_user.wav", + "user_duration_sec": 0.288 + }, + { + "timestamp": "20260515_105553", + "started_at": 1778813753.980176, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105553_user.wav", + "user_duration_sec": 1.696 + }, + { + "timestamp": "20260515_105559", + "started_at": 1778813759.8452103, + "user_text": "13 15 15 1", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105559_user.wav", + "user_duration_sec": 0.992 + }, + { + "timestamp": "20260515_105602", + "started_at": 1778813762.5192037, + "user_text": "Şu dur. İşte.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105602_user.wav", + "user_duration_sec": 0.896 + }, + { + "timestamp": "20260515_105606", + "started_at": 1778813766.116226, + "user_text": ".", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105606_user.wav", + "user_duration_sec": 2.72 + }, + { + "timestamp": "20260515_105610", + "started_at": 1778813770.641564, + "user_text": "", + "robot_text": "هلا والله يا حازم، شحالك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105610_user.wav", + "user_duration_sec": 1.152, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105610_robot.wav", + "robot_duration_sec": 2.12 + }, + { + "timestamp": "20260515_105615", + "started_at": 1778813775.3647385, + "user_text": "", + "robot_text": "عساك بخير وموفق", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105615_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105615_robot.wav", + "robot_duration_sec": 1.12 + }, + { + "timestamp": "20260515_105615", + "started_at": 1778813775.8352149, + "user_text": "對 , 現 在 我 們 的 後", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105615_user.wav", + "user_duration_sec": 3.712 + }, + { + "timestamp": "20260515_105622", + "started_at": 1778813782.676456, + "user_text": "", + "robot_text": "شخبارك يا", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105622_user.wav", + "user_duration_sec": 7.36 + }, + { + "timestamp": "20260515_105635", + "started_at": 1778813795.5579245, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105635_user.wav", + "user_duration_sec": 0.864 + }, + { + "timestamp": "20260515_105637", + "started_at": 1778813797.98301, + "user_text": ". Oh.", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105637_user.wav", + "user_duration_sec": 3.168 + }, + { + "timestamp": "20260515_105643", + "started_at": 1778813803.0237193, + "user_text": "", + "robot_text": "شغال على", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105643_user.wav", + "user_duration_sec": 1.056 + }, + { + "timestamp": "20260515_105647", + "started_at": 1778813807.0194955, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105647_user.wav", + "user_duration_sec": 3.04 + }, + { + "timestamp": "20260515_105652", + "started_at": 1778813812.3309493, + "user_text": ". . . . .", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105652_user.wav", + "user_duration_sec": 1.504 + }, + { + "timestamp": "20260515_105655", + "started_at": 1778813815.1605892, + "user_text": "", + "robot_text": "شو المشاريع الجديدة اللي عندك في شركة ys lootah tech؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105655_user.wav", + "user_duration_sec": 2.4, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105655_robot.wav", + "robot_duration_sec": 2.52 + }, + { + "timestamp": "20260515_105700", + "started_at": 1778813820.2270343, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105700_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105700_robot.wav", + "robot_duration_sec": 0.52 + }, + { + "timestamp": "20260515_105701", + "started_at": 1778813821.6776195, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105701_user.wav", + "user_duration_sec": 0.512 + }, + { + "timestamp": "20260515_105702", + "started_at": 1778813822.4117692, + "user_text": "", + "robot_text": "منورنا ليام، شحالك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105702_user.wav", + "user_duration_sec": 1.92, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105702_robot.wav", + "robot_duration_sec": 1.96 + }, + { + "timestamp": "20260515_105709", + "started_at": 1778813829.5796034, + "user_text": "Ah, ik maak hem.", + "robot_text": "تسلم، ما تقصر.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105709_user.wav", + "user_duration_sec": 12.512, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105709_robot.wav", + "robot_duration_sec": 1.52 + }, + { + "timestamp": "20260515_105746", + "started_at": 1778813866.3928776, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105746_user.wav", + "user_duration_sec": 0.16 + }, + { + "timestamp": "20260515_105747", + "started_at": 1778813867.4134119, + "user_text": "มา", + "robot_text": "اعتذر، ما أشوف عدل الحين.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105747_user.wav", + "user_duration_sec": 0.704, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105747_robot.wav", + "robot_duration_sec": 2.04 + }, + { + "timestamp": "20260515_105756", + "started_at": 1778813876.4170573, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105756_user.wav", + "user_duration_sec": 0.32 + }, + { + "timestamp": "20260515_105756", + "started_at": 1778813876.967098, + "user_text": ".", + "robot_text": "seems like there's something blocking the camera right now.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105756_user.wav", + "user_duration_sec": 0.576, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260515_105756_robot.wav", + "robot_duration_sec": 3.12 + }, + { + "timestamp": "20260515_105802", + "started_at": 1778813882.1034114, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260515_105802_user.wav", + "user_duration_sec": 0.448 + }, + { + "timestamp": "20260516_112009", + "started_at": 1778901609.5694642, + "user_text": "", + "robot_text": "هلا والله يا قسام، شحالك؟ عساك بخير وصحة؟ شو مسوي مهندس الروبوتات؟", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260516_112009_robot.wav", + "robot_duration_sec": 5.56 + }, + { + "timestamp": "20260516_112010", + "started_at": 1778901610.781415, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260516_112010_user.wav", + "user_duration_sec": 0.672 + }, + { + "timestamp": "20260519_082138", + "started_at": 1779150098.714068, + "user_text": "", + "robot_text": "مرحبا يا همام، شحالك؟ عساك بخير يا مدير المشاريع؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_082138_user.wav", + "user_duration_sec": 0.672, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_082138_robot.wav", + "robot_duration_sec": 4.24 + }, + { + "timestamp": "20260519_082148", + "started_at": 1779150108.4636102, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_082148_user.wav", + "user_duration_sec": 4.064 + }, + { + "timestamp": "20260519_082158", + "started_at": 1779150118.6651778, + "user_text": "", + "robot_text": "الكاميرا الحين مسكرة يا خوي، وما أقدر أشوف أي شي. بعد ما بقدر أميز الوجوه وأرحب بالأسماء.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_082158_user.wav", + "user_duration_sec": 0.32, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_082158_robot.wav", + "robot_duration_sec": 6.52 + }, + { + "timestamp": "20260519_084918", + "started_at": 1779151758.8081703, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_084918_user.wav", + "user_duration_sec": 0.064 + }, + { + "timestamp": "20260519_084918", + "started_at": 1779151758.9995167, + "user_text": "", + "robot_text": "مرحبا يا خوي، صباح الخير.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_084918_user.wav", + "user_duration_sec": 1.6, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_084918_robot.wav", + "robot_duration_sec": 1.8 + }, + { + "timestamp": "20260519_084923", + "started_at": 1779151763.0834322, + "user_text": "", + "robot_text": "أنا سند، شتبي مساعده؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_084923_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_084923_robot.wav", + "robot_duration_sec": 2.56 + }, + { + "timestamp": "20260519_084927", + "started_at": 1779151767.0389714, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_084927_user.wav", + "user_duration_sec": 1.472 + }, + { + "timestamp": "20260519_115018", + "started_at": 1779162618.8697824, + "user_text": "", + "robot_text": "Marhaba! Shkhabarak Ana جاهز لأي مساعدة تحتاجها، آمِر يا خوي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115018_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115018_robot.wav", + "robot_duration_sec": 4.48 + }, + { + "timestamp": "20260519_115025", + "started_at": 1779162625.484732, + "user_text": "", + "robot_text": "سموحة يا خوي، في وايد دوشة في الخلفية", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115025_user.wav", + "user_duration_sec": 5.664, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115025_robot.wav", + "robot_duration_sec": 2.44 + }, + { + "timestamp": "20260519_115036", + "started_at": 1779162636.2765567, + "user_text": "", + "robot_text": "مب قادر أسمعك زين. ممكن تعيد اللي قلته؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115036_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115036_robot.wav", + "robot_duration_sec": 3.36 + }, + { + "timestamp": "20260519_115039", + "started_at": 1779162639.1431997, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115039_user.wav", + "user_duration_sec": 0.928 + }, + { + "timestamp": "20260519_115041", + "started_at": 1779162641.4765987, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115041_user.wav", + "user_duration_sec": 3.904 + }, + { + "timestamp": "20260519_115049", + "started_at": 1779162649.8598154, + "user_text": "وال له مح تار الص راح ه اخ ذ", + "robot_text": "تفضل، الحيرة تصير. شو بالضبط محتار فيه؟ يمكن", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115049_user.wav", + "user_duration_sec": 5.312, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115049_robot.wav", + "robot_duration_sec": 3.44 + }, + { + "timestamp": "20260519_115102", + "started_at": 1779162662.9256096, + "user_text": "ط يب والله محتار صراحه", + "robot_text": "ايش اللي محيرك بالضبط؟ يمكن أقدر أساعدك بوجهة نظر.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115102_robot.wav", + "robot_duration_sec": 3.28 + }, + { + "timestamp": "20260519_115109", + "started_at": 1779162669.5039554, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115109_user.wav", + "user_duration_sec": 1.44, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115109_robot.wav", + "robot_duration_sec": 0.56 + }, + { + "timestamp": "20260519_115113", + "started_at": 1779162673.303498, + "user_text": "اه صدقوني", + "robot_text": "أكيد، قول اسمعك.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115113_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260519_115125", + "started_at": 1779162685.7355008, + "user_text": "", + "robot_text": "زين، خذ راحتك.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115125_user.wav", + "user_duration_sec": 1.344, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115125_robot.wav", + "robot_duration_sec": 1.28 + }, + { + "timestamp": "20260519_115131", + "started_at": 1779162691.091815, + "user_text": ".", + "robot_text": "تمام، أنا في انتظارك.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115131_user.wav", + "user_duration_sec": 0.256, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_115131_robot.wav", + "robot_duration_sec": 1.68 + }, + { + "timestamp": "20260519_115138", + "started_at": 1779162698.0706642, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_115138_user.wav", + "user_duration_sec": 0.128 + }, + { + "timestamp": "20260519_130234", + "started_at": 1779166954.0533257, + "user_text": "", + "robot_text": "مرحبا بك يا خوي. أنا جاهز لأي مساعدة تحتاجها.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_130234_robot.wav", + "robot_duration_sec": 3.96 + }, + { + "timestamp": "20260519_130239", + "started_at": 1779166959.1727755, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_130239_user.wav", + "user_duration_sec": 1.44 + }, + { + "timestamp": "20260519_154619", + "started_at": 1779176779.8765137, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154619_user.wav", + "user_duration_sec": 0.256 + }, + { + "timestamp": "20260519_154620", + "started_at": 1779176780.6527424, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154620_user.wav", + "user_duration_sec": 0.512 + }, + { + "timestamp": "20260519_154621", + "started_at": 1779176781.9523358, + "user_text": "", + "robot_text": "هلا، كمل يا خوي.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154621_user.wav", + "user_duration_sec": 2.24, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154621_robot.wav", + "robot_duration_sec": 1.0 + }, + { + "timestamp": "20260519_154632", + "started_at": 1779176792.8424387, + "user_text": "", + "robot_text": "أسمعك، تفضل", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154632_user.wav", + "user_duration_sec": 0.448, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154632_robot.wav", + "robot_duration_sec": 1.08 + }, + { + "timestamp": "20260519_154633", + "started_at": 1779176793.7084265, + "user_text": "مرحبا بك", + "robot_text": "مراحب، شخبارك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154633_user.wav", + "user_duration_sec": 0.096, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154633_robot.wav", + "robot_duration_sec": 1.36 + }, + { + "timestamp": "20260519_154636", + "started_at": 1779176796.5443742, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154636_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154636_robot.wav", + "robot_duration_sec": 0.24 + }, + { + "timestamp": "20260519_154640", + "started_at": 1779176800.3990128, + "user_text": "", + "robot_text": "تمام والله، أنت شعلومك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154640_user.wav", + "user_duration_sec": 8.192, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154640_robot.wav", + "robot_duration_sec": 2.0 + }, + { + "timestamp": "20260519_154703", + "started_at": 1779176823.6258621, + "user_text": "كيفك الشيء عرفتي؟", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154703_user.wav", + "user_duration_sec": 0.48 + }, + { + "timestamp": "20260519_154705", + "started_at": 1779176825.3986928, + "user_text": "كيفك السلام عليكم؟", + "robot_text": "بخير ونعمة، الحمد لله. في شي تباني أساعدك فيه؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154705_user.wav", + "user_duration_sec": 0.48, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154705_robot.wav", + "robot_duration_sec": 3.92 + }, + { + "timestamp": "20260519_154711", + "started_at": 1779176831.2435904, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154711_user.wav", + "user_duration_sec": 0.672 + }, + { + "timestamp": "20260519_154712", + "started_at": 1779176832.118435, + "user_text": "Çok para.", + "robot_text": "كل خير وبركة. علومك أنت؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154712_user.wav", + "user_duration_sec": 1.248, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154712_robot.wav", + "robot_duration_sec": 2.4 + }, + { + "timestamp": "20260519_154720", + "started_at": 1779176840.4289267, + "user_text": "الحمد لل ه ت قدر تنفع ايدك", + "robot_text": "يا خوي، أنا ما عندي أيادي", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154720_user.wav", + "user_duration_sec": 2.24, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154720_robot.wav", + "robot_duration_sec": 1.6 + }, + { + "timestamp": "20260519_154727", + "started_at": 1779176847.6233058, + "user_text": "", + "robot_text": "أرفعها، بس أقدر أساعدك بأي شي ثاني تحتاجه.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154727_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154727_robot.wav", + "robot_duration_sec": 4.32 + }, + { + "timestamp": "20260519_154733", + "started_at": 1779176853.197034, + "user_text": "سا عدني شو مثلا", + "robot_text": "أكيد، مثلاً تحتاج", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154733_user.wav", + "user_duration_sec": 1.408, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154733_robot.wav", + "robot_duration_sec": 1.52 + }, + { + "timestamp": "20260519_154737", + "started_at": 1779176857.9814293, + "user_text": "", + "robot_text": "معلومات عن شي", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154737_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154737_robot.wav", + "robot_duration_sec": 1.16 + }, + { + "timestamp": "20260519_154740", + "started_at": 1779176860.114877, + "user_text": "", + "robot_text": "معين؟ أو ترجمة لشيء؟ أو حتى سوالف عادية. آمرني.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154740_user.wav", + "user_duration_sec": 0.224, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154740_robot.wav", + "robot_duration_sec": 6.08 + }, + { + "timestamp": "20260519_154742", + "started_at": 1779176862.9782894, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154742_user.wav", + "user_duration_sec": 1.472 + }, + { + "timestamp": "20260519_154752", + "started_at": 1779176872.1843014, + "user_text": "كم كم عد د السكان في الامارات", + "robot_text": "تعداد السكان في الإمارات يتغير باستمرار، بس بشكل عام يقارب 10 مليون نسمة. تبغي معلومات أكثر عن أي إمارة محددة؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154752_user.wav", + "user_duration_sec": 0.832, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154752_robot.wav", + "robot_duration_sec": 9.72 + }, + { + "timestamp": "20260519_154804", + "started_at": 1779176884.876378, + "user_text": "من هو اش هر بر ند محاشي لبنه؟", + "robot_text": "بصراحة، ما عندي معلومات دقيقة عن أشهر براند محاشي لبن، لأن هالشيء يختلف حسب المنطقة والأذواق. بس إذا تدور على توصيات، ممكن تحصل بالكثير في تطبيقات التوصيل أو مواقع التقييم.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154804_user.wav", + "user_duration_sec": 1.952, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154804_robot.wav", + "robot_duration_sec": 14.84 + }, + { + "timestamp": "20260519_154832", + "started_at": 1779176912.7413862, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154832_user.wav", + "user_duration_sec": 0.544 + }, + { + "timestamp": "20260519_154833", + "started_at": 1779176913.5135, + "user_text": "تعرف خلطات الفريد", + "robot_text": "هلا، الحين أشوفك زين. بالنسبة لـ \"خلطات الفريد\"، ما عندي فكرة واضحة عنها، ممكن توضح لي أكثر عنها؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154833_user.wav", + "user_duration_sec": 1.056, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154833_robot.wav", + "robot_duration_sec": 8.32 + }, + { + "timestamp": "20260519_154851", + "started_at": 1779176931.2157605, + "user_text": "تعال ندخل يلا امشي", + "robot_text": "أنا ما", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154851_user.wav", + "user_duration_sec": 4.384 + }, + { + "timestamp": "20260519_154903", + "started_at": 1779176943.9759922, + "user_text": "ياخد القرار اللي شفنا", + "robot_text": "يا خوي، أنا ما أقدر أمشي ولا أتخذ قرارات بنفسي. أنا بس مساعد افتراضي. أقدر أجاوب على أسئلتك أو أقدم لك معلومات.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154903_user.wav", + "user_duration_sec": 1.952, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154903_robot.wav", + "robot_duration_sec": 10.28 + }, + { + "timestamp": "20260519_154918", + "started_at": 1779176958.5768716, + "user_text": "يعني", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154918_user.wav", + "user_duration_sec": 13.984 + }, + { + "timestamp": "20260519_154949", + "started_at": 1779176989.62968, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154949_user.wav", + "user_duration_sec": 1.248 + }, + { + "timestamp": "20260519_154955", + "started_at": 1779176995.5653973, + "user_text": "ก็ เลย นะ คะ", + "robot_text": "صحيح، ما أقدر أتصرف من تلقاء نفسي. أنا بس أستجيب للي تقوله. في شي معين في بالك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_154955_user.wav", + "user_duration_sec": 0.864, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_154955_robot.wav", + "robot_duration_sec": 7.16 + }, + { + "timestamp": "20260519_155008", + "started_at": 1779177008.4833374, + "user_text": "", + "robot_text": "كاميرا وقفت، ما أقدر أشوفك الحين.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155008_user.wav", + "user_duration_sec": 1.184, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155008_robot.wav", + "robot_duration_sec": 2.48 + }, + { + "timestamp": "20260519_155015", + "started_at": 1779177015.6755085, + "user_text": "", + "robot_text": "تأمرني أمر،", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155015_user.wav", + "user_duration_sec": 5.536, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155015_robot.wav", + "robot_duration_sec": 0.96 + }, + { + "timestamp": "20260519_155026", + "started_at": 1779177026.3300655, + "user_text": "", + "robot_text": "شو اللي تباني أساعدك فيه؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155026_user.wav", + "user_duration_sec": 0.032, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155026_robot.wav", + "robot_duration_sec": 2.16 + }, + { + "timestamp": "20260519_155029", + "started_at": 1779177029.3373148, + "user_text": "", + "robot_text": "تمام، شو الموضوع اللي حاب نتكلم عنه؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155029_user.wav", + "user_duration_sec": 12.96, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155029_robot.wav", + "robot_duration_sec": 2.56 + }, + { + "timestamp": "20260519_155101", + "started_at": 1779177061.0948687, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155101_user.wav", + "user_duration_sec": 39.68 + }, + { + "timestamp": "20260519_155251", + "started_at": 1779177171.9114149, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155251_user.wav", + "user_duration_sec": 4.416 + }, + { + "timestamp": "20260519_155317", + "started_at": 1779177197.1195416, + "user_text": "", + "robot_text": "مرحبا يا خوي، مفهوم. بتذكر هالحالة وبتعامل على أساسها.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155317_user.wav", + "user_duration_sec": 1.12, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155317_robot.wav", + "robot_duration_sec": 4.24 + }, + { + "timestamp": "20260519_155328", + "started_at": 1779177208.8839252, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155328_user.wav", + "user_duration_sec": 40.064 + }, + { + "timestamp": "20260519_155616", + "started_at": 1779177376.066496, + "user_text": "", + "robot_text": "آمرني يا خوي، كيف أقدر أساعدك؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155616_user.wav", + "user_duration_sec": 3.392, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155616_robot.wav", + "robot_duration_sec": 2.32 + }, + { + "timestamp": "20260519_155622", + "started_at": 1779177382.911812, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155622_user.wav", + "user_duration_sec": 0.576 + }, + { + "timestamp": "20260519_155625", + "started_at": 1779177385.28559, + "user_text": ".", + "robot_text": "الصوت مب واضح بالمرة، ممكن", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155625_user.wav", + "user_duration_sec": 7.2, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155625_robot.wav", + "robot_duration_sec": 1.56 + }, + { + "timestamp": "20260519_155641", + "started_at": 1779177401.9648745, + "user_text": "", + "robot_text": "تعيد اللي قلته؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260519_155641_user.wav", + "user_duration_sec": 2.912, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260519_155641_robot.wav", + "robot_duration_sec": 1.52 + }, + { + "timestamp": "20260520_113905", + "started_at": 1779248345.1616135, + "user_text": "", + "robot_text": "مرحبا يا خوي. وصلتني معلومات الحالة. أي شي تباه بخصوص الكاميرا أو التعرف على الويه، أنا حاضر، بس خبرني أفعّلهم من لوحة التحكم.", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260520_113905_robot.wav", + "robot_duration_sec": 8.92 + }, + { + "timestamp": "20260520_113914", + "started_at": 1779248354.961822, + "user_text": "", + "robot_text": "هلا بك. كل شي تمام؟ في شي معين تبغي تتكلم عنه؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260520_113914_user.wav", + "user_duration_sec": 8.512, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260520_113914_robot.wav", + "robot_duration_sec": 3.92 + }, + { + "timestamp": "20260520_121411", + "started_at": 1779250451.8024144, + "user_text": "", + "robot_text": "", + "user_wav": "/home/unitree/Sanad/data/recordings/20260520_121411_user.wav", + "user_duration_sec": 0.544 + }, + { + "timestamp": "20260520_121412", + "started_at": 1779250452.6009102, + "user_text": "I'm recording though.", + "robot_text": "مرحبا يا خوي، زين انك تسجل. شو السالفة؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260520_121412_user.wav", + "user_duration_sec": 0.896, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260520_121412_robot.wav", + "robot_duration_sec": 3.56 + }, + { + "timestamp": "20260528_164730", + "started_at": 1779958050.2168624, + "user_text": "", + "robot_text": "مرحبا، شخبارك", + "robot_wav": "/home/unitree/Sanad/data/recordings/20260528_164730_robot.wav", + "robot_duration_sec": 1.48 + }, + { + "timestamp": "20260528_164732", + "started_at": 1779958052.8729692, + "user_text": "Carte per guadagnare.", + "robot_text": "\"Carte per guadagnare\"? ما فهمت صراحة، ممكن توضح أكثر يا خوي؟", + "user_wav": "/home/unitree/Sanad/data/recordings/20260528_164732_user.wav", + "user_duration_sec": 0.96, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260528_164732_robot.wav", + "robot_duration_sec": 4.8 + }, + { + "timestamp": "20260528_174103", + "started_at": 1779961263.768733, + "user_text": "", + "robot_text": "مرحبا يا خوي، وصلتني المعلومات. أنا جاهز أساعدك بأي شيء تحتاجه.", + "user_wav": "/home/unitree/Sanad/data/recordings/20260528_174103_user.wav", + "user_duration_sec": 0.064, + "robot_wav": "/home/unitree/Sanad/data/recordings/20260528_174103_robot.wav", + "robot_duration_sec": 4.72 + } + ], + "total_records": 429 +} \ No newline at end of file diff --git a/gemini/script.py b/gemini/script.py index 0eb5130..569e486 100644 --- a/gemini/script.py +++ b/gemini/script.py @@ -78,6 +78,13 @@ _FACES_DIR = Path(os.environ.get( )) _FACES_MAX_SAMPLES = int(os.environ.get("SANAD_FACES_MAX_SAMPLES", "3")) _FACES_PRIMER_RESIZE = int(os.environ.get("SANAD_FACES_PRIMER_RESIZE", "256")) +# N3 — zones gallery (zone → place → linked faces). Folded into a Gemini +# primer turn so Gemini can recognise / talk about known locations and the +# people associated with them. +_ZONES_DIR = Path(os.environ.get( + "SANAD_ZONES_DIR", + str(BASE_DIR / "data" / "zones"), +)) # ── stdin push channel (Marcus pattern) ────────────────────── @@ -103,9 +110,16 @@ _STATE_TAGS = { "resumed": "[STATE-RESUMED]", } +# Pending audio-profile swap signalled by the parent over "profile:" stdin +# lines. _audio_swap_loop drains it inside the brain's asyncio loop. +_PROFILE_LOCK = threading.Lock() +_PROFILE_PENDING: dict = {"id": None, "reason": ""} + +_VALID_PROFILES = {"builtin", "anker", "hollyland_builtin"} + def _stdin_watcher() -> None: - """Daemon thread — parse 'frame:' / 'state:' lines off stdin. + """Daemon thread — parse 'frame:' / 'state:' / 'profile:' lines off stdin. Best-effort: any malformed line is skipped. Exits when the parent closes our stdin (subprocess teardown).""" @@ -143,6 +157,21 @@ def _stdin_watcher() -> None: msg += f" — {reason}" with _STATE_LOCK: _STATE_PENDING.append(msg) + elif line.startswith("profile:"): + # Parent signals an audio-profile hot-swap. Stash the target; + # _audio_swap_loop (asyncio task) handles the actual swap so + # PyAudio open/close happens off the stdin thread. + try: + payload = json.loads(line[len("profile:"):]) + except Exception: + continue + pid = (payload.get("id") or "").strip().lower() + if pid not in _VALID_PROFILES: + continue + with _PROFILE_LOCK: + _PROFILE_PENDING["id"] = pid + _PROFILE_PENDING["reason"] = ( + payload.get("reason") or "").strip() except Exception: return @@ -168,6 +197,18 @@ class GeminiBrain: self._audio = audio_io self._mic = audio_io.mic self._speaker = audio_io.speaker + # Kept on the brain so swap_audio_devices() can rebuild profiles that + # need DDS (`builtin`, `hollyland_builtin`) without re-init'ing. + self._audio_client = getattr(audio_io, "_audio_client", None) + # Current profile id (driven by the parent's "profile:" stdin push). + # Defaults to whatever audio_io was constructed with — `from_profile` + # sets profile_id; if SANAD_AUDIO_PROFILE override is in env, that + # value matches. + self._current_profile_id = getattr(audio_io, "profile_id", None) \ + or os.environ.get("SANAD_AUDIO_PROFILE", "builtin").strip().lower() + # Coordinates concurrent swap requests so two pending profile + # changes don't interleave mid-tear-down. + self._swap_lock: Optional[asyncio.Lock] = None # built in run() self._recorder = recorder self._voice = voice_name or GEMINI_VOICE self._system_prompt = system_prompt @@ -193,6 +234,22 @@ class GeminiBrain: or os.environ.get("SANAD_FACE_RECOGNITION_ENABLE", "0") == "1" ) self._gallery_version_primed = -1 # bumped after first successful primer + # N3 — zones knowledge toggle + primer version tracking. + self._zone_rec_enabled = bool( + _initial.zone_rec_enabled + or os.environ.get("SANAD_ZONE_RECOGNITION_ENABLE", "0") == "1" + ) + self._zones_version_primed = -1 + # "Go here" destination already announced this session (zone_id, place_id). + self._nav_target = ( + int(_initial.nav_target_zone_id), int(_initial.nav_target_place_id), + ) + # N2 — Gemini-driven locomotion enable gate (announce only; the + # actual dispatch loop lives in the parent and is wired separately). + self._movement_enabled = bool( + _initial.movement_enabled + or os.environ.get("SANAD_MOVEMENT_ENABLE", "0") == "1" + ) def stop(self) -> None: """Signal the run loop to exit at the next opportunity.""" @@ -225,8 +282,14 @@ class GeminiBrain: self._done = asyncio.Event() # Reset per-session primer state so re-priming on reconnect # actually happens. The state watcher will re-prime as soon - # as it sees vision+face-rec enabled. + # as it sees vision+face-rec (and place-rec) enabled. self._gallery_version_primed = -1 + self._zones_version_primed = -1 + # Re-announce the active destination on reconnect. + self._nav_target = (-1, -1) + # Lazy-build the swap lock on the active asyncio loop. + if self._swap_lock is None: + self._swap_lock = asyncio.Lock() try: await asyncio.wait_for( @@ -236,6 +299,7 @@ class GeminiBrain: self._send_frame_loop(session), self._send_state_loop(session), self._recognition_state_watcher(session), + self._audio_swap_loop(session), ), timeout=_SESSION_TIMEOUT, ) @@ -433,7 +497,13 @@ class GeminiBrain: if sc.output_transcription: text = (sc.output_transcription.text or "").strip() if text: - log.info("BOT : %s", text) + # Emit as "BOT:" (no space before colon) so the + # supervisor's _track_line can parse it the same + # way it parses "USER:" — this is the channel the + # movement dispatcher (N2) reads Gemini's own + # spoken phrases from. Keep in lock-step with + # GeminiSubprocess._track_line. + log.info("BOT: %s", text) self._recorder.add_robot_text(text) if sc.model_turn: @@ -571,6 +641,285 @@ class GeminiBrain: except Exception as exc: log.warning("face-rec-state inject failed: %s", exc) + # ─── place-recognition-state announcer (N3) ─────────── + # Same idea as _announce_facerec_state, for the places-gallery toggle. + # On a live OFF toggle it also tells Gemini to disregard the places it + # was given so OFF takes effect immediately instead of lingering. + + async def _announce_zonerec_state(self, session: Any, enabled: bool, + is_toggle: bool) -> None: + if is_toggle and enabled: + text = ( + "[ZONE RECOGNITION ON] You were just given the zones and places " + "you know (and the people associated with them). Briefly tell " + "the user you now know your way around, in your normal Khaleeji " + "style." + ) + elif is_toggle and not enabled: + text = ( + "[ZONE RECOGNITION OFF] Zone recognition was just disabled. " + "Disregard the zones and places you were given earlier — stop " + "naming rooms or locations. Briefly tell the user you'll no " + "longer recognise places." + ) + elif enabled: # session start, zone rec already on + text = ( + "[ZONE RECOGNITION STATUS] Zone recognition is ON — when you see " + "or are asked about a zone/place you've been told about, you may " + "name it and use its description. Do not announce this " + "unprompted." + ) + else: # session start, zone rec off + text = ( + "[ZONE RECOGNITION STATUS] Zone recognition is OFF — you do not " + "know any specific zones or places. If the user asks where they " + "are or to go somewhere by name, tell them to enable zone " + "recognition from the dashboard. Do not announce this " + "unprompted." + ) + try: + await session.send_realtime_input(text=text) + log.info("zone-rec-state injected (enabled=%s, toggle=%s)", + enabled, is_toggle) + except asyncio.CancelledError: + raise + except Exception as exc: + log.warning("zone-rec-state inject failed: %s", exc) + + # ─── navigation-target announcer (N3 "go here") ─────── + # When the operator sets a destination, tell Gemini which place to go to + # and show it the reference photo(s). Actual robot motion is wired by N2; + # this establishes the goal + visual reference. + + async def _announce_nav_target(self, session: Any, + zone_id: int, place_id: int) -> None: + if not zone_id or not place_id: + try: + await session.send_realtime_input(text=( + "[DESTINATION CLEARED] You have no specific destination right " + "now. Do not announce this unprompted." + )) + except asyncio.CancelledError: + raise + except Exception as exc: + log.warning("nav-clear inject failed: %s", exc) + return + try: + from Project.Sanad.vision.zone_gallery import ZoneGallery + gallery = ZoneGallery(_ZONES_DIR) + place = gallery.get_place(zone_id, place_id) + zone = gallery.get_zone(zone_id) + except Exception as exc: + log.warning("nav-target resolve failed: %s", exc) + return + if place is None: + log.info("nav-target zone_%d/place_%d not found — skipping", zone_id, place_id) + return + place_name = place.name or f"place {place_id}" + zone_name = (zone.name if zone else None) or f"zone {zone_id}" + instr = ( + f"[GO HERE] The user has set your destination to '{place_name}' in " + f"'{zone_name}'." + ) + if place.description: + instr += f" Notes: {place.description}." + instr += ( + " The image(s) below show what it looks like so you can recognise it. " + "If walking is enabled you will head there; if it is off, tell the " + "user to enable movement from the dashboard. Briefly acknowledge the " + "destination in your normal Khaleeji style." + ) + parts: list[dict[str, Any]] = [{"text": instr}] + for p in place.sample_paths[:_FACES_MAX_SAMPLES]: + try: + raw = p.read_bytes() + except OSError: + continue + jpeg = gallery._resize_for_primer(raw, _FACES_PRIMER_RESIZE) or raw + parts.append({"inline_data": {"mime_type": "image/jpeg", "data": jpeg}}) + try: + await session.send_client_content( + turns=[{"role": "user", "parts": parts}], turn_complete=True, + ) + log.info("nav-target injected → zone_%d/place_%d (%s)", + zone_id, place_id, place_name) + except asyncio.CancelledError: + raise + except Exception as exc: + log.warning("nav-target inject failed: %s", exc) + + # ─── movement-state announcer (N2) ──────────────────── + # Spoken confirmation when the operator enables / disables Gemini-driven + # locomotion from the dashboard. The actual movement dispatch loop lives + # in the parent; this only gives the user audible feedback on the toggle. + + async def _announce_movement_state(self, session: Any, enabled: bool, + is_toggle: bool) -> None: + if is_toggle and enabled: + text = ( + "[MOVEMENT ON] Walking is now enabled — you can move when the " + "user asks. Briefly tell the user movement is enabled and they " + "can ask you to walk, in your normal Khaleeji style." + ) + elif is_toggle and not enabled: + text = ( + "[MOVEMENT OFF] Walking was just disabled — you must not move. " + "Briefly tell the user movement is now off. If they ask you to " + "walk, tell them to enable movement from the dashboard first." + ) + elif enabled: # session start, movement already on + text = ( + "[MOVEMENT STATUS] Walking is ON — you may move when asked. Do " + "not announce this unprompted." + ) + else: # session start, movement off + text = ( + "[MOVEMENT STATUS] Walking is OFF — you cannot move right now. " + "If the user asks you to walk, tell them to enable movement " + "from the dashboard. Do not announce this unprompted." + ) + try: + await session.send_realtime_input(text=text) + log.info("movement-state injected (enabled=%s, toggle=%s)", + enabled, is_toggle) + except asyncio.CancelledError: + raise + except Exception as exc: + log.warning("movement-state inject failed: %s", exc) + + # ─── audio profile hot-swap ─────────────────────────── + # The parent (GeminiSubprocess) polls pactl for the Anker USB device + # and writes "profile:" lines to our stdin. _stdin_watcher parses + # them into _PROFILE_PENDING; this loop drains the flag on the asyncio + # loop and performs the actual swap. The brain's read/write sites + # (_send_mic_loop / _receive_loop) keep using self._mic / self._speaker — + # an atomic ref reassignment is enough because nothing caches them in + # a loop-local variable (verified in exploration). + + async def _audio_swap_loop(self, session: Any) -> None: + while not self._done.is_set() and not self._stop_flag.is_set(): + await asyncio.sleep(0.25) + with _PROFILE_LOCK: + target = _PROFILE_PENDING.get("id") + reason = _PROFILE_PENDING.get("reason", "") + _PROFILE_PENDING["id"] = None + _PROFILE_PENDING["reason"] = "" + if not target or target == self._current_profile_id: + continue + try: + await self.swap_audio_devices(session, target, reason=reason) + except asyncio.CancelledError: + return + except Exception as exc: + log.warning("audio swap failed: %s", exc) + + async def swap_audio_devices(self, session: Any, profile_id: str, + reason: str = "") -> None: + """Hot-swap mic+speaker to `profile_id` without dropping the live + Gemini session. Idempotent (no-op if already on `profile_id`). + + Order matters: start the new mic BEFORE we tear the old one down, + so a transient PyAudio failure (e.g. udev hasn't exposed Anker yet) + leaves the old backend in place. After 3 retries with backoff we + give up and log WARN — the watcher will retry on its next tick. + """ + if self._swap_lock is None: + log.warning("swap requested before session loop started — skipping") + return + async with self._swap_lock: + if profile_id == self._current_profile_id: + return + prev = self._current_profile_id + log.info("audio swap: %s → %s (reason=%s)", prev, profile_id, reason or "—") + + # Build + start the new mic. Retry: pactl can see the device + # before PyAudio's get_device_count refreshes. + try: + from Project.Sanad.voice.audio_io import AudioIO as _AudioIO + except Exception as exc: + log.error("audio swap: AudioIO import failed: %s", exc) + return + new_mic = new_spk = None + last_exc: Optional[BaseException] = None + for attempt in range(3): + try: + new_mic, new_spk = _AudioIO.build_backends( + profile_id, audio_client=self._audio_client, + ) + # mic.start() opens PyAudio + spawns reader thread. + # speaker is lazy (opens on first send_chunk). + await asyncio.to_thread(new_mic.start) + break + except Exception as exc: + last_exc = exc + # Tear down a partially-built backend so the next attempt + # gets a clean slate; don't leak PyAudio handles. + if new_mic is not None: + try: + await asyncio.to_thread(new_mic.stop) + except Exception: + pass + new_mic = new_spk = None + log.info("audio swap attempt %d failed: %s — retry in 0.4s", + attempt + 1, exc) + await asyncio.sleep(0.4) + if new_mic is None or new_spk is None: + log.warning("audio swap %s → %s: all 3 attempts failed (%s); " + "keeping current profile", + prev, profile_id, last_exc) + return + + # Drain the old playback so any in-flight AI utterance stops + # (interrupts mid-word — acceptable per spec, <1s gap). + # MUST be awaited via to_thread: _PyAudioSpeaker.stop now + # takes a per-instance RLock and an in-flight send_chunk on + # the executor may be holding it across a back-pressured + # PortAudio write. Calling stop() synchronously on the + # event-loop thread would wedge the entire loop (mic, + # vision, session.receive) until the pulse buffer drains. + try: + await asyncio.to_thread(self._speaker.stop) + except Exception: + pass + try: + self._mic.flush() + except Exception: + pass + + # Atomic ref swap — next read_chunk / send_chunk hits new. + old_mic, old_spk = self._mic, self._speaker + self._mic = new_mic + self._speaker = new_spk + self._current_profile_id = profile_id + self._reset_turn_state() + + # Tear down old AFTER the ref swap so any executor call still + # in flight finishes against the old handle and the next loop + # iteration picks up the new one cleanly. + try: + await asyncio.to_thread(old_mic.stop) + except Exception: + pass + try: + await asyncio.to_thread(old_spk.stop) + except Exception: + pass + + # Silent context to Gemini — so it knows the input chain changed + # if asked (matches the _announce_vision_state pattern). + try: + await session.send_realtime_input(text=( + f"[AUDIO SWITCH] Mic + speaker are now on the {profile_id!s} " + f"audio profile. Do not announce this unprompted; just keep " + f"replying normally — the user's voice may sound clearer or " + f"different on the new device." + )) + except asyncio.CancelledError: + raise + except Exception as exc: + log.warning("audio-swap announce failed: %s", exc) + log.info("audio swap complete: %s → %s", prev, profile_id) + # ─── recognition state watcher ──────────────────────── # Polls data/.recognition_state.json at SANAD_RECOGNITION_POLL_S Hz and # mirrors vision_enabled / face_rec_enabled into in-memory flags so the @@ -582,6 +931,9 @@ class GeminiBrain: vision_enabled=self._vision_enabled, face_rec_enabled=self._face_rec_enabled, gallery_version=self._gallery_version_primed, + zone_rec_enabled=self._zone_rec_enabled, + zones_version=self._zones_version_primed, + movement_enabled=self._movement_enabled, ) # Best-effort initial primer if face_rec is already on at session start. if self._face_rec_enabled and self._vision_enabled: @@ -591,15 +943,41 @@ class GeminiBrain: except Exception as exc: log.warning("initial gallery primer failed: %s", exc) - # Tell Gemini the current camera + face-recognition state at session - # start — silent standing context so "can you see me?" / "do you know - # who I am?" are answered honestly even if the user never toggles. + # N3 — initial zones primer if zone recognition is already on. Unlike + # faces this does NOT require vision: name+description-only places still + # give Gemini useful knowledge to talk about. + if self._zone_rec_enabled: + try: + cur = _recog_state.read(_RECOG_STATE_PATH) + await self._send_zone_primer(session, cur.zones_version) + except Exception as exc: + log.warning("initial zone primer failed: %s", exc) + + # Tell Gemini the current camera + recognition + movement state at + # session start — silent standing context so "can you see me?" / "do + # you know who I am?" are answered honestly even if nothing is toggled. await self._announce_vision_state( session, self._vision_enabled, is_toggle=False, ) await self._announce_facerec_state( session, self._face_rec_enabled, is_toggle=False, ) + await self._announce_zonerec_state( + session, self._zone_rec_enabled, is_toggle=False, + ) + await self._announce_movement_state( + session, self._movement_enabled, is_toggle=False, + ) + # N3 — announce the active "go here" destination (if any). _nav_target + # was reset to (-1,-1) per session so this fires on every reconnect. + try: + cur = _recog_state.read(_RECOG_STATE_PATH) + nav = (cur.nav_target_zone_id, cur.nav_target_place_id) + if nav != self._nav_target: + await self._announce_nav_target(session, nav[0], nav[1]) + self._nav_target = nav + except Exception as exc: + log.warning("initial nav-target announce failed: %s", exc) while not self._done.is_set() and not self._stop_flag.is_set(): await asyncio.sleep(_RECOG_POLL_S) @@ -656,6 +1034,46 @@ class GeminiBrain: except Exception as exc: log.warning("gallery primer failed: %s", exc) + # N3 — zone-recognition toggle (announce out loud, like face-rec). + if new_state.zone_rec_enabled != last_state.zone_rec_enabled: + self._zone_rec_enabled = new_state.zone_rec_enabled + log.info("zone rec toggled → %s", self._zone_rec_enabled) + await self._announce_zonerec_state( + session, self._zone_rec_enabled, is_toggle=True, + ) + + # Re-prime zones when zone-rec just turned ON or the zones version + # bumped (any zone/place/face-link/photo CRUD). No vision needed. + zone_rec_just_on = ( + new_state.zone_rec_enabled and not last_state.zone_rec_enabled + ) + zones_changed = ( + new_state.zones_version != self._zones_version_primed + ) + if self._zone_rec_enabled and (zone_rec_just_on or zones_changed): + try: + await self._send_zone_primer( + session, new_state.zones_version, + ) + except Exception as exc: + log.warning("zone primer failed: %s", exc) + + # N3 — "go here" destination changed (set or cleared). Announce + + # show the reference photo. Diffed against the announced tuple so a + # CRUD-only version bump above doesn't double-fire this. + nav = (new_state.nav_target_zone_id, new_state.nav_target_place_id) + if nav != self._nav_target: + self._nav_target = nav + await self._announce_nav_target(session, nav[0], nav[1]) + + # N2 — movement enable/disable toggle (spoken confirmation only). + if new_state.movement_enabled != last_state.movement_enabled: + self._movement_enabled = new_state.movement_enabled + log.info("movement toggled → %s", self._movement_enabled) + await self._announce_movement_state( + session, self._movement_enabled, is_toggle=True, + ) + last_state = new_state # ─── camera frame send loop ─────────────────────────── @@ -789,3 +1207,84 @@ class GeminiBrain: return self._gallery_version_primed = version log.info("face gallery primed: %d person(s), v.%d", len(entries), version) + + # ─── zones primer (N3) ──────────────────────────────── + # One multimodal turn carrying every zone, its places (name + description + + # reference photos), and the people linked to each place. A place may have + # NO photos (name + description only), so empty image lists are tolerated. + + async def _send_zone_primer(self, session: Any, version: int) -> None: + try: + from Project.Sanad.vision.zone_gallery import ZoneGallery + except Exception as exc: + log.info("zone gallery module unavailable: %s", exc) + return + + gallery = ZoneGallery(_ZONES_DIR) + try: + entries = gallery.load_for_primer( + max_samples_per_place=_FACES_MAX_SAMPLES, + resize_long_side=_FACES_PRIMER_RESIZE, + ) + except Exception as exc: + log.warning("zone gallery load failed: %s", exc) + return + + if not entries: + log.info("zone gallery empty — primer skipped (v.%d)", version) + self._zones_version_primed = version + return + + # Resolve linked face ids → names once (cheap, small galleries). + face_names: dict[int, str] = {} + try: + from Project.Sanad.vision.face_gallery import FaceGallery + for fe in FaceGallery(_FACES_DIR).list(): + if fe.name: + face_names[fe.id] = fe.name + except Exception: + pass + + parts: list[dict[str, Any]] = [{ + "text": ( + "ZONES PRIMER (do not reply to this turn). Below are the zones " + "and places you know, with the people often found at each place. " + "Use them to answer where things are, to name a place when the " + "live camera shows one, and to make directions personal. Do not " + "invent zones or places that are not listed here." + ), + }] + n_zones = n_places = 0 + for zone, places in entries: + n_zones += 1 + zhdr = f"\n# Zone: {zone.name or '(unnamed)'}" + if zone.description: + zhdr += f" — {zone.description}" + parts.append({"text": zhdr}) + if not places: + parts.append({"text": " (no places yet)"}) + for place, jpegs in places: + n_places += 1 + label = f"\n - Place: {place.name or '(unnamed)'}" + if place.description: + label += f" — {place.description}" + people = [face_names[f] for f in place.face_ids if f in face_names] + if people: + label += f" | People often here: {', '.join(people)}" + parts.append({"text": label}) + for jpeg in jpegs: + parts.append({ + "inline_data": {"mime_type": "image/jpeg", "data": jpeg}, + }) + + try: + await session.send_client_content( + turns=[{"role": "user", "parts": parts}], + turn_complete=True, + ) + except Exception as exc: + log.warning("zone primer send failed: %s", exc) + return + self._zones_version_primed = version + log.info("zones primed: %d zone(s), %d place(s), v.%d", + n_zones, n_places, version) diff --git a/gemini/subprocess.py b/gemini/subprocess.py index 2bafb43..7ed8060 100644 --- a/gemini/subprocess.py +++ b/gemini/subprocess.py @@ -36,6 +36,14 @@ _LS_CFG = _cfg_section("gemini", "subprocess") # SANAD_VISION_SEND_HZ default. The child de-stales + relays to Gemini. _FRAME_FORWARD_INTERVAL_S = float(_LS_CFG.get("frame_forward_interval_sec", 0.5)) +# Audio profile watcher — poll pactl for the Anker USB device at this +# interval, send "profile:" to the child on every state change. +_AUDIO_WATCH_INTERVAL_S = float(_LS_CFG.get("audio_watch_interval_sec", 1.5)) + +# The Anker profile id, as defined in voice/audio_devices.py. When this +# profile is fully plugged (both sink + source present), we switch the +# child to "anker"; otherwise we hold the boot fallback profile. +_ANKER_PROFILE_ID = "anker_powerconf" def _resolve_live_script() -> Path: """Locate the voice script to run as subprocess. @@ -82,29 +90,72 @@ class GeminiSubprocess: self.process: subprocess.Popen | None = None self.log_tail: deque[str] = deque(maxlen=LOG_TAIL_SIZE) self.user_transcript: deque[str] = deque(maxlen=TRANSCRIPT_TAIL_SIZE) + # Gemini's OWN spoken text (output transcription). The movement + # dispatcher (N2) polls this the way LiveVoiceLoop polls + # user_transcript — it reads what Gemini *said* and fires motion on + # a confirmation-phrase match (the Marcus pattern). Also handy for + # surfacing the bot side of the conversation on the dashboard. + self.bot_transcript: deque[str] = deque(maxlen=TRANSCRIPT_TAIL_SIZE) + # N2 Phase 3 — callbacks fired with each new BOT: line (Gemini's own + # spoken text). The MovementDispatcher registers here to drive + # locomotion off Gemini's confirmation phrases. Fired on the reader + # thread; callbacks must be cheap / non-blocking (the dispatcher just + # enqueues to its own worker). + self._bot_callbacks: list = [] + # _track_line (which runs under self._lock) stashes the latest BOT text + # here; the reader loop fires callbacks AFTER releasing the lock so a + # slow callback (e.g. movement dispatch reading state) never stalls the + # reader thread or blocks log parsing. + self._pending_bot: str | None = None self._reader_thread: threading.Thread | None = None self._log_file = None # opened per-session in _reader_loop self.state = "stopped" self.state_message = "Idle." self.last_user_text = "" + self.last_bot_text = "" self.suppressed_noise = 0 - # ── stdin push channel (camera frames + motion state) ── - # The child (gemini/script.py) reads "frame:\n" and - # "state:\n" lines off its stdin. Writes are serialised - # because the frame forwarder and the motion-state bus handler - # both call from different threads. + # ── stdin push channel (camera frames + motion state + profile) ── + # The child (gemini/script.py) reads "frame:\n", + # "state:\n", and "profile:\n" lines off its stdin. + # Writes are serialised because the frame forwarder, motion-state + # bus handler, and audio watcher all call from different threads. self._stdin_lock = threading.Lock() self._camera = None # set via attach_camera() self._frame_thread: threading.Thread | None = None self._frame_stop = threading.Event() + # ── audio profile hot-swap ──────────────────────────────── + # _audio_mgr is the parent's AudioManager — needed so we can keep + # PulseAudio defaults in sync (so /api/records/play etc. follow + # the same device the live session uses). Set via attach_audio_manager. + self._audio_mgr = None + self._audio_thread: threading.Thread | None = None + self._audio_stop = threading.Event() + # The boot profile captured at start() — what we revert to when + # the Anker is unplugged. Read from env (already in LIVE_TUNE). + self._boot_profile_id: str = "builtin" + # Last profile signalled to the child (for edge-only dispatch). + self._last_profile_id: str | None = None # ── camera attach (called once from main.py) ────────────── + def register_bot_callback(self, callback) -> None: + """Register a fn(text) fired on each new BOT: line (Gemini's spoken + text). Used by the N2 movement dispatcher. Cheap/non-blocking only.""" + if callback not in self._bot_callbacks: + self._bot_callbacks.append(callback) + def attach_camera(self, camera) -> None: """Give the supervisor a reference to the CameraDaemon so it can forward frames to the child over stdin while a session runs.""" self._camera = camera + def attach_audio_manager(self, audio_mgr) -> None: + """Hand the parent's AudioManager to the supervisor so the audio + watcher can keep PulseAudio defaults in sync on every swap (so + typed-replay / record playback follow the same device as the live + Gemini session).""" + self._audio_mgr = audio_mgr + def _open_session_log(self, pid: int): """Open (or re-open) the per-day append log file for this session.""" try: @@ -145,6 +196,19 @@ class GeminiSubprocess: self.last_user_text = text self.user_transcript.append(text) self._set_state("hearing", f"User: {text}") + elif " BOT: " in line or line.strip().startswith("BOT:"): + # GeminiBrain emits: log.info("BOT: %s", text) — Gemini's own + # spoken text. The movement dispatcher (N2) reads this deque to + # match confirmation phrases. Deliberately does NOT change the + # session state (that stays driven by USER / listening markers). + # NOTE: must precede the generic "listening" catch below, else a + # bot line that happens to contain "listening" would be misrouted. + text = line.split("BOT:", 1)[1].strip() + if text: + self.last_bot_text = text + self.bot_transcript.append(text) + # Defer callback firing to the reader loop, OUTSIDE self._lock. + self._pending_bot = text elif "BARGE-IN" in line or "Gemini interrupted" in line or "interrupt (" in line: self._set_state("interrupting", line) elif "listening" in line.lower() and "no speech" not in line: @@ -175,12 +239,23 @@ class GeminiSubprocess: fh.write(clean + "\n") except Exception: pass + fired_bot = None with self._lock: if self._is_noisy(clean): self.suppressed_noise += 1 continue self.log_tail.append(clean) self._track_line(clean) + fired_bot = self._pending_bot + self._pending_bot = None + # Fire BOT-text callbacks (movement dispatch) OUTSIDE the lock so a + # slow callback can't stall transcript parsing. + if fired_bot is not None: + for cb in self._bot_callbacks: + try: + cb(fired_bot) + except Exception: + log.exception("bot-text callback failed") with self._lock: self.log_tail.append("Live Gemini process exited.") self._set_state("stopped", "Process exited.") @@ -244,13 +319,23 @@ class GeminiSubprocess: env=env, ) - # Reap any stale frame forwarder from a previous session that ended - # by a child crash rather than a clean stop() — otherwise it keeps - # spinning on a dead pipe and we'd leak a thread per restart. - stale_ft = self._frame_thread - if stale_ft is not None and stale_ft.is_alive(): - self._frame_stop.set() - stale_ft.join(timeout=2.0) + # Reap any stale frame forwarder / audio watcher from a previous + # session that ended by a child crash rather than a clean stop() — + # otherwise they'd keep spinning and we'd leak threads per restart. + for stale, stop_evt in ( + (self._frame_thread, self._frame_stop), + (self._audio_thread, self._audio_stop), + ): + if stale is not None and stale.is_alive(): + stop_evt.set() + stale.join(timeout=2.0) + + # Capture the boot profile for this session — the audio watcher + # uses it as the fallback when the Anker is unplugged. Env var is + # already in LIVE_TUNE so parent + child agree. + self._boot_profile_id = os.environ.get( + "SANAD_AUDIO_PROFILE", "builtin").strip().lower() + self._last_profile_id = None # force one initial send_profile with self._lock: self.process = proc @@ -264,6 +349,13 @@ class GeminiSubprocess: target=self._frame_forwarder, daemon=True, name="gemini-frame-fwd", ) self._frame_thread.start() + # Audio watcher — polls pactl for Anker presence and signals + # the child to hot-swap mic+speaker when it changes. + self._audio_stop.clear() + self._audio_thread = threading.Thread( + target=self._audio_watcher, daemon=True, name="gemini-audio-watcher", + ) + self._audio_thread.start() log.info("Live Gemini subprocess started: pid=%d", proc.pid) return {"started": True, "pid": proc.pid} @@ -344,6 +436,107 @@ class GeminiSubprocess: # Best-effort — never let a frame hiccup kill the thread. pass + # ── audio profile watcher (parent-side detection) ──────────── + + def send_profile(self, profile_id: str, reason: str = "") -> None: + """Push an audio-profile hot-swap command to the child as + 'profile:\\n'. The child's _stdin_watcher parses it and + _audio_swap_loop performs the actual mic/speaker rebind. No-op + if the process isn't running or stdin is closed.""" + pid = (profile_id or "").strip().lower() + if pid not in {"builtin", "anker", "hollyland_builtin"}: + log.warning("send_profile: ignoring unknown profile %r", profile_id) + return + payload: dict[str, Any] = {"id": pid} + if reason: + payload["reason"] = reason[:120] + try: + line = "profile:" + json.dumps(payload, ensure_ascii=False) + "\n" + except Exception: + return + self._send_stdin(line) + + def _audio_watcher(self) -> None: + """Background thread — poll pactl for the Anker USB device, signal + the child on every plug/unplug edge transition. + + Detection reuses voice.audio_devices.detect_plugged_profiles() which + already shells to `pactl list short` and matches against the same + `powerconf,anker` substring AnkerMic uses. Zero new deps. + + Edge-only dispatch: we only call send_profile() when the target + flips. Rapid bounce (loose cable) is naturally rate-limited by the + poll interval. After every send_profile we also refresh the parent + audio_manager's PulseAudio defaults so non-live playback (typed + replay, record playback) follows the same device. + """ + # Lazy import — voice.audio_devices is imported at module load to + # check pactl availability without polluting our top-level imports. + try: + from Project.Sanad.voice import audio_devices as _ad + except Exception as exc: + log.warning("audio watcher disabled — audio_devices import failed: %s", exc) + return + try: + if not _ad.pactl_available(): + log.warning("audio watcher disabled — pactl not available") + return + except Exception: + # If pactl_available itself isn't exposed, fall through and try + # detect_plugged_profiles — it'll raise/return empty if pactl + # is missing and we handle that below. + pass + + boot_profile = self._boot_profile_id or "builtin" + log.info("audio watcher started — Anker→anker, no-Anker→%s (poll=%.1fs)", + boot_profile, _AUDIO_WATCH_INTERVAL_S) + + while not self._audio_stop.is_set(): + if self._audio_stop.wait(_AUDIO_WATCH_INTERVAL_S): + break + try: + # Recovery script (set_powerconf_audio.sh) is intentionally + # NOT invoked from the watcher — its old card-discovery / + # module-alsa-source attempts loaded the wrong hw device + # on this Jetson and knocked the Anker out of pactl + # entirely (observed 2026-06-03). The script is now a + # passive set-default-sink/source helper meant to be run + # by hand, not from the watcher. The watcher just detects + # plug edges and dispatches profile changes to the child. + plugged = _ad.detect_plugged_profiles() + ids = {p.get("profile", {}).get("id") for p in (plugged or [])} + anker_present = _ANKER_PROFILE_ID in ids + target = "anker" if anker_present else boot_profile + reason = "anker plugged" if anker_present else "anker unplugged" + # Surface which detection path succeeded (Path A vs pactl) + if anker_present: + for p in plugged: + if p.get("profile", {}).get("id") == _ANKER_PROFILE_ID: + via = p.get("source_via", "pactl") + if via != "pactl": + reason += f" via {via}" + break + + if target == self._last_profile_id: + continue # edge-only + + prev = self._last_profile_id + log.info("audio watcher: %s → %s (%s)", + prev or "—", target, reason) + self.send_profile(target, reason=reason) + self._last_profile_id = target + + # Keep PulseAudio defaults aligned so non-live playback + # follows the same device the live session uses. + if self._audio_mgr is not None: + try: + self._audio_mgr.refresh_devices() + except Exception as exc: + log.warning("audio watcher: refresh_devices failed: %s", exc) + except Exception as exc: + # Never let a transient pactl glitch kill the thread. + log.warning("audio watcher iteration failed: %s", exc) + def stop(self) -> dict[str, Any]: with self._lock: proc = self.process @@ -351,12 +544,17 @@ class GeminiSubprocess: return {"stopped": False, "message": "Not running."} self._set_state("stopping", "Stopping...") - # Halt the frame forwarder before we tear the pipe down. + # Halt forwarder + audio watcher before we tear the pipe down. self._frame_stop.set() + self._audio_stop.set() ft = self._frame_thread if ft is not None: ft.join(timeout=2.0) self._frame_thread = None + at = self._audio_thread + if at is not None: + at.join(timeout=2.0) + self._audio_thread = None try: proc.send_signal(signal.SIGINT) @@ -398,7 +596,9 @@ class GeminiSubprocess: "state": self.state, "state_message": self.state_message, "last_user_text": self.last_user_text, + "last_bot_text": self.last_bot_text, "user_transcript": list(self.user_transcript), + "bot_transcript": list(self.bot_transcript), "log_tail": list(self.log_tail), "suppressed_noise": self.suppressed_noise, } diff --git a/main.py b/main.py index 0f16102..e700b20 100644 --- a/main.py +++ b/main.py @@ -108,6 +108,9 @@ GeminiSubprocess = _safe_import("GeminiSubprocess", lambda: __import__(" LocalSubprocess = _safe_import("LocalSubprocess", lambda: __import__("Project.Sanad.local.subprocess", fromlist=["LocalSubprocess"]).LocalSubprocess) CameraDaemon = _safe_import("CameraDaemon", lambda: __import__("Project.Sanad.vision.camera", fromlist=["CameraDaemon"]).CameraDaemon) FaceGallery = _safe_import("FaceGallery", lambda: __import__("Project.Sanad.vision.face_gallery", fromlist=["FaceGallery"]).FaceGallery) +ZoneGallery = _safe_import("ZoneGallery", lambda: __import__("Project.Sanad.vision.zone_gallery", fromlist=["ZoneGallery"]).ZoneGallery) +LocoController = _safe_import("LocoController", lambda: __import__("Project.Sanad.G1_Controller.loco_controller", fromlist=["LocoController"]).LocoController) +MovementDispatcher = _safe_import("MovementDispatcher", lambda: __import__("Project.Sanad.voice.movement_dispatch", fromlist=["MovementDispatcher"]).MovementDispatcher) # ── global instances (imported by route modules) ── @@ -133,6 +136,51 @@ else: live_sub = _safe_construct("live_sub", GeminiSubprocess) typed_replay = _safe_construct("typed_replay", (lambda: TypedReplayEngine(voice_client, audio_mgr)) if (TypedReplayEngine and voice_client and audio_mgr) else None) +# ── Locomotion controller (N2) — manual dashboard locomotion ──────────────── +# Reuses the arm controller's single ChannelFactoryInitialize (one DDS init per +# process) — it does NOT init DDS itself. Disarmed every boot. See +# G1_Controller/loco_controller.py and dashboard/routes/controller.py. +loco_controller = _safe_construct( + "loco_controller", + (lambda: LocoController(arm)) if (LocoController and arm) else None) + +# Arm ⇄ locomotion mutual exclusion: the arm must NEVER run a replay / SDK +# action / gesture while the robot may be walking. `movement_active` is True for +# the MANUAL gate (armed/teleop) AND for ~1.5s after any move/step — so it also +# covers Phase-3 Gemini-driven moves (which call loco.move/step directly). +# Checked at every arm playback chokepoint (replay_file / _execute), so it blocks +# voice/Gemini-triggered gestures too, not just the dashboard. +if arm is not None and loco_controller is not None: + try: + if hasattr(arm, "set_motion_block"): + arm.set_motion_block(loco_controller.movement_active) + log.info("Arm motion-block wired to locomotion movement_active") + except Exception: + log.exception("Could not wire arm motion-block") + +# ── Gemini voice → movement dispatcher (N2 Phase 3) ───────────────────────── +# Reads Gemini's spoken (BOT) transcript via the live supervisor's bot-callback +# and drives loco_controller on a confirmation-phrase match (Marcus pattern). +# Gated on recognition_state.movement_enabled (the "Enable Gemini movement" +# toggle) — SEPARATE from the manual arm flag. Inert until that flag is on. +movement_dispatch = None +if MovementDispatcher and loco_controller is not None: + try: + from Project.Sanad.config import BASE_DIR as _BD2, MOTIONS_DIR as _MD + movement_dispatch = _safe_construct( + "movement_dispatch", + lambda: MovementDispatcher( + loco_controller, + _MD / "instruction.json", + _BD2 / "data" / ".recognition_state.json")) + if movement_dispatch is not None: + movement_dispatch.start() + if live_sub is not None and hasattr(live_sub, "register_bot_callback"): + live_sub.register_bot_callback(movement_dispatch.on_bot_text) + log.info("Movement dispatcher wired to Gemini BOT transcript") + except Exception: + log.exception("Could not wire movement dispatcher") + # ── Recognition (camera + face gallery) ───────────────────────────────────── # Camera is idle until the dashboard toggles vision on; face gallery is pure # file IO and always available if the import succeeded. @@ -176,8 +224,20 @@ def _build_gallery(): root = p if p.is_absolute() else (BASE_DIR / raw) return FaceGallery(root) -camera = _safe_construct("camera", _build_camera if CameraDaemon else None) -gallery = _safe_construct("gallery", _build_gallery if FaceGallery else None) +def _build_zone_gallery(): + # N3 — zones gallery (zone → place → linked faces). Honours SANAD_ZONES_DIR + # (absolute) then the core_config 'zones' section dir_rel, then a default. + from Project.Sanad.config import BASE_DIR + from Project.Sanad.core.config_loader import section as _cfg_section + zones_cfg = _cfg_section("core", "zones") + raw = os.environ.get("SANAD_ZONES_DIR") or zones_cfg.get("dir_rel", "data/zones") + p = Path(raw) + root = p if p.is_absolute() else (BASE_DIR / raw) + return ZoneGallery(root) + +camera = _safe_construct("camera", _build_camera if CameraDaemon else None) +gallery = _safe_construct("gallery", _build_gallery if FaceGallery else None) +zone_gallery = _safe_construct("zone_gallery", _build_zone_gallery if ZoneGallery else None) # Restore persisted vision_enabled at boot — start camera if the user left # it on across a reboot. Face-rec state is read by the Gemini child directly. @@ -205,6 +265,18 @@ if live_sub is not None and camera is not None: except Exception: log.exception("attach_camera failed") +# Hand the AudioManager to the supervisor so the audio watcher can keep +# PulseAudio defaults aligned with the live profile on every Anker +# plug/unplug. Without this, typed-replay / record playback would stay on +# the boot device even after the live session swapped to Anker. +if live_sub is not None and audio_mgr is not None: + try: + if hasattr(live_sub, "attach_audio_manager"): + live_sub.attach_audio_manager(audio_mgr) + log.info("AudioManager attached to live subprocess supervisor") + except Exception: + log.exception("attach_audio_manager failed") + # ── Motion-state → Gemini channel ─────────────────────────────────────────── # The arm controller emits motion.action_started / _done / _error on the bus. # Forward each to the Gemini child as a 'state:' line so the live session can @@ -271,6 +343,9 @@ SUBSYSTEMS = { "typed_replay": typed_replay, "camera": camera, "gallery": gallery, + "zone_gallery": zone_gallery, + "loco_controller": loco_controller, + "movement_dispatch": movement_dispatch, } # Critical subsystems — if any of these are None, log a warning at startup @@ -310,6 +385,18 @@ def _do_shutdown(from_signal: bool = False): except Exception: log.exception("arm.disable() failed") + if movement_dispatch is not None: + try: + movement_dispatch.stop() + except Exception: + log.exception("movement_dispatch.stop() failed") + + if loco_controller is not None: + try: + loco_controller.shutdown() # StopMove (no FSM change) + disarm + except Exception: + log.exception("loco_controller.shutdown() failed") + if live_sub is not None: try: running = live_sub.is_running() if callable(getattr(live_sub, "is_running", None)) else False diff --git a/motion/arm_controller.py b/motion/arm_controller.py index f68c280..a996600 100644 --- a/motion/arm_controller.py +++ b/motion/arm_controller.py @@ -45,6 +45,11 @@ ARM_INDICES = range( _AC.get("arm_indices_start", 15), _AC.get("arm_indices_stop", 29), ) +# N1 — motor temperature sampling. rt/lowstate arrives ~500 Hz; building the +# per-motor temperature snapshot that often is wasteful, so we refresh it at +# most this often. The 3D dashboard polls the snapshot over a WebSocket at a +# similar rate. +_TEMP_SAMPLE_INTERVAL_S = float(_AC.get("temp_sample_interval_sec", 0.1)) # -- SDK import (optional) -- try: @@ -193,6 +198,12 @@ class ArmController: self._last_action_time = 0.0 self.cooldown_sec = 1.0 self._initialized = False + # N2 — arm ⇄ locomotion mutual exclusion. While locomotion movement is + # armed, the arm must NOT run any replay / SDK action / gesture. main.py + # wires this to LocoController.is_armed via set_motion_block(); checked at + # every playback chokepoint (replay_file / _execute), so it also blocks + # voice/Gemini-triggered gestures, not just the dashboard. + self._motion_block = None # DDS handles (set in init()) self._arm_pub = None @@ -206,6 +217,10 @@ class ArmController: self._last_state_time = 0.0 self._state_lock = threading.Lock() self._first_state_event = threading.Event() + # N1 — latest per-motor temperatures (throttled snapshot for the 3D + # temperature dashboard). Each entry: {motor_id, surface, winding}. + self._current_temps: list[dict[str, Any]] = [] + self._last_temp_time = 0.0 # Cached motion file metadata self._motion_files_cache: dict[str, dict[str, Any]] = {} @@ -271,13 +286,45 @@ class ArmController: return self._initialized def _on_low_state(self, msg): - """Callback from DDS subscriber — updates current joint positions.""" + """Callback from DDS subscriber — updates current joint positions. + + Also refreshes the per-motor temperature snapshot (N1) at most every + _TEMP_SAMPLE_INTERVAL_S so the 3D temperature dashboard has live data + without a second DDS subscriber. + """ + now = time.monotonic() with self._state_lock: self._current_q = [float(msg.motor_state[i].q) for i in range(G1_NUM_MOTOR)] - self._last_state_time = time.monotonic() + self._last_state_time = now if not self._first_state_event.is_set(): self._first_state_event.set() + # Throttled temperature capture (separate from q, which we keep at the + # full callback rate for the replay watchdog). + if (now - self._last_temp_time) >= _TEMP_SAMPLE_INTERVAL_S: + temps = [] + for i in range(G1_NUM_MOTOR): + ms = msg.motor_state[i] + surface = winding = None + t = getattr(ms, "temperature", None) + try: + # G1 firmware reports [surface, winding]; some builds give + # a single value or a scalar — handle all three defensively + # (matches Marcus/Features/TempMonitor low_state_callback). + if t is not None and hasattr(t, "__len__"): + if len(t) >= 2: + surface, winding = int(t[0]), int(t[1]) + elif len(t) == 1: + surface = winding = int(t[0]) + elif t is not None: + surface = winding = int(t) + except (TypeError, ValueError): + surface = winding = None + temps.append({"motor_id": i, "surface": surface, "winding": winding}) + with self._state_lock: + self._current_temps = temps + self._last_temp_time = now + def wait_for_state(self, timeout: float = 2.0) -> bool: """Block until first LowState_ callback fires (or timeout). Returns True if state received.""" return self._first_state_event.wait(timeout=timeout) @@ -288,6 +335,16 @@ class ArmController: """Public read of current joint positions.""" return self._get_current_q() + def get_motor_temps(self) -> list[dict[str, Any]]: + """Public read of the latest per-motor temperature snapshot (N1). + + Returns a list of {motor_id, surface, winding} (values may be None if + the firmware didn't report a temperature). Empty until the first + LowState_ callback fires. + """ + with self._state_lock: + return list(self._current_temps) + def send_frame(self, arm_target_q: list[float], body_lock_q: list[float]): """Public single-frame send. Use only inside a controlled playback loop.""" self._send_frame(arm_target_q, body_lock_q) @@ -362,6 +419,9 @@ class ArmController: filepath: Path to .jsonl motion file. speed: Playback speed multiplier (1.0 = normal). """ + if self._blocked(): + log.warning("replay_file refused — locomotion movement is armed") + return with self._lock: if self._is_busy: log.warning("replay_file: arm busy, skipping") @@ -536,6 +596,22 @@ class ArmController: def is_busy(self) -> bool: return self._is_busy + def set_motion_block(self, predicate): + """Install a no-args predicate; while it returns True, every arm + playback path (replay_file / _execute → SDK actions, JSONL replays, + macros, gestures) refuses to run. Used for arm ⇄ locomotion exclusion.""" + self._motion_block = predicate + + def _blocked(self) -> bool: + pred = self._motion_block + if pred is None: + return False + try: + return bool(pred()) + except Exception: + log.exception("motion-block predicate raised — treating as NOT blocked") + return False + def cancel(self): """Cancel the currently running replay. @@ -666,6 +742,11 @@ class ArmController: self._execute(action, speed) def _execute(self, action: Action, speed: float = 1.0): + if self._blocked(): + log.warning("arm action %s refused — locomotion movement is armed", action.name) + bus.emit_sync("motion.action_error", action=action.name, + reason="movement armed") + return with self._lock: if self._is_busy: log.warning("Arm busy, skipping %s", action.name) diff --git a/note.txt b/note.txt new file mode 100644 index 0000000..6619b81 --- /dev/null +++ b/note.txt @@ -0,0 +1,12 @@ +N2 — Gemini-phrase movement via direct LocoClient · effort L (highest risk) +This is the Marcus phrase-confirmation pattern, but driven by direct LocoClient on the robot (no Holosoma/ZMQ, no laptop). The full chain: + +Gemini speaks a confirmation phrase → parent reads it → matches → LocoClient.Move(). + +Where LocoClient lives → the parent process, next to the arm. main.py:425 already calls ChannelFactoryInitialize once for the arm; the new LocoController reuses that same DDS participant (no second init). Port init_loco/move_cmd/stop_only from G1_Lootah/Controller/g1_mode_controller.py, plus a non-interactive FSM-200 bring-up adapted from hanger_boot_sequence.py (strip its own ChannelFactoryInitialize and the blocking input()). +The dispatch loop (voice/movement_loop.py) mirrors voice/live_voice_loop.py but polls the new bot_transcript, gated on a movement_enabled flag (default OFF). The matcher (voice/bot_dispatch.py) ports Marcus's _dispatch_gemini_bot: strip [STATE-]/quoted/question clauses → normalize numbers + Arabic→English → parametric-regex scan → longest-needle bot-phrase scan → dedup/cooldown. Vocabulary comes from a ported instruction.json. +Canonical → velocity (Marcus's MOVE_MAP ports 1:1 since LocoClient.Move uses the same m/s, rad/s units): forward→Move(0.3,0,0), backward→Move(-0.2,0,0), turn-left→Move(0,0,0.3), turn-right→Move(0,0,-0.3), stop→StopMove(). +Toggle in the Voice & Audio tab (#tab-voice): add movement_enabled to recognition_state.py, a dashboard/routes/movement.py route (clone the face-rec toggle), and a switch in the UI. +Announce on toggle: _announce_movement_state in script.py (clone _announce_facerec_state) — robot says "movement enabled" / "movement disabled." +Persona rules: append Marcus-style rules to Sanad's prompt (wake-word + action required; clarification questions must not contain motion verbs) so Gemini reliably emits the confirmation phrases. +🛑 Safety is mandatory, not optional: Move(continous_move=True) walks until StopMove. Needs a FIFO worker with stop-priority, a per-motion watchdog that forces StopMove after a max duration, velocity caps + deadband, and StopMove on both disable-toggle and shutdown. Sanad has no obstacle/abort stack today. \ No newline at end of file diff --git a/shell_scripts/reset_anker_usb.sh b/shell_scripts/reset_anker_usb.sh new file mode 100755 index 0000000..c50b850 --- /dev/null +++ b/shell_scripts/reset_anker_usb.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# reset_anker_usb.sh — unbind+rebind snd-usb-audio for an Anker USB device. +# +# WHY THIS EXISTS +# The Anker PowerConf A3321 on this Jetson sometimes enumerates with only +# output USB Audio Class descriptors (no capture interface). PulseAudio +# then shows the card with only output-only profiles and the dashboard +# can't expose the mic. Restarting PulseAudio does nothing — UAC +# descriptors are parsed by snd-usb-audio at probe time, persist in +# kernel structs, and only get re-parsed on a fresh driver bind. +# +# `/api/audio/usb-reset` writes directly to +# /sys/bus/usb/drivers/snd-usb-audio/{unbind,bind} when possible. That +# path needs root. This script exists as a sudo fallback so the dashboard +# can recover without Sanad itself running as root. +# +# USAGE +# reset_anker_usb.sh — unbind+rebind given device +# (bus_id like "1-3") +# reset_anker_usb.sh --setup-sudoers — install one-time NOPASSWD entry +# (must be run via sudo) +# +# Exit codes: +# 0 — unbind + rebind both succeeded +# 1 — bus_id missing or device not present +# 2 — no snd-usb-audio interfaces bound to that device +# 3 — unbind or bind sysfs write failed +# 4 — --setup-sudoers used outside of sudo + +set -u + +USAGE="usage: $(basename "$0") or $(basename "$0") --setup-sudoers" + +if [ "$#" -lt 1 ]; then + echo "$USAGE" >&2 + exit 1 +fi + +# ───────────────────── --setup-sudoers ───────────────────── +if [ "$1" = "--setup-sudoers" ]; then + if [ "$(id -u)" -ne 0 ]; then + echo "❌ --setup-sudoers must run as root (use: sudo $0 --setup-sudoers)" >&2 + exit 4 + fi + # Install a NOPASSWD entry so the unitree user can invoke THIS exact + # script path with sudo without typing a password. Scoped to one + # binary; not a blanket sudo grant. + SELF_PATH="$(readlink -f "$0")" + SUDO_FILE="/etc/sudoers.d/sanad-anker-usb-reset" + cat > "$SUDO_FILE" <&2 + exit 1 +fi + +# Discover snd-usb-audio interfaces on this device. Don't unbind anything +# else (some Anker firmwares present HID-Consumer for the mute button on +# a separate interface — we leave those alone). +declare -a IFACES=() +for iface_path in "${DEV_PATH}/${BUS_ID}:"*; do + [ -e "$iface_path" ] || continue + driver_link="${iface_path}/driver" + [ -L "$driver_link" ] || continue + driver=$(basename "$(readlink "$driver_link")") + if [ "$driver" = "snd-usb-audio" ]; then + IFACES+=("$(basename "$iface_path")") + fi +done + +if [ "${#IFACES[@]}" -eq 0 ]; then + echo "❌ No snd-usb-audio interfaces bound to device $BUS_ID" >&2 + exit 2 +fi + +echo "ℹ️ Re-binding snd-usb-audio for $BUS_ID (interfaces: ${IFACES[*]})" + +UNBIND="/sys/bus/usb/drivers/snd-usb-audio/unbind" +BIND="/sys/bus/usb/drivers/snd-usb-audio/bind" + +# Unbind first; on failure exit before rebind so we don't leave the device +# in a half-bound state. +for iface in "${IFACES[@]}"; do + if ! echo -n "$iface" > "$UNBIND" 2>/dev/null; then + echo "❌ unbind failed: $iface → $UNBIND" >&2 + exit 3 + fi + echo " unbound: $iface" +done + +# Brief settle — snd-usb-audio's release path tears down ALSA card N. +sleep 0.5 + +for iface in "${IFACES[@]}"; do + if ! echo -n "$iface" > "$BIND" 2>/dev/null; then + echo "❌ rebind failed: $iface → $BIND" >&2 + exit 3 + fi + echo " bound: $iface" +done + +# Let probe complete so callers can pactl list cards right after. +sleep 1.0 +echo "✅ snd-usb-audio re-bound for $BUS_ID" +exit 0 diff --git a/shell_scripts/start_sanad.sh b/shell_scripts/start_sanad.sh index 056a22c..7997815 100755 --- a/shell_scripts/start_sanad.sh +++ b/shell_scripts/start_sanad.sh @@ -27,6 +27,25 @@ export PORT="${PORT:-8000}" # Mandatory environment fixes for Jetson + conda + Unitree SDK export LD_PRELOAD="${LD_PRELOAD:-/usr/lib/aarch64-linux-gnu/libgomp.so.1}" export PYTHONUNBUFFERED=1 + +# Point conda's bundled PyAudio/PortAudio at the SYSTEM ALSA plugin dir +# so it can load the `pulse` PCM plugin (from the libasound2-plugins apt +# package). Without this, conda's libasound looks in its own env path +# (which doesn't ship plugins) → PortAudio's device list has no 'pulse' +# entry → AnkerMic/AnkerSpeaker fall back to direct hw:N grabs which +# fail with paInvalidSampleRate (mic) or kick PulseAudio off the card +# (speaker). Same pattern used by AI_Photographer's photo_sanad.sh on +# this Jetson. +if [ -d "/usr/lib/aarch64-linux-gnu/alsa-lib" ]; then + export ALSA_PLUGIN_DIR="/usr/lib/aarch64-linux-gnu/alsa-lib" +elif [ -d "/usr/lib/alsa-lib" ]; then + export ALSA_PLUGIN_DIR="/usr/lib/alsa-lib" +fi +if [ -f "/usr/share/alsa/alsa.conf" ]; then + export ALSA_CONFIG_PATH="/usr/share/alsa/alsa.conf" +fi +export ALSA_LOG_LEVEL=0 + # Optional: needed if/when the local pipeline imports CosyVoice if [ -d "$HOME/CosyVoice" ]; then export PYTHONPATH="$HOME/CosyVoice:$HOME/CosyVoice/third_party/Matcha-TTS:${PYTHONPATH:-}" diff --git a/vision/recognition_state.py b/vision/recognition_state.py index f7a21d3..ccd4dd3 100644 --- a/vision/recognition_state.py +++ b/vision/recognition_state.py @@ -6,9 +6,14 @@ to flip its in-memory flags without a session restart. Format (data/.recognition_state.json): { - "vision_enabled": bool, - "face_rec_enabled": bool, - "gallery_version": int # bumped on every face CRUD + "vision_enabled": bool, + "face_rec_enabled": bool, + "gallery_version": int, # bumped on every face CRUD + "zone_rec_enabled": bool, # N3 — zones/places knowledge toggle + "zones_version": int, # bumped on every zone/place CRUD + "nav_target_zone_id": int, # active "go here" destination (0 = none) + "nav_target_place_id": int, + "movement_enabled": bool # N2 — Gemini-driven locomotion gate } """ @@ -26,18 +31,38 @@ class RecognitionState: vision_enabled: bool = False face_rec_enabled: bool = False gallery_version: int = 0 + # N3 — zones/places knowledge (zone → place → linked faces) + zone_rec_enabled: bool = False + zones_version: int = 0 + # "Go here" destination — the active place the robot should head to. + # 0/0 = no destination set. Actual locomotion is wired by N2. + nav_target_zone_id: int = 0 + nav_target_place_id: int = 0 + # N2 — Gemini-driven locomotion enable gate (default OFF for safety) + movement_enabled: bool = False def read(path: Path) -> RecognitionState: """Return the persisted state, or a default if missing/corrupt.""" try: raw = json.loads(Path(path).read_text(encoding="utf-8")) - except (FileNotFoundError, json.JSONDecodeError, OSError): + except (FileNotFoundError, json.JSONDecodeError, OSError, + UnicodeDecodeError, ValueError): + # UnicodeDecodeError (bad UTF-8 bytes) and ValueError (e.g. a non-dict + # top-level) are not OSError/JSONDecodeError subclasses — catch them too + # so read() honours its "never raises" contract on a corrupt file. + return RecognitionState() + if not isinstance(raw, dict): return RecognitionState() return RecognitionState( vision_enabled=bool(raw.get("vision_enabled", False)), face_rec_enabled=bool(raw.get("face_rec_enabled", False)), gallery_version=int(raw.get("gallery_version", 0)), + zone_rec_enabled=bool(raw.get("zone_rec_enabled", False)), + zones_version=int(raw.get("zones_version", 0)), + nav_target_zone_id=int(raw.get("nav_target_zone_id", 0)), + nav_target_place_id=int(raw.get("nav_target_place_id", 0)), + movement_enabled=bool(raw.get("movement_enabled", False)), ) diff --git a/vision/zone_gallery.py b/vision/zone_gallery.py new file mode 100644 index 0000000..6f518f6 --- /dev/null +++ b/vision/zone_gallery.py @@ -0,0 +1,440 @@ +"""Zone gallery — file IO over data/zones/zone_{zid}/place_{pid}/. + +A two-level hierarchy that replaces the flat place gallery: + + Zone = name + description (a region, e.g. "Ground Floor") + └─ Place = name + description + optional reference photos + + optional linked face ids (people associated with the place) + +Layout: + zones/ + zone_{zid}/ + meta.json {name, description, added_at} + place_{pid}/ + meta.json {name, description, face_ids:[int], added_at} + place_1.jpg ← optional reference photos (0..N) + place_2.png + +`face_ids` reference enrolled faces in the SEPARATE face gallery +(data/faces/face_{id}); this module only stores the ids — name/photo +resolution is done by the caller (route layer + Gemini primer). + +Reference photos let Gemini visually recognise the place (and, later, let the +robot navigate to it). A place needs only a name + description; photos and +linked faces are both optional. Thread-safe via a single internal RLock. +""" + +from __future__ import annotations + +import io +import json +import re +import threading +import zipfile +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Iterable + +from Project.Sanad.core.logger import get_logger + +log = get_logger("zone_gallery") + +_ZONE_DIR_RE = re.compile(r"^zone_(\d+)$") +_PLACE_DIR_RE = re.compile(r"^place_(\d+)$") +ALLOWED_EXTS = {".jpg", ".jpeg", ".png"} +SAMPLE_NAME_RE = re.compile(r"^place_(\d+)\.(jpg|jpeg|png)$", re.IGNORECASE) + + +@dataclass +class PlaceEntry: + id: int + zone_id: int + name: str | None + added_at: str | None + dir: Path + description: str | None = None + face_ids: list[int] = field(default_factory=list) + sample_paths: list[Path] = field(default_factory=list) + + def to_dict(self) -> dict: + return { + "id": self.id, + "zone_id": self.zone_id, + "name": self.name, + "description": self.description, + "face_ids": list(self.face_ids), + "added_at": self.added_at, + "photos": [ + {"name": p.name, "size_bytes": p.stat().st_size} + for p in self.sample_paths if p.exists() + ], + } + + +@dataclass +class ZoneEntry: + id: int + name: str | None + added_at: str | None + dir: Path + description: str | None = None + places: list[PlaceEntry] = field(default_factory=list) + + def to_dict(self) -> dict: + return { + "id": self.id, + "name": self.name, + "description": self.description, + "added_at": self.added_at, + "places": [p.to_dict() for p in self.places], + } + + +class ZoneGallery: + """File-system backed zone/place gallery rooted at `root` (data/zones/).""" + + def __init__(self, root: Path | str) -> None: + self.root = Path(root) + self._lock = threading.RLock() + + # ── paths ──────────────────────────────────────────────── + + def _ensure_root(self) -> None: + self.root.mkdir(parents=True, exist_ok=True) + + def _zone_dir(self, zone_id: int) -> Path: + return self.root / f"zone_{zone_id}" + + def _place_dir(self, zone_id: int, place_id: int) -> Path: + return self.root / f"zone_{zone_id}" / f"place_{place_id}" + + def _iter_zone_dirs(self) -> Iterable[tuple[int, Path]]: + if not self.root.exists(): + return + for child in sorted(self.root.iterdir()): + if not child.is_dir(): + continue + m = _ZONE_DIR_RE.match(child.name) + if m: + yield int(m.group(1)), child + + def _iter_place_dirs(self, zone_dir: Path) -> Iterable[tuple[int, Path]]: + if not zone_dir.exists(): + return + for child in sorted(zone_dir.iterdir()): + if not child.is_dir(): + continue + m = _PLACE_DIR_RE.match(child.name) + if m: + yield int(m.group(1)), child + + def _samples_in(self, place_dir: Path) -> list[Path]: + return [p for p in sorted(place_dir.iterdir()) + if p.is_file() and p.suffix.lower() in ALLOWED_EXTS] + + # ── meta ───────────────────────────────────────────────── + + def _zone_meta(self, zone_dir: Path) -> tuple[str | None, str | None, str | None]: + meta_path = zone_dir / "meta.json" + if not meta_path.exists(): + return None, None, None + try: + data = json.loads(meta_path.read_text(encoding="utf-8")) + except Exception: + return None, None, None + return (data.get("name") or None), (data.get("description") or None), data.get("added_at") + + def _place_meta(self, place_dir: Path) -> tuple[str | None, str | None, list[int], str | None]: + meta_path = place_dir / "meta.json" + if not meta_path.exists(): + return None, None, [], None + try: + data = json.loads(meta_path.read_text(encoding="utf-8")) + except Exception: + return None, None, [], None + raw_ids = data.get("face_ids") or [] + face_ids: list[int] = [] + for x in raw_ids: + try: + face_ids.append(int(x)) + except (TypeError, ValueError): + continue + return (data.get("name") or None), (data.get("description") or None), face_ids, data.get("added_at") + + def _write_zone_meta(self, zone_dir: Path, name, description, added_at=None) -> None: + meta: dict = {} + if name: + meta["name"] = name + if description: + meta["description"] = description + meta["added_at"] = added_at or datetime.now().isoformat(timespec="seconds") + (zone_dir / "meta.json").write_text( + json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") + + def _write_place_meta(self, place_dir: Path, name, description, + face_ids=None, added_at=None) -> None: + meta: dict = {} + if name: + meta["name"] = name + if description: + meta["description"] = description + meta["face_ids"] = [int(x) for x in (face_ids or [])] + meta["added_at"] = added_at or datetime.now().isoformat(timespec="seconds") + (place_dir / "meta.json").write_text( + json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") + + # ── read ───────────────────────────────────────────────── + + def _build_place(self, zone_id: int, place_id: int, place_dir: Path) -> PlaceEntry: + name, desc, face_ids, added = self._place_meta(place_dir) + return PlaceEntry( + id=place_id, zone_id=zone_id, name=name, description=desc, + face_ids=face_ids, added_at=added, dir=place_dir, + sample_paths=self._samples_in(place_dir), + ) + + def _build_zone(self, zone_id: int, zone_dir: Path) -> ZoneEntry: + name, desc, added = self._zone_meta(zone_dir) + places = [self._build_place(zone_id, pid, pdir) + for pid, pdir in self._iter_place_dirs(zone_dir)] + return ZoneEntry(id=zone_id, name=name, description=desc, + added_at=added, dir=zone_dir, places=places) + + def list_zones(self) -> list[ZoneEntry]: + with self._lock: + return [self._build_zone(zid, zdir) for zid, zdir in self._iter_zone_dirs()] + + def get_zone(self, zone_id: int) -> ZoneEntry | None: + with self._lock: + zd = self._zone_dir(zone_id) + return self._build_zone(zone_id, zd) if zd.is_dir() else None + + def get_place(self, zone_id: int, place_id: int) -> PlaceEntry | None: + with self._lock: + pd = self._place_dir(zone_id, place_id) + return self._build_place(zone_id, place_id, pd) if pd.is_dir() else None + + def get_photo(self, zone_id: int, place_id: int, photo_name: str) -> Path | None: + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + return None + p = pd / photo_name + try: + p.resolve().relative_to(pd.resolve()) + except ValueError: + return None + if not p.exists() or p.suffix.lower() not in ALLOWED_EXTS: + return None + return p + + # ── ids ────────────────────────────────────────────────── + + def next_zone_id(self) -> int: + with self._lock: + return max((zid for zid, _ in self._iter_zone_dirs()), default=0) + 1 + + def next_place_id(self, zone_id: int) -> int: + with self._lock: + zd = self._zone_dir(zone_id) + return max((pid for pid, _ in self._iter_place_dirs(zd)), default=0) + 1 + + def _next_sample_name(self, place_dir: Path, ext: str) -> str: + max_n = 0 + for p in self._samples_in(place_dir): + m = SAMPLE_NAME_RE.match(p.name) + if m: + max_n = max(max_n, int(m.group(1))) + return f"place_{max_n + 1}{ext.lower()}" + + @staticmethod + def _detect_ext(data: bytes) -> str: + if len(data) >= 8 and data[:8] == b"\x89PNG\r\n\x1a\n": + return ".png" + return ".jpg" + + # ── zone write ─────────────────────────────────────────── + + def create_zone(self, name: str | None = None, + description: str | None = None) -> ZoneEntry: + with self._lock: + self._ensure_root() + zid = self.next_zone_id() + zd = self._zone_dir(zid) + zd.mkdir(parents=True, exist_ok=False) + self._write_zone_meta(zd, (name or "").strip() or None, + (description or "").strip() or None) + log.info("Created zone_%d (name=%s)", zid, name or "(unnamed)") + return self._build_zone(zid, zd) + + def rename_zone(self, zone_id: int, name: str | None) -> None: + with self._lock: + zd = self._zone_dir(zone_id) + if not zd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id} not found") + _, desc, added = self._zone_meta(zd) + self._write_zone_meta(zd, (name or "").strip() or None, desc, added_at=added) + log.info("Renamed zone_%d → %s", zone_id, name or "(unnamed)") + + def describe_zone(self, zone_id: int, description: str | None) -> None: + with self._lock: + zd = self._zone_dir(zone_id) + if not zd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id} not found") + name, _, added = self._zone_meta(zd) + self._write_zone_meta(zd, name, (description or "").strip() or None, added_at=added) + log.info("Described zone_%d", zone_id) + + def delete_zone(self, zone_id: int) -> None: + import shutil + with self._lock: + zd = self._zone_dir(zone_id) + if not zd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id} not found") + shutil.rmtree(zd) + log.info("Deleted zone_%d (and its places)", zone_id) + + # ── place write ────────────────────────────────────────── + + def create_place(self, zone_id: int, name: str | None = None, + description: str | None = None, + face_ids: list[int] | None = None, + image_bytes_list: list[bytes] | None = None) -> PlaceEntry: + with self._lock: + zd = self._zone_dir(zone_id) + if not zd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id} not found") + pid = self.next_place_id(zone_id) + pd = self._place_dir(zone_id, pid) + pd.mkdir(parents=True, exist_ok=False) + for idx, data in enumerate(image_bytes_list or [], start=1): + (pd / f"place_{idx}{self._detect_ext(data)}").write_bytes(data) + self._write_place_meta(pd, (name or "").strip() or None, + (description or "").strip() or None, face_ids or []) + log.info("Created zone_%d/place_%d (name=%s, photos=%d, faces=%d)", + zone_id, pid, name or "(unnamed)", + len(image_bytes_list or []), len(face_ids or [])) + return self._build_place(zone_id, pid, pd) + + def rename_place(self, zone_id: int, place_id: int, name: str | None) -> None: + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + _, desc, fids, added = self._place_meta(pd) + self._write_place_meta(pd, (name or "").strip() or None, desc, fids, added_at=added) + log.info("Renamed zone_%d/place_%d → %s", zone_id, place_id, name or "(unnamed)") + + def describe_place(self, zone_id: int, place_id: int, description: str | None) -> None: + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + name, _, fids, added = self._place_meta(pd) + self._write_place_meta(pd, name, (description or "").strip() or None, fids, added_at=added) + log.info("Described zone_%d/place_%d", zone_id, place_id) + + def set_place_faces(self, zone_id: int, place_id: int, face_ids: list[int]) -> None: + """Replace the set of linked face ids for a place.""" + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + name, desc, _, added = self._place_meta(pd) + clean = [] + for x in (face_ids or []): + try: + clean.append(int(x)) + except (TypeError, ValueError): + continue + self._write_place_meta(pd, name, desc, clean, added_at=added) + log.info("Set zone_%d/place_%d faces → %s", zone_id, place_id, clean) + + def add_photo(self, zone_id: int, place_id: int, image_bytes: bytes) -> str: + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + fname = self._next_sample_name(pd, self._detect_ext(image_bytes)) + (pd / fname).write_bytes(image_bytes) + log.info("Added %s to zone_%d/place_%d", fname, zone_id, place_id) + return fname + + def delete_photo(self, zone_id: int, place_id: int, photo_name: str) -> None: + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + target = self.get_photo(zone_id, place_id, photo_name) + if target is None: + raise FileNotFoundError(f"photo {photo_name} not found") + target.unlink() + log.info("Deleted %s from zone_%d/place_%d", photo_name, zone_id, place_id) + + def delete_place(self, zone_id: int, place_id: int) -> None: + import shutil + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + shutil.rmtree(pd) + log.info("Deleted zone_%d/place_%d", zone_id, place_id) + + def zip_place(self, zone_id: int, place_id: int) -> bytes: + with self._lock: + pd = self._place_dir(zone_id, place_id) + if not pd.is_dir(): + raise FileNotFoundError(f"zone_{zone_id}/place_{place_id} not found") + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for p in sorted(pd.iterdir()): + if p.is_file(): + zf.write(p, arcname=f"zone_{zone_id}_place_{place_id}/{p.name}") + return buf.getvalue() + + # ── primer support (used by gemini/script.py) ─────────── + + def load_for_primer(self, max_samples_per_place: int = 3, + resize_long_side: int = 256 + ) -> list[tuple[ZoneEntry, list[tuple[PlaceEntry, list[bytes]]]]]: + """Return [(ZoneEntry, [(PlaceEntry, [jpeg_bytes,…]), …]), …]. + + Photo lists may be empty (name+description-only place). Photos are + resized to longest-side <= resize_long_side, re-encoded JPEG q=85. + """ + out: list[tuple[ZoneEntry, list[tuple[PlaceEntry, list[bytes]]]]] = [] + for zone in self.list_zones(): + place_jpegs: list[tuple[PlaceEntry, list[bytes]]] = [] + for place in zone.places: + jpegs: list[bytes] = [] + for p in place.sample_paths[:max_samples_per_place]: + try: + raw = p.read_bytes() + except OSError: + continue + jpegs.append(self._resize_for_primer(raw, resize_long_side) or raw) + place_jpegs.append((place, jpegs)) + out.append((zone, place_jpegs)) + return out + + @staticmethod + def _resize_for_primer(raw: bytes, long_side: int) -> bytes | None: + try: + from PIL import Image # type: ignore + except Exception: + return None + try: + img = Image.open(io.BytesIO(raw)) + img.load() + if img.mode not in ("RGB", "L"): + img = img.convert("RGB") + w, h = img.size + scale = long_side / max(w, h) if max(w, h) > long_side else 1.0 + if scale < 1.0: + img = img.resize((max(1, int(w * scale)), max(1, int(h * scale))), Image.LANCZOS) + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=85, optimize=True) + return buf.getvalue() + except Exception: + return None diff --git a/voice/audio_devices.py b/voice/audio_devices.py index acc39be..583136b 100644 --- a/voice/audio_devices.py +++ b/voice/audio_devices.py @@ -236,19 +236,301 @@ def find_first_match(items: list[dict[str, str]], pattern: str, return None +# PyAudio fallback cache — avoid re-init'ing PyAudio on every poll +# (PyAudio init takes ~100 ms and the watcher polls at 1.5 s). +_PYAUDIO_CACHE: dict[str, Any] = {"ts": 0.0, "input_names": []} +_PYAUDIO_TTL_S = 2.0 + + +def _pyaudio_input_names() -> list[str]: + """Return lowercase names of all PyAudio input devices. Cached for ~2 s. + + Used as a fallback in detect_plugged_profiles() when pactl can't see + a profile's source — some USB devices (notably the Anker PowerConf on + JetPack 5) only expose their mic via the raw ALSA layer, which PyAudio + can still open even when PulseAudio's card profile is output-only. + """ + import time as _t + now = _t.time() + if now - _PYAUDIO_CACHE["ts"] < _PYAUDIO_TTL_S: + return _PYAUDIO_CACHE["input_names"] + names: list[str] = [] + try: + import pyaudio # type: ignore + pa = pyaudio.PyAudio() + try: + for i in range(pa.get_device_count()): + try: + info = pa.get_device_info_by_index(i) + except Exception: + continue + if info.get("maxInputChannels", 0) <= 0: + continue + names.append(str(info.get("name", "")).lower()) + finally: + pa.terminate() + except Exception as exc: + log.debug("PyAudio enumeration unavailable: %s", exc) + _PYAUDIO_CACHE["ts"] = now + _PYAUDIO_CACHE["input_names"] = names + return names + + +def _pyaudio_input_matches(pattern: str) -> dict[str, str] | None: + """If any PyAudio input device name matches one of the comma-separated + patterns, return a synthetic source dict (matches find_first_match()'s + shape). Else None. + """ + if not pattern: + return None + needles = [p.strip().lower() for p in pattern.split(",") if p.strip()] + if not needles: + return None + for name in _pyaudio_input_names(): + if any(n in name for n in needles): + # Synthetic — mark the origin so logs / dashboards can see it + # came from PyAudio, not pactl. Includes `description` so any + # consumer that expects the same shape as a real pactl + # source dict (`{name, description, index}`) doesn't KeyError. + return { + "name": f"pyaudio:{name}", + "driver": "pyaudio", + "description": f"PyAudio fallback — {name}", + "index": "", + } + return None + + +# Per-card cooldown for ensure_card_input_capable so a card whose firmware +# truly doesn't expose input doesn't get hammered with set-card-profile +# calls on every detection poll (every 1.5s from the live-Gemini watcher). +_CARD_PROFILE_LAST_ATTEMPT: dict[str, float] = {} +_CARD_PROFILE_COOLDOWN_S = 30.0 + + +def _parse_card_profiles(card_block: str) -> tuple[str, list[tuple[int, str, bool, bool]]]: + """Parse the `Profiles:` section of a single card stanza from + `pactl list cards`. Returns (active_profile, [(priority, name, + has_sink, has_source), ...]) — only profiles marked + `available: yes` are included. + + Profile lines look like: + \\toutput:analog-stereo+input:mono-fallback: Analog Stereo Output + Mono Input \\ + (sinks: 1, sources: 1, priority: 6501, available: yes) + """ + active = "" + profiles: list[tuple[int, str, bool, bool]] = [] + in_profiles = False + for raw in card_block.splitlines(): + line = raw.rstrip() + stripped = line.strip() + if stripped.startswith("Active Profile:"): + active = stripped[len("Active Profile:"):].strip() + continue + if stripped == "Profiles:": + in_profiles = True + continue + if in_profiles: + # End of Profiles section: next top-level key starts with non-tab + # OR an empty line. The block ends when leading whitespace drops + # to a tab/spaces shallower than the profile lines — easiest + # check: stop when we hit "Ports:" or "Active Profile:". + if stripped.startswith("Ports:") or stripped.startswith("Active Profile:"): + in_profiles = False + if stripped.startswith("Active Profile:"): + active = stripped[len("Active Profile:"):].strip() + continue + # Profile line — must contain "(sinks: N, sources: M, priority: P, available: yes)" + paren = stripped.rfind(" (") + if paren < 0 or "available: yes" not in stripped: + continue + head = stripped[:paren] + sep = head.find(": ") + if sep < 0: + continue + name = head[:sep] + props = stripped[paren+2:].rstrip(")") + sinks_n = sources_n = priority = 0 + for tok in props.split(","): + tok = tok.strip() + if tok.startswith("sinks: "): + try: sinks_n = int(tok[len("sinks: "):]) + except ValueError: pass + elif tok.startswith("sources: "): + try: sources_n = int(tok[len("sources: "):]) + except ValueError: pass + elif tok.startswith("priority: "): + try: priority = int(tok[len("priority: "):]) + except ValueError: pass + profiles.append((priority, name, sinks_n > 0, sources_n > 0)) + return active, profiles + + +def ensure_card_input_capable(card_pattern: str) -> bool: + """If a PulseAudio card whose Name matches `card_pattern` is on an + output-only profile but has an input+output profile available, switch + to the highest-priority input+output profile. + + Why: USB UAC1 conference devices (Anker PowerConf and similar) can get + pinned to an output-only profile by PulseAudio's `module-card-restore` + — observed on G1 / JetPack 5 after an earlier session left the card in + that state. The mic-capable profile is right there in the card's + advertised list (we see `output:analog-stereo+input:mono-fallback` with + `sinks: 1, sources: 1` and `available: yes`), but the active profile + is the output-only one. Calling `pactl set-card-profile` against an + already-advertised, available profile is SAFE — unlike the older + `module-alsa-source device=hw:N,0` hack, which had to guess the hw + index and broke things when it guessed wrong. + + `card_pattern` is a comma-separated substring list (same semantics as + `find_first_match`). Returns True if a switch happened OR the card was + already input-capable; False if no matching card / no switchable + profile exists. + + Rate-limited per-card via `_CARD_PROFILE_COOLDOWN_S` so a card whose + firmware genuinely can't do input doesn't get polled to death. + """ + if not pactl_available(): + return False + needles = [p.strip().lower() for p in card_pattern.split(",") if p.strip()] + if not needles: + return False + try: + r = _run_pactl(["list", "cards"]) + if r.returncode != 0: + return False + except (FileNotFoundError, subprocess.SubprocessError): + return False + + # Split into per-card blocks. `pactl list cards` separates cards with a + # blank line (and starts each with "Card #N"). + import time as _t + blocks: list[str] = [] + current: list[str] = [] + for line in (r.stdout or "").splitlines(): + if line.startswith("Card #") and current: + blocks.append("\n".join(current)) + current = [] + current.append(line) + if current: + blocks.append("\n".join(current)) + + switched_any = False + for block in blocks: + # Extract card Name + card_name = "" + for line in block.splitlines(): + s = line.strip() + if s.startswith("Name: "): + card_name = s[len("Name: "):].strip() + break + if not card_name: + continue + if not any(n in card_name.lower() for n in needles): + continue + + active, profiles = _parse_card_profiles(block) + if not profiles: + continue + + # If active profile already has input, nothing to do. + active_has_input = any( + name == active and has_src + for _, name, _, has_src in profiles + ) + if active_has_input: + return True + + # Find best input+output profile + candidates = [(prio, name) for prio, name, has_sink, has_src in profiles + if has_sink and has_src] + if not candidates: + # Card has no input+output profile (firmware truly output-only). + log.debug("ensure_card_input_capable: %s has no input+output " + "profile — nothing to switch to", card_name) + continue + + now = _t.time() + last = _CARD_PROFILE_LAST_ATTEMPT.get(card_name, 0.0) + if (now - last) < _CARD_PROFILE_COOLDOWN_S: + continue + _CARD_PROFILE_LAST_ATTEMPT[card_name] = now + + # Sort: highest priority first; on ties, alphabetical name asc so + # `mono-fallback` wins over `multichannel-input` (the source name + # `...mono-fallback` matches Sanad's hardcoded SOURCE patterns and + # the AI_Photographer setup that's known to work on this hardware). + candidates.sort(key=lambda x: (-x[0], x[1])) + target = candidates[0][1] + log.info("ensure_card_input_capable: %s active=%r → %r " + "(exposes mic to PulseAudio)", + card_name, active or "?", target) + try: + sr = _run_pactl(["set-card-profile", card_name, target]) + if sr.returncode == 0: + switched_any = True + else: + log.warning("ensure_card_input_capable: set-card-profile " + "%s %r failed: %s", card_name, target, + (sr.stderr or "").strip()) + except (FileNotFoundError, subprocess.SubprocessError) as exc: + log.warning("ensure_card_input_capable: pactl error: %s", exc) + return switched_any + + def detect_plugged_profiles() -> list[dict[str, Any]]: - """Return all profiles whose sink AND source are currently plugged in.""" + """Return all profiles whose sink AND source are currently plugged in. + + For each profile, the source is resolved in three passes: + 1. pactl list short sources (standard path) + 2. `ensure_card_input_capable` to unstick output-only PulseAudio + card profiles (Anker UAC1 quirk) — re-checks pactl sources + after the switch + 3. PyAudio device list (fallback — see _pyaudio_input_matches docstring) + + A profile counts as "plugged" if the sink matches via pactl AND a source + is found via ANY pass. + """ sinks = list_sinks() sources = list_sources() detected: list[dict[str, Any]] = [] + refreshed_sources = False for prof in PROFILES: sink = find_first_match(sinks, prof.sink_pattern) + if not sink: + continue src = find_first_match(sources, prof.source_pattern, exclude_monitors=True) + via = "pactl" + if src is None: + # Try to unstick the card's PulseAudio profile (most common + # cause of "sink present, source missing" on Anker). Re-list + # sources once if any switch happened — and cache for the rest + # of this detection pass so we don't re-list per profile. + switched = ensure_card_input_capable(prof.sink_pattern) + if switched and not refreshed_sources: + sources = list_sources() + refreshed_sources = True + src = find_first_match(sources, prof.source_pattern, + exclude_monitors=True) + if src is not None: + via = "pactl-after-profile-switch" + log.info("detect_plugged_profiles: %s source appeared " + "after card-profile switch: %s", + prof.id, src.get("name", "?")) + if src is None: + # Last resort — PyAudio may still see the mic. + src = _pyaudio_input_matches(prof.source_pattern) + if src is not None: + via = "pyaudio" + log.info("detect_plugged_profiles: %s source resolved via " + "PyAudio fallback (pactl missed it): %s", + prof.id, src.get("name", "?")) if sink and src: detected.append({ "profile": asdict(prof), "sink": sink, "source": src, + "source_via": via, }) return detected @@ -294,7 +576,8 @@ def current_selection() -> dict[str, Any]: Order: 1. Saved profile selection (if its sink/source still plugged) - 2. Saved explicit sink/source pair + 2. Saved explicit sink/source pair (ONLY if both still exist in pactl — + otherwise the saved manual override is stale and we fall through) 3. DEFAULT profile (builtin) if detected 4. First detected profile (in declaration order) 5. pactl defaults @@ -313,22 +596,41 @@ def current_selection() -> dict[str, Any]: return { "source_kind": "profile", "profile": d["profile"], - "sink": d["sink"]["name"], - "source": d["source"]["name"], - "sink_description": d["sink"]["description"], - "source_description": d["source"]["description"], + "sink": d["sink"].get("name", ""), + "source": d["source"].get("name", ""), + "sink_description": d["sink"].get("description", ""), + "source_description": d["source"].get("description", ""), } - # 2. Saved explicit sink/source - if state.get("sink") and state.get("source"): - return { - "source_kind": "manual", - "profile": None, - "sink": state["sink"], - "source": state["source"], - "sink_description": "", - "source_description": "", + # 2. Saved explicit sink/source — but VERIFY both names still exist in + # pactl before returning them. Without this check, unplugging a device + # (e.g. the Anker) leaves the dashboard showing the dead manual override + # forever, play_wav routes to a non-existent sink, PortAudio raises + # paBadIODeviceCombination, and the user gets no audio. Falling through + # here lets steps 3–5 re-resolve to whatever's actually plugged in + # (typically the builtin/chest), no user click required. + saved_sink = (state.get("sink") or "").strip() + saved_source = (state.get("source") or "").strip() + if saved_sink and saved_source: + sink_names = {s["name"] for s in (list_sinks() if pactl_available() else [])} + source_names = { + s["name"] for s in (list_sources() if pactl_available() else []) } + if saved_sink in sink_names and saved_source in source_names: + return { + "source_kind": "manual", + "profile": None, + "sink": saved_sink, + "source": saved_source, + "sink_description": "", + "source_description": "", + } + log.info( + "current_selection: manual override stale (sink=%s present=%s, " + "source=%s present=%s) — falling through to auto-detect", + saved_sink, saved_sink in sink_names, + saved_source, saved_source in source_names, + ) # 3. Default profile if it is plugged in if DEFAULT_PROFILE_ID in detected_by_id: @@ -336,10 +638,10 @@ def current_selection() -> dict[str, Any]: return { "source_kind": "default", "profile": d["profile"], - "sink": d["sink"]["name"], - "source": d["source"]["name"], - "sink_description": d["sink"]["description"], - "source_description": d["source"]["description"], + "sink": d["sink"].get("name", ""), + "source": d["source"].get("name", ""), + "sink_description": d["sink"].get("description", ""), + "source_description": d["source"].get("description", ""), } # 4. First detected profile (in declaration order) @@ -348,10 +650,10 @@ def current_selection() -> dict[str, Any]: return { "source_kind": "auto", "profile": d["profile"], - "sink": d["sink"]["name"], - "source": d["source"]["name"], - "sink_description": d["sink"]["description"], - "source_description": d["source"]["description"], + "sink": d["sink"].get("name", ""), + "source": d["source"].get("name", ""), + "sink_description": d["sink"].get("description", ""), + "source_description": d["source"].get("description", ""), } # 5. pactl defaults (system-wide) @@ -381,7 +683,19 @@ def current_selection() -> dict[str, Any]: # ───────────────────────── apply selection ───────────────────────── def apply_selection(sink: str, source: str) -> dict[str, Any]: - """Run pactl set-default-* and unmute. Returns {ok, errors}.""" + """Run pactl set-default-* and unmute. Returns {ok, errors}. + + A source name starting with `pyaudio:` is the synthetic marker emitted + by detect_plugged_profiles() when the source was resolved only via the + PyAudio fallback (PulseAudio doesn't expose the mic, but PortAudio can + open it directly via raw ALSA). We can't `pactl set-default-source` on + a synthetic name — pactl would error. So in that case we set only the + sink and skip the source; Sanad's live mic path uses PortAudio direct + via AnkerMic's substring index lookup and doesn't depend on the pactl + default source. Dashboard playback (audio_manager.play_wav) records + from whatever pactl considers default — that stays on the boot mic + until the recovery script (Path B) fully exposes Anker in PulseAudio. + """ errors: list[str] = [] if sink: if not set_default_sink(sink): @@ -392,7 +706,12 @@ def apply_selection(sink: str, source: str) -> dict[str, Any]: except (FileNotFoundError, subprocess.SubprocessError): pass if source: - if not set_default_source(source): + if source.startswith("pyaudio:"): + log.info("apply_selection: source is PyAudio-direct (%s) — " + "skipping pactl set-default-source. Live mic path " + "uses PortAudio device match; pactl defaults stay put.", + source) + elif not set_default_source(source): errors.append(f"set-default-source failed: {source}") else: try: diff --git a/voice/audio_io.py b/voice/audio_io.py index 6dcb87c..7b0bdc3 100644 --- a/voice/audio_io.py +++ b/voice/audio_io.py @@ -303,8 +303,32 @@ class _PyAudioMic(Mic): self._thread: Optional[threading.Thread] = None def _resolve_device_index(self) -> Optional[int]: + """Pick the PyAudio input device to open. + + Preference order: + 1. PortAudio's 'pulse' device — routes capture through + PulseAudio's default source. We MUST use this on USB UAC1 + cards (Anker PowerConf, Hollyland) — opening hw:N,0 + directly grabs ALSA exclusively, which makes PulseAudio's + module-alsa-card lose the device. That drops the card + from `pactl list`, the parent's audio watcher then sees + "anker unplugged" within seconds, and reverts the brain + to the boot profile (chest speaker). The dashboard's + "Apply" call already pins PulseAudio's default + source/sink to the matching Anker device, so opening + 'pulse' lands on the right hardware. + 2. PortAudio's 'default' device — also pulse-routed on a + standard Pulse-on-Linux setup, used as a fallback. + 3. Substring match against the device_pattern (legacy + direct-hw path) — only used when neither 'pulse' nor + 'default' is enumerated by PortAudio, which would only + happen on a system without Pulse. Logs a WARN because + this is the path that triggers the bug above. + """ if self._pa is None: return None + pulse_idx = default_idx = pattern_idx = None + pattern_name = "" patterns = [p.strip().lower() for p in self._device_pattern.split(",") if p.strip()] for i in range(self._pa.get_device_count()): @@ -312,13 +336,39 @@ class _PyAudioMic(Mic): if info.get("maxInputChannels", 0) <= 0: continue name_lower = str(info.get("name", "")).lower() - if any(n in name_lower for n in patterns): - return i + if pulse_idx is None and name_lower == "pulse": + pulse_idx = i + elif default_idx is None and name_lower == "default": + default_idx = i + if pattern_idx is None and any(n in name_lower for n in patterns): + pattern_idx = i + pattern_name = name_lower + if pulse_idx is not None: + return pulse_idx + if default_idx is not None: + return default_idx + if pattern_idx is not None: + log.warning( + "%sMic falling back to direct ALSA device '%s' " + "(no 'pulse'/'default' device exposed by PortAudio) — " + "this grabs the card exclusively and may cause PulseAudio " + "to drop it; consider installing the ALSA pulse plugin", + self._label, pattern_name, + ) + return pattern_idx return None def start(self) -> None: self._pa = pyaudio.PyAudio() idx = self._resolve_device_index() + # Log which device we picked so a "wrong sink" symptom is easy + # to attribute. Includes the device name (e.g. 'pulse' vs hw:N) + # since the index alone tells you nothing useful in a tail. + try: + picked = self._pa.get_device_info_by_index(idx) if idx is not None else {} + picked_name = picked.get("name", "?") + except Exception: + picked_name = "?" self._stream = self._pa.open( format=pyaudio.paInt16, channels=1, @@ -330,7 +380,8 @@ class _PyAudioMic(Mic): self._running = True self._thread = threading.Thread(target=self._recv_loop, daemon=True) self._thread.start() - log.info("%sMic started (device_index=%s)", self._label, idx) + log.info("%sMic started (device_index=%s name=%r)", + self._label, idx, picked_name) def _recv_loop(self) -> None: while self._running: @@ -410,10 +461,34 @@ class _PyAudioSpeaker(Speaker): self._stream_rate: Optional[int] = None self._stop_flag = threading.Event() self._total_sent = 0.0 + # Serialises every touch of self._stream / self._pa. PortAudio's + # ALSA→pulse plugin is NOT re-entrant: a concurrent snd_pcm_close + # (from stop()/wait_finish()) while another thread is inside + # snd_pcm_writei (from send_chunk()) corrupts the pulse mainloop + # heap — observed as `malloc_consolidate(): invalid chunk size` + # on barge-in. RLock so stop()→wait_finish() nesting is safe. + self._lock = threading.RLock() + # Sticky teardown signal — once stop() has run, refuse to + # lazy-reopen the stream from a late send_chunk on the same + # instance (the swap path replaces the instance entirely). + self._closed = False def _resolve_device_index(self) -> Optional[int]: + """Pick the PyAudio output device to open. + + Mirrors `_PyAudioMic._resolve_device_index` — see that method's + docstring for the rationale. Short version: prefer 'pulse' so + playback goes through PulseAudio's default sink (which the + dashboard's Apply pins to the active profile's sink); only fall + back to direct hw:N if PulseAudio isn't wired into PortAudio at + all. Grabbing hw:N exclusively makes PulseAudio drop the card + and the parent's audio watcher will then revert the brain to + the boot profile within seconds. + """ if self._pa is None: return None + pulse_idx = default_idx = pattern_idx = None + pattern_name = "" patterns = [p.strip().lower() for p in self._device_pattern.split(",") if p.strip()] for i in range(self._pa.get_device_count()): @@ -421,65 +496,154 @@ class _PyAudioSpeaker(Speaker): if info.get("maxOutputChannels", 0) <= 0: continue name_lower = str(info.get("name", "")).lower() - if any(n in name_lower for n in patterns): - return i + if pulse_idx is None and name_lower == "pulse": + pulse_idx = i + elif default_idx is None and name_lower == "default": + default_idx = i + if pattern_idx is None and any(n in name_lower for n in patterns): + pattern_idx = i + pattern_name = name_lower + if pulse_idx is not None: + return pulse_idx + if default_idx is not None: + return default_idx + if pattern_idx is not None: + log.warning( + "%sSpeaker falling back to direct ALSA device '%s' " + "(no 'pulse'/'default' device exposed by PortAudio) — " + "this grabs the card exclusively and may cause PulseAudio " + "to drop it; consider installing the ALSA pulse plugin", + self._label, pattern_name, + ) + return pattern_idx return None - def _open_stream(self, rate: int) -> None: + # USB-native rate for the underlying card. PortAudio's ALSA backend + # (the only backend available in conda's PyAudio build on Jetson) + # opens via the ALSA 'pulse' plugin, which on this system DOES NOT + # advertise rate conversion in `snd_pcm_hw_params` — opening at the + # source rate (24 kHz from Gemini TTS, etc.) gets rejected with + # paInvalidSampleRate. We pin the stream rate to the card's native + # 48 kHz and resample chunks app-side before writing. Same approach + # `_play_pcm_via_g1` uses for the DDS path. + _STREAM_TARGET_RATE = 48_000 + + def _open_stream(self, _ignored_rate: int) -> None: idx = self._resolve_device_index() + try: + picked = self._pa.get_device_info_by_index(idx) if idx is not None else {} + picked_name = picked.get("name", "?") + except Exception: + picked_name = "?" + # ALWAYS open at _STREAM_TARGET_RATE — see class docstring above. self._stream = self._pa.open( format=pyaudio.paInt16, channels=1, - rate=rate, + rate=self._STREAM_TARGET_RATE, output=True, output_device_index=idx, ) - self._stream_rate = rate - log.info("%sSpeaker output opened (device_index=%s, rate=%d)", - self._label, idx, rate) + self._stream_rate = self._STREAM_TARGET_RATE + log.info("%sSpeaker output opened (device_index=%s name=%r, rate=%d " + "— chunks resampled to this rate)", + self._label, idx, picked_name, self._STREAM_TARGET_RATE) def begin_stream(self) -> None: - self._stop_flag.clear() - self._total_sent = 0.0 - if self._pa is None: - self._pa = pyaudio.PyAudio() + # Hold the lock so a concurrent stop()/wait_finish() (from the + # barge-in path or a swap drain) cannot interleave with the + # flag clear + PyAudio init — which would otherwise re-enable + # writes against a stream the teardown is about to close. + with self._lock: + if self._closed: + # Speaker was torn down for swap or session end; do not + # revive on the same instance. Caller swap_audio_devices + # replaces the instance entirely. + return + self._stop_flag.clear() + self._total_sent = 0.0 + if self._pa is None: + self._pa = pyaudio.PyAudio() + + def _resample_mono16(self, arr, src_rate: int, dst_rate: int): + """Linear interp resample of mono int16. Returns ndarray (int16). + No-op when rates match. numpy-only — matches _play_pcm_via_g1.""" + import numpy as _np # local — keep top-level import surface unchanged + if src_rate == dst_rate or arr.size == 0: + return arr + n_out = max(1, int(arr.size * dst_rate / src_rate)) + return _np.interp( + _np.linspace(0, arr.size, n_out, endpoint=False), + _np.arange(arr.size, dtype=_np.float64), + arr.astype(_np.float64), + ).astype(_np.int16) def send_chunk(self, pcm: PCMLike, source_rate: int) -> None: - if self._stop_flag.is_set(): + # Cheap pre-check OUTSIDE the lock — avoids ever taking the + # lock for empty/late chunks once a stop has fired. Event + + # bool reads are atomic. + if self._stop_flag.is_set() or self._closed: return arr = _as_int16_array(pcm) if arr.size < 10: return - if self._pa is None: - self._pa = pyaudio.PyAudio() - if self._stream is None or self._stream_rate != source_rate: - if self._stream is not None: - try: - self._stream.stop_stream() - self._stream.close() - except Exception: - pass - self._stream = None - self._open_stream(source_rate) - try: - self._stream.write(arr.tobytes()) - self._total_sent += len(arr) / source_rate - except Exception as exc: - log.warning("%sSpeaker write failed: %s", self._label, exc) + # Resample BEFORE acquiring the lock — pure CPU, no shared + # state, keeps the critical section to just the PortAudio write + # so a concurrent stop() doesn't wait on numpy work. + if source_rate != self._STREAM_TARGET_RATE: + arr = self._resample_mono16(arr, source_rate, self._STREAM_TARGET_RATE) + payload = arr.tobytes() + sent_sec = len(arr) / self._STREAM_TARGET_RATE + with self._lock: + # CRITICAL re-check inside the lock: stop() may have run + # between our pre-check and acquiring the lock. Without + # this, the lazy-open below would resurrect a stream that + # barge-in just tore down — defeating the whole fix. + if self._stop_flag.is_set() or self._closed: + return + if self._pa is None: + self._pa = pyaudio.PyAudio() + if self._stream is None: + # Pass any rate — _open_stream ignores it and always + # opens at _STREAM_TARGET_RATE. + self._open_stream(source_rate) + stream = self._stream # snapshot — wait_finish nulls under same lock + if stream is None: # _open_stream failed + return + try: + stream.write(payload) + self._total_sent += sent_sec + except Exception as exc: + log.warning("%sSpeaker write failed: %s", self._label, exc) def wait_finish(self) -> None: - if self._stream is not None: - try: - self._stream.stop_stream() - self._stream.close() - except Exception: - pass + with self._lock: + stream = self._stream + # Null the ref BEFORE close so a racing send_chunk (waiting + # on the lock) re-checks and bails instead of touching a + # half-closed handle. Double-close-safe: if another caller + # already nulled it, we do nothing. self._stream = None self._stream_rate = None + if stream is not None: + try: + stream.stop_stream() + except Exception: + pass + try: + stream.close() + except Exception: + pass def stop(self) -> None: + # Set the flag FIRST (outside the lock — Event is atomic) so a + # concurrent send_chunk on another thread sees teardown ASAP + # even before it tries to acquire the lock. Then take the + # RLock and finish teardown; wait_finish re-enters the RLock + # safely. self._stop_flag.set() - self.wait_finish() + with self._lock: + self._closed = True + self.wait_finish() @property def interrupted(self) -> bool: @@ -515,6 +679,10 @@ class AudioIO: mic: Mic speaker: Speaker profile_id: str = field(default="builtin") + # Kept on the instance so the brain can rebuild a profile that needs + # the DDS handle (`builtin`, `hollyland_builtin`) during a hot-swap — + # without re-init'ing the channel. `repr=False` keeps it out of logs. + _audio_client: Optional[Any] = field(default=None, repr=False, compare=False) def start(self) -> None: self.mic.start() @@ -529,6 +697,44 @@ class AudioIO: except Exception: log.warning("AudioIO mic.stop failed", exc_info=True) + @classmethod + def build_backends( + cls, + profile_id: str, + *, + audio_client: Optional[Any] = None, + ) -> tuple[Mic, Speaker]: + """Return a fresh (Mic, Speaker) pair for a profile WITHOUT wrapping + in an AudioIO. Used by GeminiBrain.swap_audio_devices() for the + hot-swap path: build a new pair, switch refs, tear down the old. + + Same validation as from_profile(). `audio_client` is required for + profiles that route playback through the G1 chest speaker. + """ + raw = (profile_id or "").strip().lower() + resolved = _PROFILE_ALIASES.get(raw) + if resolved is None: + raise ValueError( + f"unknown audio profile {profile_id!r}; " + f"supported: {', '.join(SUPPORTED_PROFILES)}" + ) + if resolved == "builtin": + if audio_client is None: + raise ValueError( + "profile 'builtin' requires audio_client (G1 AudioClient)" + ) + return BuiltinMic(), BuiltinSpeaker(audio_client) + if resolved == "anker": + return AnkerMic(), AnkerSpeaker() + if resolved == "hollyland_builtin": + if audio_client is None: + raise ValueError( + "profile 'hollyland_builtin' uses the G1 speaker — " + "requires audio_client" + ) + return HollylandMic(), BuiltinSpeaker(audio_client) + raise AssertionError(f"unhandled resolved profile: {resolved!r}") + @classmethod def from_profile( cls, @@ -540,7 +746,8 @@ class AudioIO: `audio_client` is the initialised `unitree_sdk2py` `AudioClient` and is required for any profile that speaks through the G1's on-board - speaker (`builtin`, `hollyland_builtin`). + speaker (`builtin`, `hollyland_builtin`). It's also retained on the + returned AudioIO so a later hot-swap can rebuild without re-init. """ raw = (profile_id or "").strip().lower() resolved = _PROFILE_ALIASES.get(raw) @@ -549,32 +756,6 @@ class AudioIO: f"unknown audio profile {profile_id!r}; " f"supported: {', '.join(SUPPORTED_PROFILES)}" ) - - if resolved == "builtin": - if audio_client is None: - raise ValueError( - "profile 'builtin' requires audio_client (G1 AudioClient)" - ) - return cls( - mic=BuiltinMic(), - speaker=BuiltinSpeaker(audio_client), - profile_id=resolved, - ) - if resolved == "anker": - return cls( - mic=AnkerMic(), - speaker=AnkerSpeaker(), - profile_id=resolved, - ) - if resolved == "hollyland_builtin": - if audio_client is None: - raise ValueError( - "profile 'hollyland_builtin' uses the G1 speaker — " - "requires audio_client" - ) - return cls( - mic=HollylandMic(), - speaker=BuiltinSpeaker(audio_client), - profile_id=resolved, - ) - raise AssertionError(f"unhandled resolved profile: {resolved!r}") + mic, speaker = cls.build_backends(resolved, audio_client=audio_client) + return cls(mic=mic, speaker=speaker, profile_id=resolved, + _audio_client=audio_client) diff --git a/voice/audio_manager.py b/voice/audio_manager.py index 4f15b64..bff89cf 100644 --- a/voice/audio_manager.py +++ b/voice/audio_manager.py @@ -100,6 +100,12 @@ def ensure_audio_defaults(): log.warning("Audio defaults not applied: %s", exc) +class _PulseOpenFailed(RuntimeError): + """Signal from `_play_pcm_via_pulse` that PortAudio refused to open the + output stream (sink gone, bad I/O combination, etc.) — lets `play_wav` + fall back to G1 DDS chest playback so the user still hears the clip.""" + + class AudioManager: def __init__(self): if pyaudio is None: @@ -112,6 +118,11 @@ class AudioManager: self.play_lock = threading.Lock() # Lazily-initialised G1 DDS audio client (for play_wav → chest speaker) self._g1_audio_client: Any = None + # G1 playback state — present during an active play_wav() call, + # None when idle. Mutated by pause_playback/resume_playback/stop_playback + # from other threads while _play_pcm_via_g1 holds play_lock. + self._play_state_lock = threading.Lock() + self._play_state: dict[str, Any] | None = None # Resolve devices and set PulseAudio defaults at startup self.refresh_devices() ensure_audio_defaults() @@ -187,13 +198,93 @@ class AudioManager: stream.stop_stream() stream.close() - def play_wav(self, path: Path) -> dict[str, Any]: - """Play a WAV file through the G1 chest speaker via DDS when - available, falling back to the host PulseAudio sink otherwise. + # Sink-name substrings that mean "PulseAudio routes this somewhere + # audible without DDS" — extend the tuple to add more USB cards (e.g. + # hollyland sink). Matched case-insensitively. + _PULSE_SINK_MARKERS = ("anker", "powerconf", "hollyland") + # Sample rate Anker PowerConf (and most USB UAC1 cards) accept natively + # — used as the resample target before opening a PortAudio stream so + # we don't hit paInvalidSampleRate when the WAV's native rate + # (24kHz from Gemini TTS, 22050 from old TTS, etc.) doesn't match + # the card's HW caps. + _PULSE_TARGET_RATE = 48_000 - The G1's built-in audio (Jetson `platform-sound`) isn't wired to - any audible speaker — the robot's loudspeaker is only reachable - over DDS `AudioClient.PlayStream` (same pipe Gemini uses). + @staticmethod + def _resample_pcm16(pcm_bytes: bytes, channels: int, + src_rate: int, dst_rate: int) -> bytes: + """Linear-interpolation resample of int16 PCM. numpy-only (no scipy) + — matches the pattern used by `_play_pcm_via_g1`. + + Returns the resampled PCM bytes (same channel layout). No-op when + rates already match. Requires numpy (caller guards with _HAS_NUMPY). + """ + if src_rate == dst_rate or not pcm_bytes: + return pcm_bytes + arr = np.frombuffer(pcm_bytes, dtype=np.int16) + if channels > 1: + # De-interleave so each channel resamples independently + # (cheap on numpy; avoids stereo→mono surprises). + if arr.size % channels != 0: + arr = arr[: arr.size - (arr.size % channels)] + arr = arr.reshape(-1, channels) + n_in = arr.shape[0] + n_out = max(1, int(n_in * dst_rate / src_rate)) + xp = np.arange(n_in, dtype=np.float64) + x_new = np.linspace(0, n_in, n_out, endpoint=False) + cols = [ + np.interp(x_new, xp, arr[:, ch].astype(np.float64)) + for ch in range(channels) + ] + out = np.column_stack(cols).astype(np.int16) + return out.tobytes() + n_in = arr.size + n_out = max(1, int(n_in * dst_rate / src_rate)) + out = np.interp( + np.linspace(0, n_in, n_out, endpoint=False), + np.arange(n_in, dtype=np.float64), + arr.astype(np.float64), + ).astype(np.int16) + return out.tobytes() + + def _active_sink_name(self) -> str: + """Return the currently-tracked default sink name, ORIGINAL case + preserved. + + Reads `self.current_sink` which is kept in lock-step with pactl + defaults by `refresh_devices()` (called by the dashboard Apply + endpoint and by the live-Gemini watcher on profile swaps). Empty + string if nothing's tracked yet. + + IMPORTANT: PulseAudio sink names are CASE-SENSITIVE. paplay + --device= needs the exact name pactl uses (e.g. + `alsa_output.usb-Anker_PowerConf_A3321-DEV-SN1-01.analog-stereo`). + Routing-decision substring checks (against `_PULSE_SINK_MARKERS`) + lowercase BOTH sides explicitly so the case-sensitivity of the + sink name doesn't break marker matching. + """ + try: + return (self.current_sink or "").strip() + except Exception: + return "" + + def play_wav(self, path: Path, + record_name: str | None = None) -> dict[str, Any]: + """Play a WAV file through the speaker that matches the active + PulseAudio default sink: + + • Default sink is a USB conference speaker (Anker PowerConf, + Hollyland, anything matching `_PULSE_SINK_MARKERS`) → write + via PyAudio → PortAudio 'pulse' device → PulseAudio default + sink. This works even when the user picked the device via + the dashboard's "Manual sink/source override" (no profile + id) — we key off the sink name, not the profile. + • Default sink is the Jetson platform-sound (or anything that + doesn't match a marker) → use G1 DDS (`AudioClient.PlayStream`) + because platform-sound isn't wired to any audible speaker on + the G1; only the DDS pipe reaches the chest loudspeaker. + + `record_name` is purely a label surfaced via `playback_status()` + so the dashboard can show "Now playing: t6_1" etc. """ with wave.open(str(path), "rb") as wf: channels = wf.getnchannels() @@ -201,17 +292,62 @@ class AudioManager: rate = wf.getframerate() data = wf.readframes(wf.getnframes()) - # Prefer G1 chest speaker when the Unitree SDK is present - client = self._get_g1_audio_client() - if client is not None and _HAS_NUMPY and sw == 2: - self._play_pcm_via_g1(data, channels, rate) + sink = self._active_sink_name() + sink_lc = sink.lower() + # Marker check is case-insensitive; the original `sink` (with case + # preserved) is what gets passed to paplay --device. + use_pulse = any(m in sink_lc for m in self._PULSE_SINK_MARKERS) + client = self._get_g1_audio_client() if not use_pulse else None + + if not use_pulse and client is not None and _HAS_NUMPY and sw == 2: + log.info("play_wav route=g1_dds sink=%s record=%s", + sink or "?", record_name or "?") + self._play_pcm_via_g1(data, channels, rate, record_name=record_name) + route = "g1_dds" else: - if client is None and _HAS_G1_AUDIO: - log.warning("G1 AudioClient unavailable, using host PulseAudio sink") - self.play_pcm(data, channels, rate, sw) + if not use_pulse and _HAS_G1_AUDIO and client is None: + log.warning("play_wav: non-PulseAudio sink but G1 AudioClient " + "unavailable — falling back to PulseAudio default") + # Prefer paplay subprocess when it's installed — bypasses + # PortAudio (which on this Jetson's conda env doesn't expose a + # 'pulse' device, leading to PyAudio defaulting to the silent + # Jetson platform-sound card). paplay routes through PulseAudio + # at the daemon level so audio actually reaches the Anker sink. + use_paplay = bool(self._paplay_binary()) + try: + if use_paplay: + log.info("play_wav route=paplay sink=%s record=%s", + sink or "default", record_name or "?") + self._play_pcm_via_paplay(data, channels, rate, sw, + record_name=record_name) + route = "paplay" + else: + log.info("play_wav route=pulse sink=%s record=%s " + "(paplay not installed — using PyAudio)", + sink or "default", record_name or "?") + self._play_pcm_via_pulse(data, channels, rate, sw, + record_name=record_name) + route = "pulse" + except _PulseOpenFailed as exc: + # paplay spawn failed, USB device gone mid-flight, etc. + # Fall back to DDS chest if available so the user gets + # audio out of *something* rather than silence. + fb_client = self._get_g1_audio_client() + if fb_client is not None and _HAS_NUMPY and sw == 2: + log.warning("play_wav route=%s failed (%s); falling " + "back to g1_dds", + "paplay" if use_paplay else "pulse", exc) + self._play_pcm_via_g1(data, channels, rate, + record_name=record_name) + route = ("paplay" if use_paplay else "pulse") + "_failed_to_g1_dds" + else: + log.warning("play_wav pulse path failed (%s); no DDS " + "fallback available", exc) + route = ("paplay" if use_paplay else "pulse") + "_failed" duration = len(data) / (rate * channels * sw) if rate else 0 - return {"path": str(path), "duration_seconds": round(duration, 3)} + return {"path": str(path), "duration_seconds": round(duration, 3), + "route": route, "sink": sink or "default"} # -- G1 DDS-routed playback -- @@ -219,12 +355,15 @@ class AudioManager: _G1_HW_RATE = 16_000 def stop_playback(self) -> None: - """Stop any in-flight G1 DDS audio stream. + """Stop any in-flight G1 DDS audio stream + tear down the playback + state so a pause/resume cycle can't keep trying. - Used by the dashboard's Stop button to halt `play_wav` / - `_play_pcm_via_g1` mid-stream. Safe to call even when nothing - is playing — the DDS call is idempotent. + Used by the dashboard's Stop button. Safe to call even when + nothing is playing — the DDS call is idempotent. """ + with self._play_state_lock: + if self._play_state is not None: + self._play_state["stop"] = True client = self._get_g1_audio_client() if client is None: return @@ -237,22 +376,78 @@ class AudioManager: except Exception as exc: log.warning("stop_playback failed: %s", exc) - def _play_pcm_via_g1(self, pcm_bytes: bytes, channels: int, source_rate: int) -> None: - """Stream int16 PCM to the G1 chest speaker via AudioClient.PlayStream. + def pause_playback(self) -> dict[str, Any]: + """Pause the active G1 playback. The play loop notices the flag, + sends STOP_PLAY to halt the chest speaker, and advances the saved + position by the time elapsed since this chunk started. resume() + re-pushes from there. No-op if nothing is playing.""" + with self._play_state_lock: + if self._play_state is None: + return {"ok": False, "reason": "nothing playing"} + if self._play_state["paused"]: + return {"ok": True, "already": True, "paused": True} + self._play_state["paused"] = True + log.info("Playback paused (record=%s)", + self._play_state.get("record_name") or "?") + return {"ok": True, "paused": True} + + def resume_playback(self) -> dict[str, Any]: + """Resume after a pause. The play loop re-pushes pcm[pos:] to G1 + and re-enters the wait/poll cycle.""" + with self._play_state_lock: + if self._play_state is None: + return {"ok": False, "reason": "nothing playing"} + if not self._play_state["paused"]: + return {"ok": True, "already": True, "paused": False} + self._play_state["paused"] = False + log.info("Playback resumed (record=%s)", + self._play_state.get("record_name") or "?") + return {"ok": True, "resumed": True} + + def playback_status(self) -> dict[str, Any]: + """Snapshot of the current playback for the dashboard. Returns + `playing=False` when idle. `position_sec` is best-effort — + derived from elapsed wall time since the last PlayStream call.""" + with self._play_state_lock: + if self._play_state is None: + return {"playing": False, "paused": False, "record_name": None, + "position_sec": 0.0, "duration_sec": 0.0} + rate = self._play_state["rate"] or 1 + total = self._play_state["total_samples"] + pos = self._play_state["pos"] + if (not self._play_state["paused"] + and self._play_state["play_started_at"] > 0): + elapsed = time.time() - self._play_state["play_started_at"] + advance = int(max(0.0, elapsed) * rate) + pos = min(self._play_state["play_started_pos"] + advance, total) + return { + "playing": True, + "paused": self._play_state["paused"], + "record_name": self._play_state.get("record_name"), + "position_sec": round(pos / rate, 2), + "duration_sec": round(total / rate, 2) if rate else 0.0, + } + + def _play_pcm_via_g1(self, pcm_bytes: bytes, channels: int, + source_rate: int, + record_name: str | None = None) -> None: + """Stream int16 PCM to the G1 chest speaker via AudioClient.PlayStream, + with pause / resume / stop support. Converts stereo → mono and resamples to 16 kHz (the rate - AudioClient expects). Uses a fresh stream_id per call so - back-to-back plays don't interfere. + AudioClient expects). The play loop pushes pcm[pos:] in one + PlayStream call, then polls _play_state every 50 ms while the + clip drains so pause / stop are honoured promptly. Pause sends + STOP_PLAY, snapshots the position from elapsed wall time, then + loops until resumed or stopped. Resume re-pushes pcm[pos:]. """ client = self._get_g1_audio_client() if client is None: raise RuntimeError("G1 AudioClient not available") arr = np.frombuffer(pcm_bytes, dtype=np.int16) - # Stereo → mono average if channels == 2 and arr.size % 2 == 0: arr = arr.reshape(-1, 2).mean(axis=1).astype(np.int16) - # Resample to 16 kHz if source_rate != self._G1_HW_RATE and arr.size: target_len = max(1, int(len(arr) * self._G1_HW_RATE / source_rate)) arr = np.interp( @@ -260,31 +455,450 @@ class AudioManager: np.arange(len(arr)), arr.astype(np.float64), ).astype(np.int16) + rate = self._G1_HW_RATE + total_samples = len(arr) - stream_id = f"wav_{int(time.time() * 1000)}" - # Clear any lingering stream from a previous call - try: - client._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, - json.dumps({"app_name": self._G1_STREAM_APP})) - except Exception: - pass - time.sleep(0.15) + with self._play_state_lock: + self._play_state = { + "record_name": record_name, + "rate": rate, + "total_samples": total_samples, + "pos": 0, + "paused": False, + "stop": False, + "play_started_at": 0.0, + "play_started_pos": 0, + } - # Push the whole clip in one PlayStream — G1 handles buffering + # play_lock serialises overlapping play_wav() calls; pause/resume/stop + # do NOT take it (they only touch _play_state under _play_state_lock). with self.play_lock: - play_start = time.time() - client.PlayStream(self._G1_STREAM_APP, stream_id, arr.tobytes()) - total_sec = len(arr) / self._G1_HW_RATE - # Block until audio has drained (plus a small safety margin) - elapsed = time.time() - play_start - remaining = total_sec - elapsed + 0.3 - if remaining > 0: - time.sleep(remaining) try: - client._Call(ROBOT_API_ID_AUDIO_STOP_PLAY, - json.dumps({"app_name": self._G1_STREAM_APP})) - except Exception: - pass + while True: + # Snapshot the state for this iteration + with self._play_state_lock: + st = self._play_state + if st is None or st["stop"]: + break + if st["paused"]: + paused_now = True + sub_bytes = None + sub_total_sec = 0.0 + else: + paused_now = False + pos = st["pos"] + if pos >= total_samples: + break + sub_bytes = arr[pos:].tobytes() + sub_total_sec = (total_samples - pos) / rate + st["play_started_pos"] = pos + st["play_started_at"] = time.time() + + if paused_now: + time.sleep(0.1) + continue + + # Push remainder to G1 + stream_id = f"wav_{int(time.time() * 1000)}" + try: + client._Call( + ROBOT_API_ID_AUDIO_STOP_PLAY, + json.dumps({"app_name": self._G1_STREAM_APP}), + ) + except Exception: + pass + time.sleep(0.15) + client.PlayStream(self._G1_STREAM_APP, stream_id, sub_bytes) + + # Poll for pause / stop while the clip drains + poll_deadline = time.time() + sub_total_sec + 0.3 + interrupted = False + while time.time() < poll_deadline: + with self._play_state_lock: + if self._play_state is None or self._play_state["stop"]: + interrupted = True + try: + client._Call( + ROBOT_API_ID_AUDIO_STOP_PLAY, + json.dumps({"app_name": self._G1_STREAM_APP}), + ) + except Exception: + pass + break + if self._play_state["paused"]: + # Halt G1 and snapshot the new position + try: + client._Call( + ROBOT_API_ID_AUDIO_STOP_PLAY, + json.dumps({"app_name": self._G1_STREAM_APP}), + ) + except Exception: + pass + elapsed = (time.time() + - self._play_state["play_started_at"]) + advance = int(max(0.0, elapsed) * rate) + self._play_state["pos"] = min( + self._play_state["play_started_pos"] + advance, + total_samples, + ) + interrupted = True + break + time.sleep(0.05) + + if not interrupted: + # Finished naturally — mark fully consumed and exit + with self._play_state_lock: + if self._play_state is not None: + self._play_state["pos"] = total_samples + try: + client._Call( + ROBOT_API_ID_AUDIO_STOP_PLAY, + json.dumps({"app_name": self._G1_STREAM_APP}), + ) + except Exception: + pass + break + finally: + with self._play_state_lock: + self._play_state = None + + # paplay binary path. Cached on first probe so we don't keep re-shelling + # `which paplay` on every play_wav call. None = probe pending; "" = absent. + _PAPLAY_BIN: str | None = None + + @classmethod + def _paplay_binary(cls) -> str: + """Return the absolute path to `paplay` if installed, else "". + Cached for the lifetime of the process — paplay doesn't appear/ + disappear mid-run.""" + if cls._PAPLAY_BIN is None: + from shutil import which + cls._PAPLAY_BIN = which("paplay") or "" + return cls._PAPLAY_BIN + + def _play_pcm_via_paplay(self, pcm_bytes: bytes, channels: int, + sample_rate: int, sample_width: int, + record_name: str | None = None) -> None: + """Play int16 PCM via the `paplay` subprocess. Bypasses PortAudio + entirely — we just pipe raw PCM into paplay's stdin and let + PulseAudio do the resampling/format conversion/device routing. + + Why this exists: on conda's bundled PyAudio (the build shipped in + the gemini_sdk env on this Jetson), PortAudio does NOT enumerate a + 'pulse' device — only direct ALSA hw:N entries. Opening + `output_device_index=None` then defaults to hw:0 which is the + Jetson `platform-sound` card → silent (not wired to any speaker). + Opening a discrete `hw:N` for the Anker grabs the card exclusively + and PulseAudio drops it. Neither path actually plays through the + Anker. paplay sidesteps the whole stack. + + Targets the dashboard's currently-selected sink by name via + `--device=`, which guarantees the audio goes to the same + place pactl set-default-sink would have routed. + + Reuses the same `_play_state` machinery as the DDS path so the + dashboard's Pause / Stop / position-meter behave identically. + """ + sink_name = self._active_sink_name() + bytes_per_sample = max(1, channels * sample_width) + total_bytes = len(pcm_bytes) - (len(pcm_bytes) % bytes_per_sample) + total_samples = total_bytes // bytes_per_sample + chunk_bytes = max( + bytes_per_sample, (sample_rate // 10) * bytes_per_sample, + ) + # paplay format codes: s16le is the only one we ever produce here. + fmt = "s16le" if sample_width == 2 else \ + "s32le" if sample_width == 4 else \ + "u8" + # Keep cmd minimal — older paplay versions reject unknown long + # options and exit immediately (manifests as instant paplay death + + # a flood of BrokenPipeError on stdin write). --raw / --format / + # --rate / --channels / --device are all standard since 0.9.x. + cmd = [ + self._paplay_binary(), "--raw", + f"--format={fmt}", f"--rate={sample_rate}", + f"--channels={channels}", + ] + if sink_name: + cmd.extend(["--device", sink_name]) + + with self._play_state_lock: + self._play_state = { + "record_name": record_name, + "rate": sample_rate, + "total_samples": total_samples, + "pos": 0, + "paused": False, + "stop": False, + "play_started_at": 0.0, + "play_started_pos": 0, + } + + with self.play_lock: + try: + while True: + with self._play_state_lock: + st = self._play_state + if st is None or st["stop"]: + break + if st["paused"]: + time.sleep(0.1) + continue + pos = st["pos"] + if pos >= total_samples: + break + st["play_started_pos"] = pos + st["play_started_at"] = time.time() + + byte_pos = pos * bytes_per_sample + local_pos = pos + try: + proc = subprocess.Popen( + cmd, stdin=subprocess.PIPE, + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + ) + except Exception as exc: + log.warning("paplay spawn failed (%s) — signalling " + "DDS fallback", exc) + with self._play_state_lock: + self._play_state = None + raise _PulseOpenFailed(str(exc)) from exc + + # Brief settle so paplay can validate args + connect to + # PulseAudio. If it's going to die (bad sink, format, + # connection refused), it dies within ~50ms. Without + # this check, the next stdin.write() would get a sea + # of BrokenPipeError messages and the outer loop would + # keep re-spawning forever. + time.sleep(0.05) + if proc.poll() is not None: + try: + err = (proc.stderr.read() or b"").decode( + "utf-8", "replace").strip()[:400] + except Exception: + err = "" + log.warning("paplay died immediately rc=%d device=%s err=%s", + proc.returncode, sink_name or "default", err) + with self._play_state_lock: + self._play_state = None + raise _PulseOpenFailed( + f"paplay rc={proc.returncode} {err or 'no stderr'}" + ) + + interrupted = False + fatal_exc: Exception | None = None + try: + while byte_pos < total_bytes: + with self._play_state_lock: + ps = self._play_state + if ps is None or ps["stop"]: + interrupted = True + break + if ps["paused"]: + ps["pos"] = local_pos + interrupted = True + break + end = min(byte_pos + chunk_bytes, total_bytes) + try: + proc.stdin.write(pcm_bytes[byte_pos:end]) + proc.stdin.flush() + except (BrokenPipeError, OSError) as exc: + # paplay died mid-stream (USB unplugged, + # PulseAudio crashed, etc.). Abort entire + # clip — DO NOT let the outer loop respawn + # paplay; we just got hundreds of + # broken-pipe lines as a result of that bug. + try: + err = (proc.stderr.read() or b"").decode( + "utf-8", "replace").strip()[:400] + except Exception: + err = "" + log.warning("paplay died mid-stream (%s) " + "device=%s stderr=%s", + exc, sink_name or "default", err) + fatal_exc = _PulseOpenFailed( + f"paplay died: {err or exc}") + break + byte_pos = end + local_pos = byte_pos // bytes_per_sample + finally: + try: + proc.stdin.close() + except Exception: + pass + if interrupted or fatal_exc is not None: + proc.terminate() + try: + rc = proc.wait(timeout=3.0) + except subprocess.TimeoutExpired: + proc.kill() + rc = -1 + if rc != 0 and not interrupted and fatal_exc is None: + # Drained successfully but paplay exited non-zero + # — surface stderr so the failure isn't silent. + try: + err = (proc.stderr.read() or b"").decode( + "utf-8", "replace").strip()[:300] + except Exception: + err = "" + log.warning("paplay exit rc=%d device=%s err=%s", + rc, sink_name or "default", err) + + if fatal_exc is not None: + # Re-raise OUTSIDE the inner try/finally so play_wav + # catches it and falls back to G1 DDS chest. Without + # this, the outer `while True` loop would respawn + # paplay and we'd loop indefinitely. + with self._play_state_lock: + self._play_state = None + raise fatal_exc + + if not interrupted: + with self._play_state_lock: + if self._play_state is not None: + self._play_state["pos"] = total_samples + break + finally: + with self._play_state_lock: + self._play_state = None + + def _play_pcm_via_pulse(self, pcm_bytes: bytes, channels: int, + sample_rate: int, sample_width: int, + record_name: str | None = None) -> None: + """Play int16 PCM via PyAudio (→ PulseAudio default sink) with + pause / resume / stop support. + + Mirrors `_play_pcm_via_g1`'s state-poll pattern so the dashboard's + Play / Pause / Stop / Position buttons behave identically whether + the active profile uses DDS or PyAudio. Writes ~100 ms chunks so + pause / stop latency is bounded. + """ + # Make sure pactl defaults reflect the current selection — this is + # a no-op when the watcher or dashboard Apply already aligned them. + ensure_audio_defaults() + + # Resample to a USB-native rate before opening the stream. + # PortAudio's ALSA backend (the one PyAudio uses) opens the underlying + # hardware via the ALSA 'pulse' plugin, which on this Jetson does + # NOT advertise rate conversion in `snd_pcm_hw_params` — so opening + # at the WAV's native rate (24kHz from Gemini TTS, etc.) gets + # rejected with paInvalidSampleRate. Resampling app-side mirrors + # what `_play_pcm_via_g1` already does for the DDS path. Anker + # PowerConf and most USB UAC1 cards report 48kHz s16le stereo + # natively, so target that. + if _HAS_NUMPY and sample_width == 2 and sample_rate != self._PULSE_TARGET_RATE: + try: + pcm_bytes = self._resample_pcm16( + pcm_bytes, channels, sample_rate, self._PULSE_TARGET_RATE, + ) + log.info("_play_pcm_via_pulse: resampled %dHz → %dHz " + "(USB card native rate)", + sample_rate, self._PULSE_TARGET_RATE) + sample_rate = self._PULSE_TARGET_RATE + except Exception as exc: + log.warning("_play_pcm_via_pulse: resample failed (%s) — " + "trying native rate, may hit paInvalidSampleRate", + exc) + + bytes_per_sample = max(1, channels * sample_width) + total_bytes = len(pcm_bytes) - (len(pcm_bytes) % bytes_per_sample) + total_samples = total_bytes // bytes_per_sample + chunk_bytes = max(bytes_per_sample, (sample_rate // 10) * bytes_per_sample) + + with self._play_state_lock: + self._play_state = { + "record_name": record_name, + "rate": sample_rate, + "total_samples": total_samples, + "pos": 0, + "paused": False, + "stop": False, + "play_started_at": 0.0, + "play_started_pos": 0, + } + + # play_lock serialises overlapping play_wav() calls; pause/resume/stop + # only touch _play_state under _play_state_lock so they don't block. + with self.play_lock: + try: + while True: + # Snapshot — decide whether to play, wait, or exit + with self._play_state_lock: + st = self._play_state + if st is None or st["stop"]: + break + if st["paused"]: + paused_now = True + pos = 0 + else: + paused_now = False + pos = st["pos"] + if pos >= total_samples: + break + st["play_started_pos"] = pos + st["play_started_at"] = time.time() + if paused_now: + time.sleep(0.1) + continue + + byte_pos = pos * bytes_per_sample + local_pos = pos + try: + stream = self.pya.open( + format=self.pya.get_format_from_width(sample_width), + channels=channels, + rate=sample_rate, + output=True, + frames_per_buffer=CHUNK_SIZE, + ) + except Exception as exc: + # PortAudio open failed (sink gone, paBadIODevice + # combination, etc.). Signal the caller so play_wav + # can fall back to DDS chest rather than silently + # dropping the clip. + log.warning("Pulse playback open failed: %s — " + "signalling caller for DDS fallback", exc) + with self._play_state_lock: + self._play_state = None + raise _PulseOpenFailed(str(exc)) from exc + interrupted = False + try: + while byte_pos < total_bytes: + with self._play_state_lock: + ps = self._play_state + if ps is None or ps["stop"]: + interrupted = True + break + if ps["paused"]: + ps["pos"] = local_pos + interrupted = True + break + end = min(byte_pos + chunk_bytes, total_bytes) + try: + stream.write(pcm_bytes[byte_pos:end]) + except Exception as exc: + log.warning("Pulse playback write failed: %s", exc) + interrupted = True + break + byte_pos = end + local_pos = byte_pos // bytes_per_sample + finally: + try: + stream.stop_stream() + stream.close() + except Exception: + pass + + if not interrupted: + with self._play_state_lock: + if self._play_state is not None: + self._play_state["pos"] = total_samples + break + # Interrupted by pause → outer loop will wait for resume + # or exit on stop. Interrupted by stop → outer loop exits. + finally: + with self._play_state_lock: + self._play_state = None # -- recording -- diff --git a/voice/movement_dispatch.py b/voice/movement_dispatch.py new file mode 100644 index 0000000..e712f54 --- /dev/null +++ b/voice/movement_dispatch.py @@ -0,0 +1,371 @@ +"""MovementDispatcher — Gemini voice → locomotion (N2 Phase 3). + +The Marcus phrase-confirmation pattern, ported to Sanad. Gemini Live runs in a +child subprocess; the parent supervisor (gemini/subprocess.py) parses Gemini's +OWN spoken output into BOT: transcript lines and fires `on_bot_text(line)` here. + +Flow: + Gemini speaks a canonical confirmation phrase ("Turning right." / "أستدير + يميناً.") → on_bot_text() matches it against data/motions/instruction.json + → enqueues a canonical command → a worker thread drives loco_controller + (discrete steps that self-terminate with StopMove). + +Gating: every dispatch is gated on `recognition_state.movement_enabled` (the +"Enable Gemini movement" dashboard toggle) — SEPARATE from the manual +"Enable movement" arm flag (loco_controller._armed). When the flag is off, +matches are dropped. "stop" is honoured immediately (cross-thread abort + drain ++ E-STOP) regardless of cooldown. + +Safety: discrete `loco.step()` self-stops; velocity caps live in LocoController; +N-step / N-degree commands are bounded by instruction.json (max_steps, +max_degrees) and check the abort flag + enable gate between each step. Numbers +are kept verbatim but the degrees→steps and steps mapping is APPROXIMATE and +must be calibrated on the real robot. +""" + +from __future__ import annotations + +import json +import queue +import re +import threading +import time +from pathlib import Path +from typing import Any, List, Optional + +from Project.Sanad.core.logger import get_logger + +log = get_logger("movement_dispatch") + +_SENTINEL = object() +_STATE_CACHE_TTL = 0.5 # seconds — re-read recognition_state at most this often + +# Map a resolved canonical command to a LocoController discrete-step direction. +_FIXED_STEP = { + "move forward": "forward", + "move backward": "backward", + "turn right": "rotate_right", + "turn left": "rotate_left", + "slide left": "slide_left", + "slide right": "slide_right", +} + +# ── transcript cleaning (Marcus pattern) ────────────────────────────────────── +# Gemini's spoken text can CONTAIN our trigger phrases without intending a +# command — inside a question ("do you want me to move forward?"), a negation +# ("I'm not turning right"), a hypothetical ("I would be turning right"), a +# quote, or an echoed [STATE] tag. We drop those whole clauses before matching so +# only genuine confirmations actuate the robot. +_BRACKET_RE = re.compile(r"\[[^\]]*\]") # [STATE-DONE] echoes +_QUOTE_RE = re.compile(r"[\"'«»“”„‟‹›][^\"'«»“”„‟‹›]{0,80}?[\"'«»“”„‟‹›]") +_SENT_SPLIT_RE = re.compile(r"([.!?؟؛\n]+)") # keep delimiters +# NOTE: Arabic tokens are whitespace-delimited so we don't match a negation +# substring inside a real word — e.g. "ما" lives inside "أمام" (forward), "لا" +# inside many words. \b doesn't help for Arabic (all letters are \w), so we +# anchor on spaces/string-edges explicitly. +_NEG_RE = re.compile( + r"\b(?:not|never|without|cannot|would|could|should|might|instead|" + r"going to|want to|trying to|rather than)\b|\w+n['’]t\b|" + r"(?:^|\s)(?:لا|ما|لن|لم|مش|بدون|غير|لست|ليس|بدل)(?:\s|$)") + + +class MovementDispatcher: + def __init__(self, loco, instruction_path: Path, state_path: Path): + self._loco = loco + self._instruction_path = Path(instruction_path) + self._state_path = Path(state_path) + + self._queue: "queue.Queue[Any]" = queue.Queue(maxsize=32) + self._abort = threading.Event() + self._worker: Optional[threading.Thread] = None + self._running = False + # Hard-stop latch set by a dashboard E-STOP. Drops all voice commands + # until cleared (by re-enabling Gemini movement). Kept SEPARATE from the + # movement_enabled file flag so an E-STOP doesn't trigger the spoken + # "movement disabled" announcement. + self._estop = False + + # dedup / cooldown + self._last_canon = "" + self._last_at = 0.0 + + # cached enable-flag + self._enabled_cached = False + self._enabled_at = 0.0 + + # config (filled by _load) + self._cooldown = 1.5 + self._max_steps = 8 + self._max_degrees = 360 + self._deg_per_step = 15 + self._fixed_patterns: List[tuple] = [] # (needle_lower, canonical) sorted long→short + self._parametric: List[tuple] = [] # (compiled_regex, template) + + self._load() + + # ── instruction.json ───────────────────────────────────────────────────── + + def _load(self): + try: + data = json.loads(self._instruction_path.read_text(encoding="utf-8")) + except Exception as exc: + log.error("could not load %s: %s — dispatcher inert", self._instruction_path, exc) + data = {} + self._cooldown = float(data.get("command_cooldown_sec", 1.5)) + self._max_steps = int(data.get("max_steps", 8)) + self._max_degrees = int(data.get("max_degrees", 360)) + self._deg_per_step = max(1, int(data.get("degrees_per_step", 15))) + + needles: List[tuple] = [] + for spec in (data.get("actions") or {}).values(): + canonical = spec.get("canonical", "") + phrases = spec.get("bot_phrases", {}) or {} + for lang_list in phrases.values(): + for p in lang_list: + if p: + # English folded to lower; Arabic unaffected by .lower() + needles.append((p.lower(), canonical)) + # longest needle first so "walking forward" wins over "forward" + needles.sort(key=lambda t: len(t[0]), reverse=True) + self._fixed_patterns = needles + + self._parametric = [] + for pa in (data.get("parametric_actions") or []): + try: + self._parametric.append((re.compile(pa["regex"], re.IGNORECASE), pa["canonical"])) + except re.error as exc: + log.warning("bad parametric regex %r: %s", pa.get("regex"), exc) + log.info("instruction.json loaded: %d fixed phrases, %d parametric, cooldown=%.1fs", + len(self._fixed_patterns), len(self._parametric), self._cooldown) + + # ── lifecycle ───────────────────────────────────────────────────────────── + + def start(self): + if self._running: + return + self._running = True + self._worker = threading.Thread(target=self._worker_loop, daemon=True, + name="movement-dispatch") + self._worker.start() + log.info("movement dispatcher started") + + def stop(self): + self._running = False + self._abort.set() + try: + self._queue.put_nowait(_SENTINEL) + except queue.Full: + pass + + def status(self) -> dict: + return { + "running": self._running, + "movement_enabled": self._movement_enabled(force=True), + "estopped": self._estop, + "queue_depth": self._queue.qsize(), + "fixed_phrases": len(self._fixed_patterns), + "parametric": len(self._parametric), + } + + # ── E-STOP latch ────────────────────────────────────────────────────────── + + def emergency_stop(self): + """Latch off after a dashboard E-STOP: abort the in-flight command, drain + the queue, and refuse new commands until clear_estop(). Does NOT touch the + movement_enabled file flag (so the Gemini child stays quiet).""" + self._estop = True + self._abort.set() + self._drain() + log.warning("movement dispatch E-STOP latch set") + + def clear_estop(self): + self._estop = False + + def is_estopped(self) -> bool: + return self._estop + + # ── enable gate ─────────────────────────────────────────────────────────── + + def _movement_enabled(self, force: bool = False) -> bool: + now = time.monotonic() + if not force and (now - self._enabled_at) < _STATE_CACHE_TTL: + return self._enabled_cached + try: + from Project.Sanad.vision import recognition_state + self._enabled_cached = bool(recognition_state.read(self._state_path).movement_enabled) + except Exception: + self._enabled_cached = False + self._enabled_at = now + return self._enabled_cached + + # ── transcript hook (called from the supervisor reader thread) ──────────── + + def on_bot_text(self, text: str): + if not text or not self._running or self._estop: + return + if not self._movement_enabled(): + return + cmds = self._match(text) + if not cmds: + return + now = time.monotonic() # monotonic — immune to NTP/wall-clock jumps + for c in cmds: + if c == "stop": + # Safety: preempt anything in flight immediately, then E-STOP. + self._abort.set() + self._drain() + self._enqueue("stop") + self._last_canon = "stop" + self._last_at = now + continue + # cross-turn cooldown: same canonical not re-fired too soon + if c == self._last_canon and (now - self._last_at) < self._cooldown: + continue + self._last_canon = c + self._last_at = now + self._enqueue(c) + + def _enqueue(self, cmd: str): + try: + self._queue.put_nowait(cmd) + except queue.Full: + log.warning("motion queue full — dropping %r", cmd) + + def _drain(self): + try: + while True: + self._queue.get_nowait() + except queue.Empty: + pass + + # ── matcher ─────────────────────────────────────────────────────────────── + + def _clean(self, text: str) -> str: + """Drop clauses that are NOT genuine motion confirmations: bracketed + [STATE] echoes, quoted spans, questions, and negation/hypothetical + sentences. Only the surviving clauses are matched.""" + t = _BRACKET_RE.sub(" ", text) + t = _QUOTE_RE.sub(" ", t) + parts = _SENT_SPLIT_RE.split(t) + kept: List[str] = [] + i = 0 + while i < len(parts): + seg = parts[i].strip() + delim = parts[i + 1] if i + 1 < len(parts) else "" + is_question = ("?" in delim) or ("؟" in delim) + if seg and not is_question and not _NEG_RE.search(seg.lower()): + kept.append(seg) + i += 2 + return " . ".join(kept) + + def _match(self, text: str) -> List[str]: + """Return canonical commands in spoken order. Parametric (with numbers) + claim their spans first so a bare phrase doesn't double-fire.""" + low = self._clean(text).lower() + matches: List[tuple] = [] # (start, canonical) + claimed: List[tuple] = [] # (start, end) spans already taken + + def overlaps(s, e): + return any(s < ce and cs < e for cs, ce in claimed) + + # 1) parametric first. Claim the span even when the quantity is zero so a + # mis-heard "0 steps" suppresses the bare phrase underneath (no surprise + # motion) rather than falling through to a single step. + for rx, template in self._parametric: + for m in rx.finditer(low): + if overlaps(*m.span()): + continue + claimed.append(m.span()) + canonical = self._format(template, m.groups()) + if canonical: + matches.append((m.start(), canonical)) + + # 2) fixed phrases (longest first), skipping claimed spans + for needle, canonical in self._fixed_patterns: + start = 0 + while True: + j = low.find(needle, start) + if j < 0: + break + end = j + len(needle) + if not overlaps(j, end): + matches.append((j, canonical)) + claimed.append((j, end)) + start = end + + matches.sort(key=lambda t: t[0]) + # de-dup consecutive repeats within this single line + out: List[str] = [] + for _, c in matches: + if not out or out[-1] != c: + out.append(c) + return out + + @staticmethod + def _format(template: str, groups) -> str: + out = template + for i, g in enumerate(groups, start=1): + out = out.replace(f"${i}", str(g)) + # reject zero-quantity motions ("walk 0 steps") + nums = re.findall(r"\d+", out) + if nums and all(int(n) == 0 for n in nums): + return "" + return out + + # ── worker ──────────────────────────────────────────────────────────────── + + def _worker_loop(self): + while self._running: + cmd = self._queue.get() + if cmd is _SENTINEL: + return + if cmd != "stop": + self._abort.clear() + if self._estop: + continue # E-STOP latched — drop everything + # force a fresh read — don't let the 0.5s cache execute a command + # after the operator just toggled movement off. + if cmd != "stop" and not self._movement_enabled(force=True): + continue # toggled off while queued — drop + try: + self._execute(cmd) + except Exception: + log.exception("execute %r failed", cmd) + + def _execute(self, canonical: str): + c = canonical.lower().strip() + if c == "stop": + log.info("voice → STOP") + self._loco.estop() + return + + m = re.match(r"walk (forward|backward) (\d+) steps?$", c) + if m: + direction = "forward" if m.group(1) == "forward" else "backward" + n = min(int(m.group(2)), self._max_steps) + log.info("voice → walk %s %d steps", direction, n) + self._repeat_step(direction, n) + return + + m = re.match(r"turn (right|left) (\d+) degrees?$", c) + if m: + direction = "rotate_right" if m.group(1) == "right" else "rotate_left" + deg = min(int(m.group(2)), self._max_degrees) + n = max(1, round(deg / self._deg_per_step)) + log.info("voice → turn %s %d° (~%d steps)", m.group(1), deg, n) + self._repeat_step(direction, n) + return + + direction = _FIXED_STEP.get(c) + if direction: + log.info("voice → %s", c) + self._loco.step(direction) + return + log.debug("no loco mapping for canonical %r", c) + + def _repeat_step(self, direction: str, n: int): + for _ in range(max(1, n)): + if self._abort.is_set() or self._estop or not self._movement_enabled(force=True): + log.info("voice multi-step aborted") + break + self._loco.step(direction) diff --git a/voice/sanad_voice.py b/voice/sanad_voice.py index fe1e3b4..e0524d0 100644 --- a/voice/sanad_voice.py +++ b/voice/sanad_voice.py @@ -133,15 +133,69 @@ _FALLBACK_SYSTEM_PROMPT = ( ) +# N2 Phase 3 — movement confirmation-phrase rules. The parent's +# MovementDispatcher drives the robot off Gemini's OWN spoken phrases, so Gemini +# must say one of these EXACT short phrases (per motion) when it agrees to move. +# Kept in lock-step with data/motions/instruction.json. Always appended; Gemini +# is told at runtime ([MOVEMENT ON/OFF/STATUS]) whether movement is enabled and +# must only confirm motion when it is ON. +_MOVEMENT_PROMPT_RULES = ( + "\n\n--- MOVEMENT (walking) ---\n" + "You can make the robot walk ONLY when you are told movement is ON " + "(you receive a [MOVEMENT ON] or [MOVEMENT STATUS] note). When movement is " + "OFF, never confirm a motion — tell the user to enable movement from the " + "dashboard.\n" + "When movement is ON and the user addresses you by name (Bousandah / بوسنده) AND " + "asks you to move, reply with ONE short confirmation phrase per requested " + "motion, in the SAME language, in the order asked. Use these EXACT shapes — " + "they are what triggers the motion:\n" + " forward : 'Moving forward.' / 'أمشي للأمام.'\n" + " backward : 'Moving back.' / 'أمشي للخلف.'\n" + " turn right : 'Turning right.' / 'أستدير يميناً.'\n" + " turn left : 'Turning left.' / 'أستدير يساراً.'\n" + " slide left : 'Sliding left.' / 'أنزلق لليسار.'\n" + " slide right : 'Sliding right.' / 'أنزلق لليمين.'\n" + " stop : 'Stopping.' / 'أتوقف.'\n" + "With a NUMBER, keep it: 'Walking forward 3 steps.' / 'أمشي للأمام 3 خطوات.' " + "— 'Turning right 90 degrees.' / 'أستدير يميناً 90 درجة.'\n" + "STOP is safety-first: if the user clearly wants the robot to halt " + "(stop/halt/wait/توقف/استنى), confirm 'Stopping.' / 'أتوقف.' immediately, " + "even without your name.\n" + "Never emit bracketed tags like [STATE-DONE] or numbers in parentheses — " + "speak only plain prose. Never include 0 or a negative quantity; if you " + "mis-hear a 0, drop the number and say the bare motion." +) + + def _load_system_prompt() -> str: - """scripts/sanad_script.txt → config default → hardcoded fallback.""" + """scripts/ → config default → hardcoded fallback, with the + movement confirmation-phrase rules appended (N2 Phase 3). + + A missing persona file used to be silent — that hid a config-vs-filename + mismatch (e.g. `persona: "sanad_v2"` while only `sanad_script.txt` + existed) which made the robot fall back to the English default that + introduces itself as "Sanad" instead of using the Arabic persona on + disk. We now WARN so the same trap doesn't bite again.""" + base = None try: text = _PERSONA_FILE.read_text(encoding="utf-8-sig").strip() if text: - return text + base = text except FileNotFoundError: - pass - return _GEMINI_DEFAULTS.get("default_system_prompt", _FALLBACK_SYSTEM_PROMPT) + log.warning( + "Persona file not found at %s — falling back to " + "config.core.gemini_defaults.default_system_prompt. " + "Check `script_files.persona` in config/core_config.json " + "matches an actual file under scripts/.", _PERSONA_FILE, + ) + except (OSError, UnicodeDecodeError) as exc: + # An existing-but-unreadable persona file (bad encoding, permissions, a + # directory) must NOT crash the voice child — fall back to the default. + log.warning("Persona file at %s could not be read (%s) — " + "falling back to default system prompt.", _PERSONA_FILE, exc) + if base is None: + base = _GEMINI_DEFAULTS.get("default_system_prompt", _FALLBACK_SYSTEM_PROMPT) + return base + _MOVEMENT_PROMPT_RULES def _audio_energy(pcm: bytes) -> int: