huri-events 0.1.0.dev0__py3-none-any.whl
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/__init__.py +34 -0
- huri_events/audio.py +47 -0
- huri_events/emotion.py +16 -0
- huri_events/motion.py +81 -0
- huri_events/question.py +58 -0
- huri_events/speech.py +34 -0
- huri_events/text.py +15 -0
- huri_events-0.1.0.dev0.dist-info/METADATA +38 -0
- huri_events-0.1.0.dev0.dist-info/RECORD +12 -0
- huri_events-0.1.0.dev0.dist-info/WHEEL +5 -0
- huri_events-0.1.0.dev0.dist-info/entry_points.txt +10 -0
- huri_events-0.1.0.dev0.dist-info/top_level.txt +1 -0
huri_events/__init__.py
ADDED
|
@@ -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
|
+
]
|
huri_events/audio.py
ADDED
|
@@ -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})"
|
huri_events/emotion.py
ADDED
|
@@ -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
|
huri_events/motion.py
ADDED
|
@@ -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
|
+
)
|
huri_events/question.py
ADDED
|
@@ -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)
|
huri_events/speech.py
ADDED
|
@@ -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()
|
huri_events/text.py
ADDED
|
@@ -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.dev0
|
|
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.0.dev0
|
|
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,12 @@
|
|
|
1
|
+
huri_events/__init__.py,sha256=vToUbd1mI8IeBDbu9DzYjPSU8koDLhLVXzsYzBJ6Flk,1164
|
|
2
|
+
huri_events/audio.py,sha256=i-kUIS1TCgZ3dSLSpzJygNb9qke_2GeRDl3Npu8-860,1401
|
|
3
|
+
huri_events/emotion.py,sha256=AP7XutAGA6VxWfzQfLK5EcJZs1bCBs3LH3cR7GAhIqc,413
|
|
4
|
+
huri_events/motion.py,sha256=_8iKWL1GYWPU3CkdclSIBHw7WAzOrsQCmPnFGZBEyWo,2577
|
|
5
|
+
huri_events/question.py,sha256=hP3io6nHF5seqnKK3aEtrH1UVcQV5DWrJokvFjm0YdE,2182
|
|
6
|
+
huri_events/speech.py,sha256=Xn24ZtGw7GErot_bw48yOVas0g5VIciXTVkjzN-bwjY,858
|
|
7
|
+
huri_events/text.py,sha256=gHQe-f7Fa6zNtiAjTQ2ERLX4oqcKLHtLsiBYHW6bqqc,311
|
|
8
|
+
huri_events-0.1.0.dev0.dist-info/METADATA,sha256=y5yKwmscmruCqYtrKGoyVBU70jfOcflG6xjS5ghB64M,1539
|
|
9
|
+
huri_events-0.1.0.dev0.dist-info/WHEEL,sha256=YLJXdYXQ2FQ0Uqn2J-6iEIC-3iOey8lH3xCtvFLkd8Q,91
|
|
10
|
+
huri_events-0.1.0.dev0.dist-info/entry_points.txt,sha256=clc3USkQxq78TyRpwCwQO_gmIBj9Vg2v4luzI3Nwmp0,419
|
|
11
|
+
huri_events-0.1.0.dev0.dist-info/top_level.txt,sha256=EmPGEYiIh5iXk-WuHIpq-njvZVH6vEExz6n_6cXTE3o,12
|
|
12
|
+
huri_events-0.1.0.dev0.dist-info/RECORD,,
|
|
@@ -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
|