Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/deepgram/helpers/text_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,12 +239,19 @@ def ssml_to_deepgram(ssml_text: str) -> str:
# Parse XML fragments manually to handle mixed content
# Use regex to find and replace SSML elements

# Handle <phoneme> tags
phoneme_pattern = r'<phoneme\s+alphabet=["\']ipa["\']\s+ph=["\'](.*?)["\']\s*>(.*?)</phoneme>'
# Handle <phoneme> tags. Attribute order is not significant in SSML, so match
# the attributes as a group and pull `ph` out of it rather than requiring
# `alphabet` before `ph` (which silently dropped the pronunciation otherwise).
phoneme_pattern = r"<phoneme\s+([^<>]*?)\s*>([^<]*)</phoneme>"

def replace_phoneme(match):
ipa = match.group(1)
attributes = match.group(1)
word = match.group(2)
alphabet_match = re.search(r'(?:^|\s)alphabet\s*=\s*(["\'])ipa\1(?=\s|$)', attributes)
ph_match = re.search(r'(?:^|\s)ph\s*=\s*(["\'])(.*?)\1(?=\s|$)', attributes)
if alphabet_match is None or ph_match is None:
return word
ipa = ph_match.group(2)
return json.dumps({"word": word, "pronounce": ipa}, ensure_ascii=False)

ssml_text = re.sub(phoneme_pattern, replace_phoneme, ssml_text)
Expand Down
47 changes: 44 additions & 3 deletions tests/custom/test_text_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,52 @@ def test_basic_phoneme(self):
"""Test converting basic phoneme tag"""
ssml = '<phoneme alphabet="ipa" ph="ˌæzəˈθaɪəpriːn">azathioprine</phoneme>'
result = ssml_to_deepgram(ssml)

assert '"word": "azathioprine"' in result
assert '"pronounce": "ˌæzəˈθaɪəpriːn"' in result


def test_phoneme_attribute_order_independent(self):
"""ph before alphabet must work too (SSML attribute order is not significant)"""
ssml = '<phoneme ph="ˌæzəˈθaɪəpriːn" alphabet="ipa">azathioprine</phoneme>'
result = ssml_to_deepgram(ssml)

assert '"word": "azathioprine"' in result
assert '"pronounce": "ˌæzəˈθaɪəpriːn"' in result

def test_phoneme_attributes_allow_whitespace_around_equals(self):
"""Valid XML whitespace around attribute equals signs must be accepted"""
ssml = "<phoneme ph = 'ˌæzəˈθaɪəpriːn' alphabet = \"ipa\">azathioprine</phoneme>"
result = ssml_to_deepgram(ssml)

assert '"word": "azathioprine"' in result
assert '"pronounce": "ˌæzəˈθaɪəpriːn"' in result

@pytest.mark.parametrize(
"attributes",
[
'ph="test"',
'alphabet="x-sampa" ph="test"',
'alphabet="ipa" data-ph="test"',
],
)
def test_phoneme_requires_ipa_alphabet_and_ph_attribute(self, attributes):
"""Unsupported or lookalike attributes must degrade to plain text"""
ssml = f"<phoneme {attributes}>medicine</phoneme>"

assert ssml_to_deepgram(ssml) == "medicine"

def test_unclosed_phoneme_does_not_consume_following_phoneme(self):
"""Malformed input must not capture a later valid phoneme tag"""
ssml = (
'<phoneme alphabet="ipa" ph="bad">'
'<phoneme ph="good" alphabet="ipa">medicine</phoneme>'
)
result = ssml_to_deepgram(ssml)

assert '"word": "medicine"' in result
assert '"pronounce": "good"' in result
assert '"pronounce": "bad"' not in result

def test_basic_break(self):
"""Test converting break tag (milliseconds)"""
ssml = '<break time="500ms"/>'
Expand Down Expand Up @@ -497,4 +539,3 @@ def test_standalone_function_workflow(self):
assert '"pronounce": "ˌæzəˈθaɪəpriːn"' in text
assert '"word": "dupilumab"' in text
assert '"pronounce": "duːˈpɪljuːmæb"' in text

Loading