Spaces:
Runtime error
Runtime error
GVillegasPM commited on
Commit ·
4703065
1
Parent(s): dd602d1
feat: reply protocol parser for voice segments and control tags
Browse files- imagine_buddy/protocol.py +42 -0
- tests/test_protocol.py +48 -0
imagine_buddy/protocol.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Parser for the brain's reply protocol: [voice:x] segments + control tags."""
|
| 2 |
+
import re
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
|
| 5 |
+
VOICE_TAG = re.compile(r"\[voice:([^\]]+)\]")
|
| 6 |
+
CONTROL_TAG = re.compile(r"\[(cast:add|beat:advance|fateroll)((?:[^\]])*)\]")
|
| 7 |
+
ATTR = re.compile(r'(\w+)=("[^"]*"|\S+)')
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True)
|
| 11 |
+
class ReplySegment:
|
| 12 |
+
voice: str
|
| 13 |
+
text: str
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class ParsedReply:
|
| 18 |
+
segments: tuple[ReplySegment, ...]
|
| 19 |
+
controls: tuple[dict, ...]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def parse_reply(raw: str) -> ParsedReply:
|
| 23 |
+
controls = []
|
| 24 |
+
|
| 25 |
+
def collect(match):
|
| 26 |
+
attrs = {k: v.strip('"').strip() for k, v in ATTR.findall(match.group(2) or "")}
|
| 27 |
+
controls.append({"type": match.group(1), **attrs})
|
| 28 |
+
return ""
|
| 29 |
+
|
| 30 |
+
text = CONTROL_TAG.sub(collect, raw)
|
| 31 |
+
|
| 32 |
+
segments = []
|
| 33 |
+
parts = VOICE_TAG.split(text) # [pre, voice1, text1, voice2, text2, ...]
|
| 34 |
+
pre = parts[0].strip()
|
| 35 |
+
if pre:
|
| 36 |
+
segments.append(ReplySegment("narrator", pre))
|
| 37 |
+
for i in range(1, len(parts) - 1, 2):
|
| 38 |
+
voice = parts[i].strip()
|
| 39 |
+
seg_text = parts[i + 1].strip()
|
| 40 |
+
if seg_text:
|
| 41 |
+
segments.append(ReplySegment(voice, seg_text))
|
| 42 |
+
return ParsedReply(tuple(segments), tuple(controls))
|
tests/test_protocol.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from imagine_buddy.protocol import ReplySegment, parse_reply
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_plain_text_becomes_narrator_segment():
|
| 5 |
+
p = parse_reply("The wood holds its breath.")
|
| 6 |
+
assert p.segments == (ReplySegment("narrator", "The wood holds its breath."),)
|
| 7 |
+
assert p.controls == ()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def test_multiple_voice_segments():
|
| 11 |
+
p = parse_reply('[voice:narrator] A bridge appears. [voice:Rexy] "Who goes there?"')
|
| 12 |
+
assert p.segments == (
|
| 13 |
+
ReplySegment("narrator", "A bridge appears."),
|
| 14 |
+
ReplySegment("Rexy", '"Who goes there?"'),
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_control_tags_extracted_and_stripped():
|
| 19 |
+
raw = ('[voice:narrator] A new friend! '
|
| 20 |
+
'[cast:add name=Rexy role="bridge guardian" temperament=gruff] '
|
| 21 |
+
'[beat:advance][fateroll]')
|
| 22 |
+
p = parse_reply(raw)
|
| 23 |
+
assert p.segments == (ReplySegment("narrator", "A new friend!"),)
|
| 24 |
+
assert {"type": "cast:add", "name": "Rexy", "role": "bridge guardian",
|
| 25 |
+
"temperament": "gruff"} in p.controls
|
| 26 |
+
assert {"type": "beat:advance"} in p.controls
|
| 27 |
+
assert {"type": "fateroll"} in p.controls
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_text_before_first_voice_tag_is_narrator():
|
| 31 |
+
p = parse_reply('Once upon a time. [voice:Knight] Onward!')
|
| 32 |
+
assert p.segments[0] == ReplySegment("narrator", "Once upon a time.")
|
| 33 |
+
assert p.segments[1] == ReplySegment("Knight", "Onward!")
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def test_empty_segments_are_dropped():
|
| 37 |
+
p = parse_reply("[voice:narrator] [voice:Rexy] Rawr!")
|
| 38 |
+
assert p.segments == (ReplySegment("Rexy", "Rawr!"),)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_unknown_bracket_tags_left_in_text():
|
| 42 |
+
p = parse_reply("[voice:narrator] He waves [enthusiastically].")
|
| 43 |
+
assert p.segments == (ReplySegment("narrator", "He waves [enthusiastically]."),)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_attr_values_are_whitespace_stripped():
|
| 47 |
+
p = parse_reply('[cast:add name=" Rexy " role=dino temperament=gruff]')
|
| 48 |
+
assert p.controls[0]["name"] == "Rexy"
|