huri-module-tag 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_module_tag-0.1.0/PKG-INFO +36 -0
- huri_module_tag-0.1.0/README.md +27 -0
- huri_module_tag-0.1.0/huri_module_tag.egg-info/PKG-INFO +36 -0
- huri_module_tag-0.1.0/huri_module_tag.egg-info/SOURCES.txt +12 -0
- huri_module_tag-0.1.0/huri_module_tag.egg-info/dependency_links.txt +1 -0
- huri_module_tag-0.1.0/huri_module_tag.egg-info/entry_points.txt +2 -0
- huri_module_tag-0.1.0/huri_module_tag.egg-info/requires.txt +2 -0
- huri_module_tag-0.1.0/huri_module_tag.egg-info/top_level.txt +1 -0
- huri_module_tag-0.1.0/huri_tag/__init__.py +0 -0
- huri_module_tag-0.1.0/huri_tag/huri_module.toml +7 -0
- huri_module_tag-0.1.0/huri_tag/module.py +89 -0
- huri_module_tag-0.1.0/pyproject.toml +23 -0
- huri_module_tag-0.1.0/setup.cfg +4 -0
- huri_module_tag-0.1.0/tests/test_tag.py +83 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: huri-module-tag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HuRI module: Text aggregator: stitches overlapping transcripts into one question
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: huri-core[server]<0.5,>=0.4
|
|
8
|
+
Requires-Dist: huri-events<0.2,>=0.1
|
|
9
|
+
|
|
10
|
+
# huri-module-tag
|
|
11
|
+
|
|
12
|
+
Text aggregator: stitches overlapping transcripts into one question.
|
|
13
|
+
|
|
14
|
+
| Module | Topics | What it does |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `tag` | `huri.transcript` → `huri.partial_question` | Stitches overlapping transcript passes into one sentence |
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install -e packages/huri-module-tag # from a huri-modules checkout, next to huri-core and huri-events
|
|
20
|
+
HURI_MODULES=tag ... # enable it on the server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A distribution of
|
|
24
|
+
[huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
|
|
25
|
+
`pyproject.toml` (see RELEASING.md there). It depends on `huri-core` and
|
|
26
|
+
`huri-events`, which are not on PyPI: without a checkout, install them from git
|
|
27
|
+
in the same pip command, so pip never looks them up on an index:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
ref=0.1.0 # a huri-modules release tag
|
|
31
|
+
core=0.4.0 # HURI_CORE_REF in that release's versions.env
|
|
32
|
+
pip install \
|
|
33
|
+
"huri-core[server] @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
|
|
34
|
+
"huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events" \
|
|
35
|
+
"huri-module-tag @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-module-tag"
|
|
36
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# huri-module-tag
|
|
2
|
+
|
|
3
|
+
Text aggregator: stitches overlapping transcripts into one question.
|
|
4
|
+
|
|
5
|
+
| Module | Topics | What it does |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `tag` | `huri.transcript` → `huri.partial_question` | Stitches overlapping transcript passes into one sentence |
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install -e packages/huri-module-tag # from a huri-modules checkout, next to huri-core and huri-events
|
|
11
|
+
HURI_MODULES=tag ... # enable it on the server
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A distribution of
|
|
15
|
+
[huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
|
|
16
|
+
`pyproject.toml` (see RELEASING.md there). It depends on `huri-core` and
|
|
17
|
+
`huri-events`, which are not on PyPI: without a checkout, install them from git
|
|
18
|
+
in the same pip command, so pip never looks them up on an index:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
ref=0.1.0 # a huri-modules release tag
|
|
22
|
+
core=0.4.0 # HURI_CORE_REF in that release's versions.env
|
|
23
|
+
pip install \
|
|
24
|
+
"huri-core[server] @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
|
|
25
|
+
"huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events" \
|
|
26
|
+
"huri-module-tag @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-module-tag"
|
|
27
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: huri-module-tag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HuRI module: Text aggregator: stitches overlapping transcripts into one question
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: huri-core[server]<0.5,>=0.4
|
|
8
|
+
Requires-Dist: huri-events<0.2,>=0.1
|
|
9
|
+
|
|
10
|
+
# huri-module-tag
|
|
11
|
+
|
|
12
|
+
Text aggregator: stitches overlapping transcripts into one question.
|
|
13
|
+
|
|
14
|
+
| Module | Topics | What it does |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `tag` | `huri.transcript` → `huri.partial_question` | Stitches overlapping transcript passes into one sentence |
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install -e packages/huri-module-tag # from a huri-modules checkout, next to huri-core and huri-events
|
|
20
|
+
HURI_MODULES=tag ... # enable it on the server
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A distribution of
|
|
24
|
+
[huri-modules](https://github.com/Lucy-Robotics/huri-modules), versioned in its
|
|
25
|
+
`pyproject.toml` (see RELEASING.md there). It depends on `huri-core` and
|
|
26
|
+
`huri-events`, which are not on PyPI: without a checkout, install them from git
|
|
27
|
+
in the same pip command, so pip never looks them up on an index:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
ref=0.1.0 # a huri-modules release tag
|
|
31
|
+
core=0.4.0 # HURI_CORE_REF in that release's versions.env
|
|
32
|
+
pip install \
|
|
33
|
+
"huri-core[server] @ git+https://github.com/Lucy-Robotics/huri-core.git@$core" \
|
|
34
|
+
"huri-events @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-events" \
|
|
35
|
+
"huri-module-tag @ git+https://github.com/Lucy-Robotics/huri-modules.git@$ref#subdirectory=packages/huri-module-tag"
|
|
36
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
huri_module_tag.egg-info/PKG-INFO
|
|
4
|
+
huri_module_tag.egg-info/SOURCES.txt
|
|
5
|
+
huri_module_tag.egg-info/dependency_links.txt
|
|
6
|
+
huri_module_tag.egg-info/entry_points.txt
|
|
7
|
+
huri_module_tag.egg-info/requires.txt
|
|
8
|
+
huri_module_tag.egg-info/top_level.txt
|
|
9
|
+
huri_tag/__init__.py
|
|
10
|
+
huri_tag/huri_module.toml
|
|
11
|
+
huri_tag/module.py
|
|
12
|
+
tests/test_tag.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
huri_tag
|
|
File without changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
name = "tag"
|
|
2
|
+
summary = "Stitches overlapping transcript passes into one sentence"
|
|
3
|
+
input = { topic = "huri.transcript", event = "huri_events.speech:Transcript" }
|
|
4
|
+
output = { topic = "huri.partial_question", event = "huri_events.question:PartialQuestion" }
|
|
5
|
+
events = ["huri-events>=0.1,<0.2"]
|
|
6
|
+
requires = { gpu = false, actuation = false }
|
|
7
|
+
huri_core = ">=0.4,<0.5"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from huri_events.question import PartialQuestion
|
|
5
|
+
from huri_events.speech import Transcript
|
|
6
|
+
|
|
7
|
+
from huri_core.core.module import Module
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TAG(Module):
|
|
11
|
+
"""TAG Module
|
|
12
|
+
|
|
13
|
+
Aggregate all transcriptions and send when transcript end.
|
|
14
|
+
|
|
15
|
+
Whisper transcribes each sliding window independently, so the start of each
|
|
16
|
+
transcript repeats the end of the previous one (with possibly different
|
|
17
|
+
casing or punctuation). Transcripts are stitched together on that overlap.
|
|
18
|
+
|
|
19
|
+
input: huri.transcript,
|
|
20
|
+
output: huri.partial_question
|
|
21
|
+
|
|
22
|
+
:min_prefix: shortest word fragment (in characters) that may match a longer
|
|
23
|
+
word when merging overlapping transcripts, e.g. "weath" and "weather".
|
|
24
|
+
Guards against 1-2 letter fragments matching unrelated words.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
input_type = "huri.transcript"
|
|
28
|
+
output_type = "huri.partial_question"
|
|
29
|
+
input_event = Transcript
|
|
30
|
+
output_event = PartialQuestion
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
min_prefix: int = 3,
|
|
35
|
+
):
|
|
36
|
+
super().__init__()
|
|
37
|
+
|
|
38
|
+
self.min_prefix = min_prefix
|
|
39
|
+
|
|
40
|
+
self.sentence: str = ""
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _norm(word: str) -> str:
|
|
44
|
+
return re.sub(r"[^\w]", "", word.lower())
|
|
45
|
+
|
|
46
|
+
def _same_word(self, a: str, b: str) -> bool:
|
|
47
|
+
"""Words match after normalisation, or when one is a truncated form of
|
|
48
|
+
the other (a window edge can cut a word in half: "weath" / "weather")."""
|
|
49
|
+
|
|
50
|
+
na, nb = self._norm(a), self._norm(b)
|
|
51
|
+
if na == nb:
|
|
52
|
+
return True
|
|
53
|
+
short, long_ = sorted((na, nb), key=len)
|
|
54
|
+
return len(short) >= self.min_prefix and long_.startswith(short)
|
|
55
|
+
|
|
56
|
+
def _merge(self, current: str, new: str) -> str:
|
|
57
|
+
"""Find the longest run of words where the tail of ``current`` equals the
|
|
58
|
+
head of ``new`` and append only what follows it. When there is no
|
|
59
|
+
overlap, ``new`` is appended whole: text is never dropped from either
|
|
60
|
+
side.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
cur_words = current.split()
|
|
64
|
+
new_words = new.split()
|
|
65
|
+
if not cur_words:
|
|
66
|
+
return new.strip()
|
|
67
|
+
if not new_words:
|
|
68
|
+
return current.strip()
|
|
69
|
+
|
|
70
|
+
for k in range(min(len(cur_words), len(new_words)), 0, -1):
|
|
71
|
+
if all(
|
|
72
|
+
self._same_word(a, b) for a, b in zip(cur_words[-k:], new_words[:k])
|
|
73
|
+
):
|
|
74
|
+
# Keep the newer spelling of the overlap (more audio context).
|
|
75
|
+
return " ".join(cur_words[:-k] + new_words)
|
|
76
|
+
return " ".join(cur_words + new_words)
|
|
77
|
+
|
|
78
|
+
async def process(self, transcript: Transcript) -> Optional[PartialQuestion]:
|
|
79
|
+
text = transcript.text
|
|
80
|
+
|
|
81
|
+
if text != "":
|
|
82
|
+
self.sentence = self._merge(self.sentence, text)
|
|
83
|
+
|
|
84
|
+
if transcript.end and self.sentence != "":
|
|
85
|
+
transcript = Transcript(self.sentence, True)
|
|
86
|
+
self.sentence = ""
|
|
87
|
+
return PartialQuestion(transcript=transcript, emotion=None)
|
|
88
|
+
else:
|
|
89
|
+
return None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64,<82"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "huri-module-tag"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "HuRI module: Text aggregator: stitches overlapping transcripts into one question"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"huri-core[server]>=0.4,<0.5",
|
|
13
|
+
"huri-events>=0.1,<0.2",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.entry-points."huri.modules"]
|
|
17
|
+
tag = "huri_tag.module:TAG"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
include = ["huri_tag*"]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.package-data]
|
|
23
|
+
huri_tag = ["huri_module.toml", "manifests/*/huri_module.toml"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from huri_events.speech import Transcript
|
|
5
|
+
from huri_tag.module import TAG
|
|
6
|
+
|
|
7
|
+
from huri_core.testing import ModuleHarness
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
"current,new,expected",
|
|
12
|
+
[
|
|
13
|
+
(
|
|
14
|
+
"what is the weather",
|
|
15
|
+
"the weather like in Paris today",
|
|
16
|
+
"what is the weather like in Paris today",
|
|
17
|
+
),
|
|
18
|
+
# No overlap must never corrupt/replace the sentence.
|
|
19
|
+
(
|
|
20
|
+
"what is the weather like",
|
|
21
|
+
"in Paris today",
|
|
22
|
+
"what is the weather like in Paris today",
|
|
23
|
+
),
|
|
24
|
+
(
|
|
25
|
+
"What is the weather like.",
|
|
26
|
+
"weather like in Paris today?",
|
|
27
|
+
"What is the weather like in Paris today?",
|
|
28
|
+
),
|
|
29
|
+
(
|
|
30
|
+
"I want to book a table for two",
|
|
31
|
+
"for two people at eight",
|
|
32
|
+
"I want to book a table for two people at eight",
|
|
33
|
+
),
|
|
34
|
+
("it is a very very", "very very long day", "it is a very very long day"),
|
|
35
|
+
# Word cut by the window edge.
|
|
36
|
+
("what is the weath", "weather like", "what is the weather like"),
|
|
37
|
+
# New text fully contained in the tail.
|
|
38
|
+
("hello there my friend", "my friend", "hello there my friend"),
|
|
39
|
+
("", "hello", "hello"),
|
|
40
|
+
("hello", "", "hello"),
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_merge(current, new, expected):
|
|
44
|
+
assert TAG()._merge(current, new) == expected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_tag_emits_on_end_and_resets():
|
|
48
|
+
out = ModuleHarness(TAG).run(
|
|
49
|
+
Transcript("what is the", False),
|
|
50
|
+
Transcript("the weather", False),
|
|
51
|
+
Transcript("weather like", True),
|
|
52
|
+
Transcript("", True),
|
|
53
|
+
Transcript("next question", True),
|
|
54
|
+
)
|
|
55
|
+
assert [q.transcript.text for q in out] == [
|
|
56
|
+
"what is the weather like",
|
|
57
|
+
"next question",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_stitches_the_overlapping_passes_stt_emits():
|
|
62
|
+
out = ModuleHarness(TAG).run(
|
|
63
|
+
Transcript("what is the", False), Transcript("the weather", True)
|
|
64
|
+
)
|
|
65
|
+
assert [q.transcript.text for q in out] == ["what is the weather"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_min_prefix_is_configurable():
|
|
69
|
+
# "th" (from a cut "the") only matches when min_prefix allows 2 chars.
|
|
70
|
+
assert TAG(min_prefix=3)._merge("what is th", "the weather") == (
|
|
71
|
+
"what is th the weather"
|
|
72
|
+
)
|
|
73
|
+
assert TAG(min_prefix=2)._merge("what is th", "the weather") == (
|
|
74
|
+
"what is the weather"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
async def run():
|
|
78
|
+
tag = TAG(min_prefix=2)
|
|
79
|
+
await tag.process(Transcript("what is th", False))
|
|
80
|
+
q = await tag.process(Transcript("the weather", True))
|
|
81
|
+
assert q.transcript.text == "what is the weather"
|
|
82
|
+
|
|
83
|
+
asyncio.run(run())
|