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.
@@ -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,2 @@
1
+ [huri.modules]
2
+ tag = huri_tag.module:TAG
@@ -0,0 +1,2 @@
1
+ huri-core[server]<0.5,>=0.4
2
+ huri-events<0.2,>=0.1
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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())