huri-events 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- huri_events-0.1.0/PKG-INFO +38 -0
- huri_events-0.1.0/README.md +29 -0
- huri_events-0.1.0/huri_events/__init__.py +34 -0
- huri_events-0.1.0/huri_events/audio.py +47 -0
- huri_events-0.1.0/huri_events/emotion.py +16 -0
- huri_events-0.1.0/huri_events/motion.py +81 -0
- huri_events-0.1.0/huri_events/question.py +58 -0
- huri_events-0.1.0/huri_events/speech.py +34 -0
- huri_events-0.1.0/huri_events/text.py +15 -0
- huri_events-0.1.0/huri_events.egg-info/PKG-INFO +38 -0
- huri_events-0.1.0/huri_events.egg-info/SOURCES.txt +16 -0
- huri_events-0.1.0/huri_events.egg-info/dependency_links.txt +1 -0
- huri_events-0.1.0/huri_events.egg-info/entry_points.txt +10 -0
- huri_events-0.1.0/huri_events.egg-info/requires.txt +2 -0
- huri_events-0.1.0/huri_events.egg-info/top_level.txt +1 -0
- huri_events-0.1.0/pyproject.toml +31 -0
- huri_events-0.1.0/setup.cfg +4 -0
- huri_events-0.1.0/tests/test_wire.py +112 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: huri-events
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The curated HuRI event vocabulary: the huri.* topics and their wire formats
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: huri-core<0.5,>=0.4
|
|
8
|
+
Requires-Dist: numpy<2,>=1.26
|
|
9
|
+
|
|
10
|
+
# huri-events
|
|
11
|
+
|
|
12
|
+
The curated HuRI event vocabulary: one event class per `huri.*` topic, with its
|
|
13
|
+
wire format, registered through the `huri.events` entry point group. The
|
|
14
|
+
`huri.` namespace is reserved for this package; third-party events use their
|
|
15
|
+
own (`acme.*`), see [Modules as plugins](../../README.md#modules-as-plugins).
|
|
16
|
+
|
|
17
|
+
It is deliberately light (huri-core and numpy): a client installs it next to
|
|
18
|
+
`huri-core[client]` to decode every curated topic.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# from a huri-modules checkout:
|
|
22
|
+
pip install "huri-core[client] @ git+https://github.com/Lucy-Robotics/huri-core.git@$(. ./versions.env && echo "$HURI_CORE_REF")" \
|
|
23
|
+
./packages/huri-events
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A distribution of
|
|
27
|
+
[huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
|
|
28
|
+
`pyproject.toml` (see RELEASING.md there). It depends on `huri-core`, which is
|
|
29
|
+
not on PyPI: without a checkout, install it from git in the same pip command,
|
|
30
|
+
so pip never looks it up on an index:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ref=0.1.0 # a huri-modules release tag
|
|
34
|
+
core=0.4.0 # HURI_CORE_REF in that release's versions.env
|
|
35
|
+
pip install \
|
|
36
|
+
"huri-core @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
|
|
37
|
+
"huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events"
|
|
38
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# huri-events
|
|
2
|
+
|
|
3
|
+
The curated HuRI event vocabulary: one event class per `huri.*` topic, with its
|
|
4
|
+
wire format, registered through the `huri.events` entry point group. The
|
|
5
|
+
`huri.` namespace is reserved for this package; third-party events use their
|
|
6
|
+
own (`acme.*`), see [Modules as plugins](../../README.md#modules-as-plugins).
|
|
7
|
+
|
|
8
|
+
It is deliberately light (huri-core and numpy): a client installs it next to
|
|
9
|
+
`huri-core[client]` to decode every curated topic.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# from a huri-modules checkout:
|
|
13
|
+
pip install "huri-core[client] @ git+https://github.com/Lucy-Robotics/huri-core.git@$(. ./versions.env && echo "$HURI_CORE_REF")" \
|
|
14
|
+
./packages/huri-events
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
A distribution of
|
|
18
|
+
[huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
|
|
19
|
+
`pyproject.toml` (see RELEASING.md there). It depends on `huri-core`, which is
|
|
20
|
+
not on PyPI: without a checkout, install it from git in the same pip command,
|
|
21
|
+
so pip never looks it up on an index:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
ref=0.1.0 # a huri-modules release tag
|
|
25
|
+
core=0.4.0 # HURI_CORE_REF in that release's versions.env
|
|
26
|
+
pip install \
|
|
27
|
+
"huri-core @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
|
|
28
|
+
"huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events"
|
|
29
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The curated HuRI events, one class per `huri.*` topic.
|
|
2
|
+
|
|
3
|
+
| topic | class | wire |
|
|
4
|
+
|-------------------------|-------------------|--------|
|
|
5
|
+
| huri.audio.in | BytesEvent (core) | binary |
|
|
6
|
+
| huri.voice | Voice | json |
|
|
7
|
+
| huri.transcript | Transcript | json |
|
|
8
|
+
| huri.emotion | Emotion | - |
|
|
9
|
+
| huri.partial_question | PartialQuestion | json |
|
|
10
|
+
| huri.question | RAGQuestion | json |
|
|
11
|
+
| huri.token | Token | json |
|
|
12
|
+
| huri.audio.out | Audio | binary |
|
|
13
|
+
| huri.motion | Motion | binary |
|
|
14
|
+
|
|
15
|
+
`Emotion` and `Voice` only travel between modules inside the server.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from huri_events.audio import Audio
|
|
19
|
+
from huri_events.emotion import Emotion
|
|
20
|
+
from huri_events.motion import Motion
|
|
21
|
+
from huri_events.question import PartialQuestion, RAGQuestion
|
|
22
|
+
from huri_events.speech import Transcript, Voice
|
|
23
|
+
from huri_events.text import Token
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Audio",
|
|
27
|
+
"Emotion",
|
|
28
|
+
"Motion",
|
|
29
|
+
"PartialQuestion",
|
|
30
|
+
"RAGQuestion",
|
|
31
|
+
"Token",
|
|
32
|
+
"Transcript",
|
|
33
|
+
"Voice",
|
|
34
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import struct
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from huri_core.core.events import EventData
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("ray.serve")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Audio(EventData[bytes]):
|
|
14
|
+
"""Synthesized speech: float32 mono samples.
|
|
15
|
+
|
|
16
|
+
Wire (binary): ``>IBd`` header — sample_rate (u32), end (u8), pts (f64,
|
|
17
|
+
seconds from utterance start) — then the float32 samples.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
data: np.ndarray
|
|
21
|
+
sample_rate: int
|
|
22
|
+
end: bool = False
|
|
23
|
+
pts: float = 0.0 # presentation timestamp in seconds from utterance start
|
|
24
|
+
|
|
25
|
+
def to_wire(self) -> bytes:
|
|
26
|
+
logger.info(
|
|
27
|
+
"Audio samples=%d sr=%d end=%s pts=%.3fs",
|
|
28
|
+
self.data.shape[0],
|
|
29
|
+
self.sample_rate,
|
|
30
|
+
self.end,
|
|
31
|
+
self.pts,
|
|
32
|
+
)
|
|
33
|
+
header = struct.pack(">IBd", self.sample_rate, int(self.end), self.pts)
|
|
34
|
+
return header + bytes(self.data.tobytes())
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_wire(cls, data: bytes) -> "Audio":
|
|
38
|
+
sample_rate, end, pts = struct.unpack(">IBd", data[:13])
|
|
39
|
+
samples = np.frombuffer(data[13:], dtype=np.float32)
|
|
40
|
+
|
|
41
|
+
return cls(sample_rate=sample_rate, end=end, pts=pts, data=samples)
|
|
42
|
+
|
|
43
|
+
def summarize(self) -> str:
|
|
44
|
+
"""Short repr that avoids dumping full numpy arrays into the log."""
|
|
45
|
+
|
|
46
|
+
cls = type(self).__name__
|
|
47
|
+
return f"{cls}(shape={self.data.shape}, dtype={self.data.dtype})"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from huri_core.core.events import EventData
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Emotion(EventData):
|
|
9
|
+
"""Speech emotion: best `label`, its `confidence`, all `scores`; `end`
|
|
10
|
+
marks the utterance's final estimate. Travels between modules only
|
|
11
|
+
(EMO -> EAG), it has no wire format."""
|
|
12
|
+
|
|
13
|
+
label: str
|
|
14
|
+
confidence: float
|
|
15
|
+
scores: Dict[str, float]
|
|
16
|
+
end: bool
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import struct
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from huri_core.core.events import EventData
|
|
8
|
+
|
|
9
|
+
_EMAGE_FPS = 30
|
|
10
|
+
|
|
11
|
+
# Layout of the per-frame vectors, shared by to_wire/from_wire.
|
|
12
|
+
_POSE_DIM = 165 # 55 SMPL-X joints x 3 axis-angle
|
|
13
|
+
_EXPR_DIM = 100 # facial expression coefficients
|
|
14
|
+
_TRANS_DIM = 3 # global root translation
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("ray.serve")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class Motion(EventData[bytes]):
|
|
21
|
+
"""SMPL-X gesture frames paired with the speech (`pts` matches Audio.pts).
|
|
22
|
+
|
|
23
|
+
Wire (binary): ``>dII`` header — pts (f64), fps (u32), frame count (u32) —
|
|
24
|
+
then float32 poses (t x 165), expressions (t x 100) and trans (t x 3).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
poses: np.ndarray # (t, 165) SMPL-X axis-angle, 55 joints × 3
|
|
28
|
+
expressions: np.ndarray # (t, 100) facial expression coefficients
|
|
29
|
+
trans: np.ndarray # (t, 3) global root translation
|
|
30
|
+
fps: int = _EMAGE_FPS
|
|
31
|
+
pts: float = 0.0 # presentation timestamp in seconds, paired with Audio.pts
|
|
32
|
+
|
|
33
|
+
def to_wire(self) -> bytes:
|
|
34
|
+
n_frames = self.poses.shape[0]
|
|
35
|
+
logger.info(
|
|
36
|
+
"Motion frames=%d fps=%d pts=%.3fs",
|
|
37
|
+
n_frames,
|
|
38
|
+
self.fps,
|
|
39
|
+
self.pts,
|
|
40
|
+
)
|
|
41
|
+
header = struct.pack(">dII", self.pts, self.fps, n_frames)
|
|
42
|
+
body: bytes = (
|
|
43
|
+
self.poses.astype(np.float32).tobytes()
|
|
44
|
+
+ self.expressions.astype(np.float32).tobytes()
|
|
45
|
+
+ self.trans.astype(np.float32).tobytes()
|
|
46
|
+
)
|
|
47
|
+
return header + body
|
|
48
|
+
|
|
49
|
+
def summarize(self) -> str:
|
|
50
|
+
cls = type(self).__name__
|
|
51
|
+
return (
|
|
52
|
+
f"{cls}(frames={self.poses.shape[0]}, "
|
|
53
|
+
f"fps={self.fps}, pts={self.pts:.3f}s)"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_wire(cls, data: bytes) -> "Motion":
|
|
58
|
+
pts, fps, n_frames = struct.unpack(">dII", data[:16])
|
|
59
|
+
|
|
60
|
+
body = np.frombuffer(data[16:], dtype=np.float32)
|
|
61
|
+
expected = n_frames * (_POSE_DIM + _EXPR_DIM + _TRANS_DIM)
|
|
62
|
+
if body.size < expected:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"truncated motion frame: {body.size} floats, expected {expected} "
|
|
65
|
+
f"for {n_frames} frames"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
offset = 0
|
|
69
|
+
out = []
|
|
70
|
+
for dim in (_POSE_DIM, _EXPR_DIM, _TRANS_DIM):
|
|
71
|
+
out.append(body[offset : offset + n_frames * dim].reshape(n_frames, dim))
|
|
72
|
+
offset += n_frames * dim
|
|
73
|
+
poses, expressions, trans = out
|
|
74
|
+
|
|
75
|
+
return cls(
|
|
76
|
+
poses=poses,
|
|
77
|
+
expressions=expressions,
|
|
78
|
+
trans=trans,
|
|
79
|
+
fps=fps,
|
|
80
|
+
pts=pts,
|
|
81
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Mapping, Optional
|
|
3
|
+
|
|
4
|
+
from huri_events.emotion import Emotion
|
|
5
|
+
from huri_events.speech import Transcript
|
|
6
|
+
|
|
7
|
+
from huri_core.core.events import JsonEvent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PartialQuestion(JsonEvent):
|
|
12
|
+
"""Partial question used to aggregate a sentence to an emotion: either
|
|
13
|
+
part may be missing (TAG sends the transcript, EAG the emotion)."""
|
|
14
|
+
|
|
15
|
+
transcript: Optional[Transcript]
|
|
16
|
+
emotion: Optional[Emotion]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RAGQuestion(JsonEvent):
|
|
21
|
+
"""Fully aggregated question to send to the RAG.
|
|
22
|
+
|
|
23
|
+
Wire: JSON ``{"transcript": {"text", "end"}, "emotion": {...} | null}``,
|
|
24
|
+
or the shorthand ``{"text": str}`` a client sends for a typed question.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
transcript: Transcript
|
|
28
|
+
emotion: Optional[Emotion] = None
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_wire(cls, data: Mapping[str, Any]) -> "RAGQuestion":
|
|
32
|
+
"""Accept a typed question straight off the wire.
|
|
33
|
+
|
|
34
|
+
In-pipeline, QAG builds this from a Transcript + Emotion. External clients
|
|
35
|
+
(the website backend, the CLI text sender) inject a typed question as a
|
|
36
|
+
bare ``{"text": ...}`` — no live emotion — which we wrap into a final
|
|
37
|
+
Transcript. The nested branch also hydrates the dataclasses (JSON gives
|
|
38
|
+
plain dicts) so a fully-shaped payload round-trips too.
|
|
39
|
+
"""
|
|
40
|
+
if "text" in data:
|
|
41
|
+
return cls(Transcript(text=data["text"], end=True), None)
|
|
42
|
+
|
|
43
|
+
# Thomas: it's nasty, but ragquestion is a subclass and doesn't work well with
|
|
44
|
+
# ray for my end, i get RAGQuestion.__init__() got an unexpected keyword
|
|
45
|
+
# argument 'text' if there is a agnostic way, we can fix this, but until now
|
|
46
|
+
# it's like this
|
|
47
|
+
|
|
48
|
+
transcript = data.get("transcript")
|
|
49
|
+
if isinstance(transcript, Mapping):
|
|
50
|
+
transcript = Transcript(**transcript)
|
|
51
|
+
if not isinstance(transcript, Transcript):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"RAGQuestion.from_wire needs a 'text' or 'transcript' field"
|
|
54
|
+
)
|
|
55
|
+
emotion = data.get("emotion")
|
|
56
|
+
if isinstance(emotion, Mapping):
|
|
57
|
+
emotion = Emotion(**emotion)
|
|
58
|
+
return cls(transcript, emotion)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from huri_core.core.events import JsonEvent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Transcript(JsonEvent):
|
|
11
|
+
"""Text recognized from speech; `end` closes the utterance.
|
|
12
|
+
|
|
13
|
+
Wire: JSON ``{"text": str, "end": bool}``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
text: str
|
|
17
|
+
end: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Voice(JsonEvent):
|
|
22
|
+
"""A frame of detected speech (float32 samples), or `data=None` to mark the
|
|
23
|
+
end of the utterance. Travels between modules only (VAD -> STT, EMO)."""
|
|
24
|
+
|
|
25
|
+
data: Optional[np.ndarray]
|
|
26
|
+
|
|
27
|
+
def summarize(self) -> str:
|
|
28
|
+
"""Short repr that avoids dumping full numpy arrays into the log."""
|
|
29
|
+
|
|
30
|
+
if self.data is not None:
|
|
31
|
+
cls = type(self).__name__
|
|
32
|
+
return f"{cls}(shape={self.data.shape}, dtype={self.data.dtype})"
|
|
33
|
+
else:
|
|
34
|
+
return super().summarize()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from huri_core.core.events import JsonEvent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Token(JsonEvent):
|
|
8
|
+
"""A delta of the assistant's streamed reply; `end` marks the end of the
|
|
9
|
+
utterance (with empty text).
|
|
10
|
+
|
|
11
|
+
Wire: JSON ``{"text": str, "end": bool}``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
text: str
|
|
15
|
+
end: bool
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: huri-events
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The curated HuRI event vocabulary: the huri.* topics and their wire formats
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: huri-core<0.5,>=0.4
|
|
8
|
+
Requires-Dist: numpy<2,>=1.26
|
|
9
|
+
|
|
10
|
+
# huri-events
|
|
11
|
+
|
|
12
|
+
The curated HuRI event vocabulary: one event class per `huri.*` topic, with its
|
|
13
|
+
wire format, registered through the `huri.events` entry point group. The
|
|
14
|
+
`huri.` namespace is reserved for this package; third-party events use their
|
|
15
|
+
own (`acme.*`), see [Modules as plugins](../../README.md#modules-as-plugins).
|
|
16
|
+
|
|
17
|
+
It is deliberately light (huri-core and numpy): a client installs it next to
|
|
18
|
+
`huri-core[client]` to decode every curated topic.
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
# from a huri-modules checkout:
|
|
22
|
+
pip install "huri-core[client] @ git+https://github.com/Lucy-Robotics/huri-core.git@$(. ./versions.env && echo "$HURI_CORE_REF")" \
|
|
23
|
+
./packages/huri-events
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A distribution of
|
|
27
|
+
[huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
|
|
28
|
+
`pyproject.toml` (see RELEASING.md there). It depends on `huri-core`, which is
|
|
29
|
+
not on PyPI: without a checkout, install it from git in the same pip command,
|
|
30
|
+
so pip never looks it up on an index:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
ref=0.1.0 # a huri-modules release tag
|
|
34
|
+
core=0.4.0 # HURI_CORE_REF in that release's versions.env
|
|
35
|
+
pip install \
|
|
36
|
+
"huri-core @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
|
|
37
|
+
"huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events"
|
|
38
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
huri_events/__init__.py
|
|
4
|
+
huri_events/audio.py
|
|
5
|
+
huri_events/emotion.py
|
|
6
|
+
huri_events/motion.py
|
|
7
|
+
huri_events/question.py
|
|
8
|
+
huri_events/speech.py
|
|
9
|
+
huri_events/text.py
|
|
10
|
+
huri_events.egg-info/PKG-INFO
|
|
11
|
+
huri_events.egg-info/SOURCES.txt
|
|
12
|
+
huri_events.egg-info/dependency_links.txt
|
|
13
|
+
huri_events.egg-info/entry_points.txt
|
|
14
|
+
huri_events.egg-info/requires.txt
|
|
15
|
+
huri_events.egg-info/top_level.txt
|
|
16
|
+
tests/test_wire.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
[huri.events]
|
|
2
|
+
huri.audio.in = huri_core.core.events:BytesEvent
|
|
3
|
+
huri.audio.out = huri_events.audio:Audio
|
|
4
|
+
huri.emotion = huri_events.emotion:Emotion
|
|
5
|
+
huri.motion = huri_events.motion:Motion
|
|
6
|
+
huri.partial_question = huri_events.question:PartialQuestion
|
|
7
|
+
huri.question = huri_events.question:RAGQuestion
|
|
8
|
+
huri.token = huri_events.text:Token
|
|
9
|
+
huri.transcript = huri_events.speech:Transcript
|
|
10
|
+
huri.voice = huri_events.speech:Voice
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
huri_events
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64,<82"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "huri-events"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The curated HuRI event vocabulary: the huri.* topics and their wire formats"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
# Light on purpose: a client decodes every curated topic with this and
|
|
12
|
+
# huri-core[client], no torch, no ray.
|
|
13
|
+
dependencies = [
|
|
14
|
+
"huri-core>=0.4,<0.5",
|
|
15
|
+
"numpy>=1.26,<2",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# Topic -> event class. The huri.* namespace is reserved for this package.
|
|
19
|
+
[project.entry-points."huri.events"]
|
|
20
|
+
"huri.audio.in" = "huri_core.core.events:BytesEvent" # inbound mic frames (raw int16 PCM)
|
|
21
|
+
"huri.audio.out" = "huri_events.audio:Audio"
|
|
22
|
+
"huri.voice" = "huri_events.speech:Voice"
|
|
23
|
+
"huri.transcript" = "huri_events.speech:Transcript"
|
|
24
|
+
"huri.emotion" = "huri_events.emotion:Emotion"
|
|
25
|
+
"huri.partial_question" = "huri_events.question:PartialQuestion"
|
|
26
|
+
"huri.question" = "huri_events.question:RAGQuestion"
|
|
27
|
+
"huri.token" = "huri_events.text:Token"
|
|
28
|
+
"huri.motion" = "huri_events.motion:Motion"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
include = ["huri_events*"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Every curated event survives its wire format, and huri-events owns huri.*."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
from huri_events import (
|
|
8
|
+
Audio,
|
|
9
|
+
Emotion,
|
|
10
|
+
Motion,
|
|
11
|
+
PartialQuestion,
|
|
12
|
+
RAGQuestion,
|
|
13
|
+
Token,
|
|
14
|
+
Transcript,
|
|
15
|
+
Voice,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from huri_core.core.events import BytesEvent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def roundtrip(event):
|
|
22
|
+
return type(event).from_wire(event.to_wire())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.mark.parametrize(
|
|
26
|
+
"event",
|
|
27
|
+
[
|
|
28
|
+
Transcript("hello robot", True),
|
|
29
|
+
Token("Hel", False),
|
|
30
|
+
Token("", True),
|
|
31
|
+
BytesEvent(b"\x00\x01\x02\x03"),
|
|
32
|
+
],
|
|
33
|
+
ids=lambda e: type(e).__name__,
|
|
34
|
+
)
|
|
35
|
+
def test_simple_events_roundtrip(event):
|
|
36
|
+
assert roundtrip(event) == event
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_question_roundtrips_with_and_without_emotion():
|
|
40
|
+
emotion = Emotion("neu", 0.9, {"neu": 0.9, "ang": 0.1}, True)
|
|
41
|
+
for question in (
|
|
42
|
+
RAGQuestion(Transcript("hi", True), None),
|
|
43
|
+
RAGQuestion(Transcript("hi", True), emotion),
|
|
44
|
+
):
|
|
45
|
+
assert roundtrip(question) == question
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_typed_question_shorthand():
|
|
49
|
+
assert RAGQuestion.from_wire({"text": "hi"}) == RAGQuestion(Transcript("hi", True))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_audio_roundtrip_keeps_samples_and_timing():
|
|
53
|
+
audio = Audio(np.linspace(-1, 1, 480, dtype=np.float32), 24000, True, 1.25)
|
|
54
|
+
back = roundtrip(audio)
|
|
55
|
+
assert (back.sample_rate, bool(back.end), back.pts) == (24000, True, 1.25)
|
|
56
|
+
np.testing.assert_array_equal(back.data, audio.data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_motion_roundtrip_and_truncation():
|
|
60
|
+
frames = 3
|
|
61
|
+
motion = Motion(
|
|
62
|
+
poses=np.arange(frames * 165, dtype=np.float32).reshape(frames, 165),
|
|
63
|
+
expressions=np.ones((frames, 100), dtype=np.float32),
|
|
64
|
+
trans=np.zeros((frames, 3), dtype=np.float32),
|
|
65
|
+
pts=0.5,
|
|
66
|
+
)
|
|
67
|
+
back = roundtrip(motion)
|
|
68
|
+
np.testing.assert_array_equal(back.poses, motion.poses)
|
|
69
|
+
assert (back.fps, back.pts) == (motion.fps, 0.5)
|
|
70
|
+
with pytest.raises(ValueError, match="truncated"):
|
|
71
|
+
Motion.from_wire(motion.to_wire()[:-4])
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_internal_events_are_constructible():
|
|
75
|
+
assert Voice(None).data is None
|
|
76
|
+
assert PartialQuestion(Transcript("a", True), None).emotion is None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_huri_events_registers_every_curated_topic():
|
|
80
|
+
eps = {ep.name: ep for ep in entry_points(group="huri.events")}
|
|
81
|
+
curated = {name for name in eps if name.startswith("huri.")}
|
|
82
|
+
assert curated == {
|
|
83
|
+
"huri.audio.in",
|
|
84
|
+
"huri.audio.out",
|
|
85
|
+
"huri.voice",
|
|
86
|
+
"huri.transcript",
|
|
87
|
+
"huri.emotion",
|
|
88
|
+
"huri.partial_question",
|
|
89
|
+
"huri.question",
|
|
90
|
+
"huri.token",
|
|
91
|
+
"huri.motion",
|
|
92
|
+
}
|
|
93
|
+
dists = {getattr(eps[name].dist, "name", None) for name in curated}
|
|
94
|
+
assert dists == {"huri-events"}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_motion_survives_the_wire():
|
|
98
|
+
"""from_wire used to drop the body, yielding a zero-frame Motion."""
|
|
99
|
+
frames = 4
|
|
100
|
+
original = Motion(
|
|
101
|
+
poses=np.arange(frames * 165, dtype=np.float32).reshape(frames, 165),
|
|
102
|
+
expressions=np.arange(frames * 100, dtype=np.float32).reshape(frames, 100),
|
|
103
|
+
trans=np.arange(frames * 3, dtype=np.float32).reshape(frames, 3),
|
|
104
|
+
pts=1.5,
|
|
105
|
+
)
|
|
106
|
+
decoded = roundtrip(original)
|
|
107
|
+
|
|
108
|
+
assert decoded.pts == pytest.approx(original.pts)
|
|
109
|
+
assert decoded.fps == original.fps
|
|
110
|
+
np.testing.assert_array_equal(decoded.poses, original.poses)
|
|
111
|
+
np.testing.assert_array_equal(decoded.expressions, original.expressions)
|
|
112
|
+
np.testing.assert_array_equal(decoded.trans, original.trans)
|