Skip to content

Commit 4324120

Browse files
authored
feat(helpers): add TextBuilder class for TTS pronunciation and pause controls (#660)
## Summary Add `TextBuilder` — a fluent builder class for constructing TTS text with inline pronunciation (IPA) and pause controls. Includes SSML-to-Deepgram conversion for migrating from other TTS providers. Rebased from #646 onto current main to resolve conflicts. ## Usage ```python from deepgram import DeepgramClient from deepgram.helpers import TextBuilder text = ( TextBuilder() .text("Take ") .pronunciation("azathioprine", "ˌæzəˈθaɪəpriːn") .pause(500) .text(" twice daily.") .build() ) client = DeepgramClient() response = client.speak.v1.audio.generate(text=text, model="aura-2-asteria-en") ``` ### SSML Migration ```python from deepgram.helpers import ssml_to_deepgram ssml = '<speak>Take <phoneme alphabet="ipa" ph="ˌæzəˈθaɪəpriːn">azathioprine</phoneme> daily.</speak>' deepgram_text = ssml_to_deepgram(ssml) ``` ## What's Included - **`src/deepgram/helpers/text_builder.py`** — `TextBuilder` class, `add_pronunciation()`, `ssml_to_deepgram()`, `validate_ipa()`, `validate_pause()` - **`tests/custom/test_text_builder.py`** — 44 tests covering all features and edge cases - **`examples/22-text-builder-demo.py`** — interactive demo (no API key needed) - **`examples/23-text-builder-helper.py`** — REST API integration examples - **`examples/24-text-builder-streaming.py`** — WebSocket streaming examples ## Design Decision Imports come from `deepgram.helpers` (not `deepgram`) so the auto-generated `__init__.py` doesn't need modification. Fern regeneration won't break anything. ## Test plan - [x] 44 tests pass (`pytest tests/custom/test_text_builder.py`) - [x] `mypy src/` clean (708 files, 0 errors) - [x] No changes to auto-generated files
1 parent 2046175 commit 4324120

29 files changed

Lines changed: 1625 additions & 29 deletions

.fernignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ CONTRIBUTING.md
3333
# Reference with Fern-generated REST API docs plus manually maintained WebSocket sections
3434
reference.md
3535

36+
# TextBuilder helpers for TTS pronunciation and pause controls.
37+
# Manually maintained — not auto-generated.
38+
src/deepgram/helpers
39+
3640
# Custom WebSocket transport support:
3741
# - transport_interface.py: Protocol definitions (SyncTransport, AsyncTransport) for
3842
# users implementing custom transports. This is the public-facing interface file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

examples/22-text-builder-demo.py

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
#!/usr/bin/env python3
2+
"""
3+
TextBuilder Demo - Interactive demonstration of all TextBuilder features
4+
5+
This demo script showcases all TextBuilder capabilities without requiring
6+
an API key. It generates the formatted text that would be sent to the API.
7+
"""
8+
9+
from deepgram.helpers import (
10+
TextBuilder,
11+
add_pronunciation,
12+
ssml_to_deepgram,
13+
validate_ipa,
14+
validate_pause,
15+
)
16+
17+
18+
def print_section(title: str):
19+
"""Print a formatted section header"""
20+
print("\n" + "=" * 70)
21+
print(f" {title}")
22+
print("=" * 70)
23+
24+
25+
def demo_basic_text_builder():
26+
"""Demonstrate basic TextBuilder usage"""
27+
print_section("1. Basic TextBuilder Usage")
28+
29+
text = (
30+
TextBuilder()
31+
.text("Take ")
32+
.pronunciation("azathioprine", "ˌæzəˈθaɪəpriːn")
33+
.text(" twice daily with ")
34+
.pronunciation("dupilumab", "duːˈpɪljuːmæb")
35+
.text(" injections")
36+
.pause(500)
37+
.text(" Do not exceed prescribed dosage.")
38+
.build()
39+
)
40+
41+
print("\nCode:")
42+
print("""
43+
text = (
44+
TextBuilder()
45+
.text("Take ")
46+
.pronunciation("azathioprine", "ˌæzəˈθaɪəpriːn")
47+
.text(" twice daily with ")
48+
.pronunciation("dupilumab", "duːˈpɪljuːmæb")
49+
.text(" injections")
50+
.pause(500)
51+
.text(" Do not exceed prescribed dosage.")
52+
.build()
53+
)
54+
""")
55+
56+
print("\nGenerated TTS Text:")
57+
print(f" {text}")
58+
59+
60+
def demo_standalone_functions():
61+
"""Demonstrate standalone helper functions"""
62+
print_section("2. Standalone Helper Functions")
63+
64+
# add_pronunciation
65+
print("\n▸ add_pronunciation()")
66+
text = "The patient should take methotrexate weekly."
67+
print(f" Original: {text}")
68+
69+
text = add_pronunciation(text, "methotrexate", "mɛθəˈtrɛkseɪt")
70+
print(f" Modified: {text}")
71+
72+
73+
def demo_ssml_conversion():
74+
"""Demonstrate SSML to Deepgram conversion"""
75+
print_section("3. SSML Migration")
76+
77+
ssml = """<speak>
78+
Welcome to your medication guide.
79+
<break time="500ms"/>
80+
Take <phoneme alphabet="ipa" ph="ˌæzəˈθaɪəpriːn">azathioprine</phoneme>
81+
as prescribed.
82+
<break time="1s"/>
83+
Contact your doctor if you experience side effects.
84+
</speak>"""
85+
86+
print("\nOriginal SSML:")
87+
print(ssml)
88+
89+
text = ssml_to_deepgram(ssml)
90+
print("\nConverted to Deepgram Format:")
91+
print(f" {text}")
92+
93+
94+
def demo_mixed_usage():
95+
"""Demonstrate mixing SSML with builder methods"""
96+
print_section("4. Mixed SSML + Builder Methods")
97+
98+
ssml = '<speak>Take <phoneme alphabet="ipa" ph="test">medicine</phoneme> daily.</speak>'
99+
100+
text = (
101+
TextBuilder()
102+
.from_ssml(ssml)
103+
.pause(500)
104+
.text(" Store at room temperature.")
105+
.pause(500)
106+
.text(" Keep out of reach of children.")
107+
.build()
108+
)
109+
110+
print("\nStarting SSML:")
111+
print(f" {ssml}")
112+
113+
print("\nAdded via builder:")
114+
print(" .pause(500)")
115+
print(" .text(' Store at room temperature.')")
116+
print(" .pause(500)")
117+
print(" .text(' Keep out of reach of children.')")
118+
119+
print("\nFinal Result:")
120+
print(f" {text}")
121+
122+
123+
def demo_validation():
124+
"""Demonstrate validation functions"""
125+
print_section("5. Validation Functions")
126+
127+
print("\n▸ validate_ipa()")
128+
129+
# Valid IPA
130+
is_valid, msg = validate_ipa("ˌæzəˈθaɪəpriːn")
131+
print(f" validate_ipa('ˌæzəˈθaɪəpriːn'): {is_valid} {msg}")
132+
133+
# Invalid IPA (contains quote)
134+
is_valid, msg = validate_ipa('test"quote')
135+
print(f" validate_ipa('test\"quote'): {is_valid} - {msg}")
136+
137+
# Too long
138+
is_valid, msg = validate_ipa("x" * 101)
139+
print(f" validate_ipa('x' * 101): {is_valid} - {msg}")
140+
141+
print("\n▸ validate_pause()")
142+
143+
# Valid pauses
144+
is_valid, msg = validate_pause(500)
145+
print(f" validate_pause(500): {is_valid}")
146+
147+
is_valid, msg = validate_pause(5000)
148+
print(f" validate_pause(5000): {is_valid}")
149+
150+
# Invalid pauses
151+
is_valid, msg = validate_pause(400)
152+
print(f" validate_pause(400): {is_valid} - {msg}")
153+
154+
is_valid, msg = validate_pause(550)
155+
print(f" validate_pause(550): {is_valid} - {msg}")
156+
157+
158+
def demo_error_handling():
159+
"""Demonstrate error handling"""
160+
print_section("6. Error Handling")
161+
162+
print("\n▸ Pronunciation limit (500 max)")
163+
try:
164+
builder = TextBuilder()
165+
for i in range(501):
166+
builder.pronunciation(f"word{i}", "test")
167+
builder.build()
168+
except ValueError as e:
169+
print(f" ✓ Caught expected error: {e}")
170+
171+
print("\n▸ Pause limit (50 max)")
172+
try:
173+
builder = TextBuilder()
174+
for i in range(51):
175+
builder.pause(500)
176+
builder.build()
177+
except ValueError as e:
178+
print(f" ✓ Caught expected error: {e}")
179+
180+
print("\n▸ Character limit (2000 max)")
181+
try:
182+
builder = TextBuilder()
183+
builder.text("x" * 2001)
184+
builder.build()
185+
except ValueError as e:
186+
print(f" ✓ Caught expected error: {e}")
187+
188+
print("\n▸ Invalid pause duration")
189+
try:
190+
builder = TextBuilder()
191+
builder.pause(450)
192+
except ValueError as e:
193+
print(f" ✓ Caught expected error: {e}")
194+
195+
196+
def demo_real_world_examples():
197+
"""Demonstrate real-world use cases"""
198+
print_section("7. Real-World Examples")
199+
200+
print("\n▸ Pharmacy Prescription Instructions")
201+
text = (
202+
TextBuilder()
203+
.text("Prescription for ")
204+
.pronunciation("lisinopril", "laɪˈsɪnəprɪl")
205+
.pause(500)
206+
.text(" Take one tablet daily for hypertension.")
207+
.pause(500)
208+
.text(" Common side effects may include ")
209+
.pronunciation("hypotension", "ˌhaɪpoʊˈtɛnʃən")
210+
.text(" or dizziness.")
211+
.build()
212+
)
213+
print(f"\n {text}")
214+
215+
print("\n▸ Medical Device Instructions")
216+
text = (
217+
TextBuilder()
218+
.text("Insert the ")
219+
.pronunciation("cannula", "ˈkænjʊlə")
220+
.text(" at a forty-five degree angle.")
221+
.pause(1000)
222+
.text(" Ensure the ")
223+
.pronunciation("catheter", "ˈkæθɪtər")
224+
.text(" is properly secured.")
225+
.build()
226+
)
227+
print(f"\n {text}")
228+
229+
print("\n▸ Scientific Terminology")
230+
text = (
231+
TextBuilder()
232+
.text("The study examined ")
233+
.pronunciation("mitochondrial", "ˌmaɪtəˈkɑːndriəl")
234+
.text(" function in ")
235+
.pronunciation("erythrocytes", "ɪˈrɪθrəsaɪts")
236+
.pause(500)
237+
.text(" using advanced imaging.")
238+
.build()
239+
)
240+
print(f"\n {text}")
241+
242+
243+
def demo_api_limits():
244+
"""Display API limits summary"""
245+
print_section("8. API Limits Summary")
246+
247+
print("\n Limit Type Maximum Unit")
248+
print(" " + "-" * 60)
249+
print(" Pronunciations per request 500 count")
250+
print(" Pauses per request 50 count")
251+
print(" Total characters 2000 characters*")
252+
print(" IPA string length 100 characters")
253+
print(" Pause duration (min) 500 milliseconds")
254+
print(" Pause duration (max) 5000 milliseconds")
255+
print(" Pause increment 100 milliseconds")
256+
print("\n * Character count excludes pronunciation IPA and control syntax")
257+
258+
259+
def main():
260+
"""Run all demonstrations"""
261+
print("\n" + "█" * 70)
262+
print(" DEEPGRAM TEXTBUILDER - COMPREHENSIVE DEMONSTRATION")
263+
print("█" * 70)
264+
265+
demo_basic_text_builder()
266+
demo_standalone_functions()
267+
demo_ssml_conversion()
268+
demo_mixed_usage()
269+
demo_validation()
270+
demo_error_handling()
271+
demo_real_world_examples()
272+
demo_api_limits()
273+
274+
print("\n" + "=" * 70)
275+
print(" Demo Complete!")
276+
print("=" * 70)
277+
print("\n REST API generation: examples/23-text-builder-helper.py")
278+
print(" Streaming TTS: examples/24-text-builder-streaming.py")
279+
print("=" * 70 + "\n")
280+
281+
282+
if __name__ == "__main__":
283+
main()

0 commit comments

Comments
 (0)