feat(pose): emit /pose/mouth from bridge

Add PoseSoundBridge.send_mouth(pid, mouth_open) mirroring send_kin
pattern; call it in ActionHeadPublisher._tick after send_kin using
the already-in-scope `mouth` scalar; add route to module docstring.
3 unit tests (address, VJ mirror, boundary values) all green.
This commit is contained in:
L'électron rare
2026-06-29 15:07:54 +02:00
parent 244d5dd944
commit 3ecef87f4b
3 changed files with 59 additions and 0 deletions
+1
View File
@@ -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:
+10
View File
@@ -10,6 +10,7 @@ Routes emises :
/pose/head <pid> <x> <y> <c> position du nez (visage)
/pose/sho_span <pid> <dx> ecart epaules (estime distance camera)
/pose/limb_span <pid> <span> envergure brassse (poignet a poignet)
/pose/mouth <pid> <open> mouth openness 0..1
/face/count <n> nombre de visages detectes
/face/kp <pid> <idx> <x> <y> <z> <c> 68-pt subset (dlib mapping)
/hand/count <n_left> <n_right> 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:
@@ -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