utils(string): add new function parse_start_end_tags()
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from typing import Optional, Sequence, Tuple
|
||||||
|
|
||||||
ENDOFSENTENCE_PATTERN_STR = r"""
|
ENDOFSENTENCE_PATTERN_STR = r"""
|
||||||
(?<![A-Z]) # Negative lookbehind: not preceded by an uppercase letter (e.g., "U.S.A.")
|
(?<![A-Z]) # Negative lookbehind: not preceded by an uppercase letter (e.g., "U.S.A.")
|
||||||
@@ -25,8 +26,23 @@ EMAIL_PATTERN = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
|
|||||||
|
|
||||||
NUMBER_PATTERN = re.compile(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?")
|
NUMBER_PATTERN = re.compile(r"[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?")
|
||||||
|
|
||||||
|
StartEndTags = Tuple[str, str]
|
||||||
|
|
||||||
|
|
||||||
def replace_match(text: str, match: re.Match, old: str, new: str) -> str:
|
def replace_match(text: str, match: re.Match, old: str, new: str) -> str:
|
||||||
|
"""Replace occurrences of a substring within a matched section of a given
|
||||||
|
text.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The input text in which replacements will be made.
|
||||||
|
match (re.Match): A regex match object representing the section of text to modify.
|
||||||
|
old (str): The substring to be replaced.
|
||||||
|
new (str): The substring to replace `old` with.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The modified text with the specified replacements made within the matched section.
|
||||||
|
|
||||||
|
"""
|
||||||
start = match.start()
|
start = match.start()
|
||||||
end = match.end()
|
end = match.end()
|
||||||
replacement = text[start:end].replace(old, new)
|
replacement = text[start:end].replace(old, new)
|
||||||
@@ -35,6 +51,20 @@ def replace_match(text: str, match: re.Match, old: str, new: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def match_endofsentence(text: str) -> int:
|
def match_endofsentence(text: str) -> int:
|
||||||
|
"""Finds the position of the end of a sentence in the provided text string.
|
||||||
|
|
||||||
|
This function processes the input text by replacing periods in email
|
||||||
|
addresses and numbers with ampersands to prevent them from being
|
||||||
|
misidentified as sentence terminals. It then searches for the end of a
|
||||||
|
sentence using a specified regex pattern.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The input text in which to find the end of the sentence.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The position of the end of the sentence if found, otherwise 0.
|
||||||
|
|
||||||
|
"""
|
||||||
text = text.rstrip()
|
text = text.rstrip()
|
||||||
|
|
||||||
# Replace email dots by ampersands so we can find the end of sentence. For
|
# Replace email dots by ampersands so we can find the end of sentence. For
|
||||||
@@ -52,3 +82,49 @@ def match_endofsentence(text: str) -> int:
|
|||||||
match = ENDOFSENTENCE_PATTERN.search(text)
|
match = ENDOFSENTENCE_PATTERN.search(text)
|
||||||
|
|
||||||
return match.end() if match else 0
|
return match.end() if match else 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_start_end_tags(
|
||||||
|
text: str,
|
||||||
|
tags: Sequence[StartEndTags],
|
||||||
|
current_tag: Optional[StartEndTags],
|
||||||
|
current_tag_index: int,
|
||||||
|
) -> Tuple[Optional[StartEndTags], int]:
|
||||||
|
"""Parses the given text to identify a pair of start/end tags.
|
||||||
|
|
||||||
|
If a start tag was previously found (i.e. current_tags is valid), wait for
|
||||||
|
the corresponding end tag. Otherwise, wait for a start tag.
|
||||||
|
|
||||||
|
This function will return the index in the text that we should start parsing
|
||||||
|
in the next call and the current or new tags.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- text (str): The text to be parsed.
|
||||||
|
- tags (Sequence[StartEndTags]): List of tuples containing start and end tags.
|
||||||
|
- current_tags (Optional[StartEndTags]): The currently active tags, if any.
|
||||||
|
- current_tags_index (int): The current index in the text.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[Optional[StartEndTags], int]: A tuple containing None or the current
|
||||||
|
tag and the index of the text.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# If we are already inside a tag, check if the end tag is in the text.
|
||||||
|
if current_tag:
|
||||||
|
_, end_tag = current_tag
|
||||||
|
if end_tag in text[current_tag_index:]:
|
||||||
|
return (None, len(text))
|
||||||
|
return (current_tag, current_tag_index)
|
||||||
|
|
||||||
|
# Check if any start tag appears in the text
|
||||||
|
for start_tag, end_tag in tags:
|
||||||
|
start_tag_count = text[current_tag_index:].count(start_tag)
|
||||||
|
end_tag_count = text[current_tag_index:].count(end_tag)
|
||||||
|
if start_tag_count == 0 and end_tag_count == 0:
|
||||||
|
return (None, current_tag_index)
|
||||||
|
elif start_tag_count > end_tag_count:
|
||||||
|
return ((start_tag, end_tag), len(text))
|
||||||
|
elif start_tag_count == end_tag_count:
|
||||||
|
return (None, len(text))
|
||||||
|
|
||||||
|
return (None, current_tag_index)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pipecat.utils.string import match_endofsentence
|
from pipecat.utils.string import match_endofsentence, parse_start_end_tags
|
||||||
|
|
||||||
|
|
||||||
class TestUtilsString(unittest.IsolatedAsyncioTestCase):
|
class TestUtilsString(unittest.IsolatedAsyncioTestCase):
|
||||||
@@ -23,6 +23,7 @@ class TestUtilsString(unittest.IsolatedAsyncioTestCase):
|
|||||||
assert match_endofsentence("My emails are foo@pipecat.ai and bar@pipecat.ai.") == 48
|
assert match_endofsentence("My emails are foo@pipecat.ai and bar@pipecat.ai.") == 48
|
||||||
assert match_endofsentence("My email is foo.bar@pipecat.ai.") == 31
|
assert match_endofsentence("My email is foo.bar@pipecat.ai.") == 31
|
||||||
assert match_endofsentence("My email is spell(foo.bar@pipecat.ai).") == 38
|
assert match_endofsentence("My email is spell(foo.bar@pipecat.ai).") == 38
|
||||||
|
assert match_endofsentence("My email is <spell>foo.bar@pipecat.ai</spell>.") == 46
|
||||||
assert match_endofsentence("The number pi is 3.14159.") == 25
|
assert match_endofsentence("The number pi is 3.14159.") == 25
|
||||||
assert match_endofsentence("Valid scientific notation 1.23e4.") == 33
|
assert match_endofsentence("Valid scientific notation 1.23e4.") == 33
|
||||||
assert match_endofsentence("Valid scientific notation 0.e4.") == 31
|
assert match_endofsentence("Valid scientific notation 0.e4.") == 31
|
||||||
@@ -60,3 +61,50 @@ class TestUtilsString(unittest.IsolatedAsyncioTestCase):
|
|||||||
for i in hindi_sentences:
|
for i in hindi_sentences:
|
||||||
assert match_endofsentence(i)
|
assert match_endofsentence(i)
|
||||||
assert not match_endofsentence("हैलो,")
|
assert not match_endofsentence("हैलो,")
|
||||||
|
|
||||||
|
|
||||||
|
class TestStartEndTags(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_empty(self):
|
||||||
|
assert parse_start_end_tags("", [], None, 0) == (None, 0)
|
||||||
|
assert parse_start_end_tags("Hello from Pipecat!", [], None, 0) == (None, 0)
|
||||||
|
|
||||||
|
async def test_simple(self):
|
||||||
|
# (<a>, </a>)
|
||||||
|
assert parse_start_end_tags("Hello from <a>Pipecat</a>!", [("<a>", "</a>")], None, 0) == (
|
||||||
|
None,
|
||||||
|
26,
|
||||||
|
)
|
||||||
|
assert parse_start_end_tags("Hello from <a>Pipecat", [("<a>", "</a>")], None, 0) == (
|
||||||
|
("<a>", "</a>"),
|
||||||
|
21,
|
||||||
|
)
|
||||||
|
assert parse_start_end_tags("Hello from <a>Pipecat", [("<a>", "</a>")], None, 6) == (
|
||||||
|
("<a>", "</a>"),
|
||||||
|
21,
|
||||||
|
)
|
||||||
|
|
||||||
|
# (spell(, ))
|
||||||
|
assert parse_start_end_tags("Hello from spell(Pipecat)!", [("spell(", ")")], None, 0) == (
|
||||||
|
None,
|
||||||
|
26,
|
||||||
|
)
|
||||||
|
assert parse_start_end_tags("Hello from spell(Pipecat", [("spell(", ")")], None, 0) == (
|
||||||
|
("spell(", ")"),
|
||||||
|
24,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_multiple(self):
|
||||||
|
# (<a>, </a>)
|
||||||
|
assert parse_start_end_tags(
|
||||||
|
"Hello from <a>Pipecat</a>! Hello <a>World</a>!", [("<a>", "</a>")], None, 0
|
||||||
|
) == (
|
||||||
|
None,
|
||||||
|
46,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert parse_start_end_tags(
|
||||||
|
"Hello from <a>Pipecat</a>! Hello <a>World", [("<a>", "</a>")], None, 0
|
||||||
|
) == (
|
||||||
|
("<a>", "</a>"),
|
||||||
|
41,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user