diff --git a/data_only_viz/action_head_pub.py b/data_only_viz/action_head_pub.py index 3d0bf15..5a5941c 100644 --- a/data_only_viz/action_head_pub.py +++ b/data_only_viz/action_head_pub.py @@ -139,6 +139,7 @@ class ActionHeadPublisher(threading.Thread): idx = LABELS.index(label) self.bridge.send_action(pid, idx, probs, t_now, force=True) self.bridge.send_kin(pid, kin, t_now, force=True) + self.bridge.send_mouth(pid, mouth) if pid not in self._last_pids: self.bridge.send_enter(pid=pid) for gone in self._last_pids - current_pids: diff --git a/data_only_viz/pose_bridge.py b/data_only_viz/pose_bridge.py index c0c46b8..c2173a3 100644 --- a/data_only_viz/pose_bridge.py +++ b/data_only_viz/pose_bridge.py @@ -10,6 +10,7 @@ Routes emises : /pose/head position du nez (visage) /pose/sho_span ecart epaules (estime distance camera) /pose/limb_span envergure brassse (poignet a poignet) + /pose/mouth mouth openness 0..1 /face/count nombre de visages detectes /face/kp 68-pt subset (dlib mapping) /hand/count nombre de mains gauche / droite @@ -350,6 +351,15 @@ class PoseSoundBridge: self._client.send_message("/pose/kin", kin_args) self._vj("/pose/kin", kin_args) + def send_mouth(self, pid: int, mouth_open: float) -> None: + """Send mouth openness via /pose/mouth OSC route. + + Sends: [pid (int), mouth_open (float)] — mouth_open is 0..1. + """ + args = [int(pid), float(mouth_open)] + self._client.send_message("/pose/mouth", args) + self._vj("/pose/mouth", args) + @staticmethod def _hand_slot(h): if not h: diff --git a/data_only_viz/tests/test_pose_bridge_mouth.py b/data_only_viz/tests/test_pose_bridge_mouth.py new file mode 100644 index 0000000..eddb460 --- /dev/null +++ b/data_only_viz/tests/test_pose_bridge_mouth.py @@ -0,0 +1,48 @@ +"""Tests for /pose/mouth OSC route.""" +from __future__ import annotations + +from unittest.mock import MagicMock + + +def test_send_mouth_address_and_args() -> None: + from data_only_viz.pose_bridge import PoseSoundBridge + b = PoseSoundBridge() + b._client = MagicMock() + b._vdmx = None + b.send_mouth(pid=5, mouth_open=0.42) + b._client.send_message.assert_called_once() + address, args = b._client.send_message.call_args.args + assert address == "/pose/mouth" + assert args[0] == 5 + assert isinstance(args[0], int) + assert abs(args[1] - 0.42) < 1e-6 + assert isinstance(args[1], float) + + +def test_send_mouth_vj_mirror() -> None: + """VJ mirror receives the same address and args.""" + from data_only_viz.pose_bridge import PoseSoundBridge + b = PoseSoundBridge() + b._client = MagicMock() + b._vdmx = MagicMock() + b.send_mouth(pid=1, mouth_open=0.75) + vj_call = b._vdmx.send_message.call_args.args + assert vj_call[0] == "/pose/mouth" + assert vj_call[1][0] == 1 + assert abs(vj_call[1][1] - 0.75) < 1e-6 + + +def test_send_mouth_zero_and_one_clamp() -> None: + """Boundary values: 0.0 and 1.0 are forwarded without change.""" + from data_only_viz.pose_bridge import PoseSoundBridge + b = PoseSoundBridge() + b._client = MagicMock() + b._vdmx = None + b.send_mouth(pid=0, mouth_open=0.0) + _, args0 = b._client.send_message.call_args.args + assert args0[1] == 0.0 + + b._client.reset_mock() + b.send_mouth(pid=0, mouth_open=1.0) + _, args1 = b._client.send_message.call_args.args + assert args1[1] == 1.0