ssmd 0.5.3__py3-none-any.whl

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.
ssmd/types.py ADDED
@@ -0,0 +1,122 @@
1
+ """Data types for SSMD.
2
+
3
+ This module defines the core data structures used throughout the SSMD library.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+
10
+ @dataclass
11
+ class VoiceAttrs:
12
+ """Voice attributes for TTS voice selection.
13
+
14
+ Attributes:
15
+ name: Voice name (e.g., "Joanna", "en-US-Wavenet-A")
16
+ language: BCP-47 language code (e.g., "en-US", "fr-FR")
17
+ gender: Voice gender
18
+ variant: Variant number for disambiguation
19
+ """
20
+
21
+ name: str | None = None
22
+ language: str | None = None
23
+ gender: Literal["male", "female", "neutral"] | None = None
24
+ variant: int | None = None
25
+
26
+
27
+ @dataclass
28
+ class ProsodyAttrs:
29
+ """Prosody attributes for volume, rate, and pitch control.
30
+
31
+ Attributes:
32
+ volume: Volume level ('silent', 'x-soft', 'soft', 'medium', 'loud',
33
+ 'x-loud', or relative like '+10dB')
34
+ rate: Speech rate ('x-slow', 'slow', 'medium', 'fast', 'x-fast',
35
+ or relative like '+20%')
36
+ pitch: Pitch level ('x-low', 'low', 'medium', 'high', 'x-high',
37
+ or relative like '-5%')
38
+ """
39
+
40
+ volume: str | None = None
41
+ rate: str | None = None
42
+ pitch: str | None = None
43
+
44
+
45
+ @dataclass
46
+ class BreakAttrs:
47
+ """Break/pause attributes.
48
+
49
+ Attributes:
50
+ time: Time duration (e.g., '500ms', '2s')
51
+ strength: Break strength ('none', 'x-weak', 'medium', 'strong', 'x-strong')
52
+ """
53
+
54
+ time: str | None = None
55
+ strength: str | None = None
56
+
57
+
58
+ @dataclass
59
+ class SayAsAttrs:
60
+ """Say-as attributes for text interpretation.
61
+
62
+ Attributes:
63
+ interpret_as: Interpretation type ('telephone', 'date', 'cardinal',
64
+ 'ordinal', 'characters', 'expletive', etc.)
65
+ format: Optional format string (e.g., 'dd.mm.yyyy' for dates)
66
+ detail: Optional detail level (e.g., '2' for verbosity)
67
+ """
68
+
69
+ interpret_as: str
70
+ format: str | None = None
71
+ detail: str | None = None
72
+
73
+
74
+ @dataclass
75
+ class AudioAttrs:
76
+ """Audio file attributes.
77
+
78
+ Attributes:
79
+ src: Audio file URL or path
80
+ alt_text: Fallback text if audio cannot be played
81
+ clip_begin: Start time for playback (e.g., "0s", "500ms")
82
+ clip_end: End time for playback (e.g., "10s", "5000ms")
83
+ speed: Playback speed as percentage (e.g., "150%", "80%")
84
+ repeat_count: Number of times to repeat audio
85
+ repeat_dur: Total duration for repetitions (e.g., "10s")
86
+ sound_level: Volume adjustment in dB (e.g., "+6dB", "-3dB")
87
+ """
88
+
89
+ src: str
90
+ alt_text: str | None = None
91
+ clip_begin: str | None = None
92
+ clip_end: str | None = None
93
+ speed: str | None = None
94
+ repeat_count: int | None = None
95
+ repeat_dur: str | None = None
96
+ sound_level: str | None = None
97
+
98
+
99
+ @dataclass
100
+ class PhonemeAttrs:
101
+ """Phoneme pronunciation attributes.
102
+
103
+ Attributes:
104
+ ph: Phonetic pronunciation string
105
+ alphabet: Phonetic alphabet (ipa or x-sampa)
106
+ """
107
+
108
+ ph: str
109
+ alphabet: str = "ipa"
110
+
111
+
112
+ # Heading configuration type
113
+ HeadingEffect = tuple[str, str | dict[str, str]] # e.g., ('emphasis', 'strong')
114
+ HeadingConfig = dict[int, list[HeadingEffect]]
115
+
116
+
117
+ # Default heading configurations
118
+ DEFAULT_HEADING_LEVELS: HeadingConfig = {
119
+ 1: [("pause_before", "300ms"), ("emphasis", "strong"), ("pause", "300ms")],
120
+ 2: [("pause_before", "75ms"), ("emphasis", "moderate"), ("pause", "75ms")],
121
+ 3: [("pause_before", "50ms"), ("pause", "50ms")],
122
+ }
ssmd/utils.py ADDED
@@ -0,0 +1,333 @@
1
+ """Utility functions for SSMD processing."""
2
+
3
+ import html
4
+ import re
5
+
6
+
7
+ def escape_xml(text: str) -> str:
8
+ """Escape XML special characters.
9
+
10
+ Args:
11
+ text: Input text to escape
12
+
13
+ Returns:
14
+ Text with XML entities escaped
15
+ """
16
+ return html.escape(text, quote=True)
17
+
18
+
19
+ def unescape_xml(text: str) -> str:
20
+ """Unescape XML entities.
21
+
22
+ Args:
23
+ text: Text with XML entities
24
+
25
+ Returns:
26
+ Unescaped text
27
+ """
28
+ return html.unescape(text)
29
+
30
+
31
+ def format_xml(xml_text: str, pretty: bool = True) -> str:
32
+ """Format XML with optional pretty printing.
33
+
34
+ Args:
35
+ xml_text: XML string to format
36
+ pretty: Enable pretty printing
37
+
38
+ Returns:
39
+ Formatted XML string
40
+ """
41
+ if not pretty:
42
+ return xml_text
43
+
44
+ try:
45
+ from xml.dom import minidom
46
+
47
+ dom = minidom.parseString(xml_text)
48
+ return dom.toprettyxml(indent=" ", encoding=None)
49
+ except Exception:
50
+ # Fallback: return as-is if parsing fails
51
+ return xml_text
52
+
53
+
54
+ def extract_sentences(ssml: str) -> list[str]:
55
+ """Extract sentences from SSML.
56
+
57
+ Looks for <s> tags or splits by sentence boundaries.
58
+
59
+ Args:
60
+ ssml: SSML string
61
+
62
+ Returns:
63
+ List of SSML sentence strings
64
+ """
65
+ # First try to extract <s> tags
66
+ s_tag_pattern = re.compile(r"<s>(.*?)</s>", re.DOTALL)
67
+ sentences = s_tag_pattern.findall(ssml)
68
+
69
+ if sentences:
70
+ return sentences
71
+
72
+ # Fallback: extract <p> tags
73
+ p_tag_pattern = re.compile(r"<p>(.*?)</p>", re.DOTALL)
74
+ paragraphs = p_tag_pattern.findall(ssml)
75
+
76
+ if paragraphs:
77
+ return paragraphs
78
+
79
+ # Last resort: remove <speak> wrapper and return as single sentence
80
+ clean = re.sub(r"</?speak>", "", ssml).strip()
81
+ return [clean] if clean else []
82
+
83
+
84
+ # Unicode private use area characters for placeholders
85
+ # Using \uf000+ range which is not transformed by phrasplit/spaCy
86
+ # (The \ue000-\ue00f range gets converted to dots/ellipses by some NLP tools)
87
+ _PLACEHOLDER_MAP = {
88
+ "*": "\uf000", # ASTERISK
89
+ "_": "\uf001", # UNDERSCORE
90
+ "[": "\uf002", # LEFT BRACKET
91
+ "]": "\uf003", # RIGHT BRACKET
92
+ ".": "\uf004", # DOT
93
+ "@": "\uf005", # AT SIGN
94
+ "#": "\uf006", # HASH
95
+ "~": "\uf007", # TILDE
96
+ "+": "\uf008", # PLUS
97
+ "-": "\uf009", # HYPHEN
98
+ "<": "\uf00a", # LESS THAN
99
+ ">": "\uf00b", # GREATER THAN
100
+ "^": "\uf00c", # CARET
101
+ }
102
+
103
+ # Reverse map for unescaping
104
+ _REVERSE_PLACEHOLDER_MAP = {v: k for k, v in _PLACEHOLDER_MAP.items()}
105
+
106
+
107
+ def escape_ssmd_syntax(
108
+ text: str,
109
+ patterns: list[str] | None = None,
110
+ ) -> str:
111
+ """Escape SSMD syntax patterns to prevent interpretation as markup.
112
+
113
+ This is useful when processing plain text or markdown that may contain
114
+ characters that coincidentally match SSMD syntax patterns. Uses placeholder
115
+ replacement which is reversed after SSML processing.
116
+
117
+ Args:
118
+ text: Input text that may contain SSMD-like patterns
119
+ patterns: List of pattern types to escape. If None, escapes all.
120
+ Valid values: 'emphasis', 'annotations', 'breaks', 'marks',
121
+ 'headings', 'voice_directives', 'prosody_shorthand'
122
+
123
+ Returns:
124
+ Text with SSMD patterns replaced with placeholders
125
+
126
+ Example:
127
+ >>> text = "This *word* should not be emphasized"
128
+ >>> escape_ssmd_syntax(text)
129
+ 'This \ue000word\ue000 should not be emphasized'
130
+
131
+ >>> text = "Visit [our site](https://example.com)"
132
+ >>> escaped = escape_ssmd_syntax(text)
133
+ # Placeholders prevent SSMD interpretation
134
+
135
+ >>> # Selective escaping
136
+ >>> escape_ssmd_syntax(text, patterns=['emphasis', 'breaks'])
137
+ """
138
+ if patterns is None:
139
+ # Escape all patterns by default
140
+ patterns = [
141
+ "emphasis",
142
+ "annotations",
143
+ "breaks",
144
+ "marks",
145
+ "headings",
146
+ "voice_directives",
147
+ "prosody_shorthand",
148
+ ]
149
+
150
+ result = text
151
+
152
+ # Process patterns in specific order (most specific first)
153
+ # Replace special characters with placeholders
154
+
155
+ if "voice_directives" in patterns:
156
+ # Voice directives at line start: @voice: or @voice(
157
+ result = re.sub(
158
+ r"^(@)voice([:(])",
159
+ lambda m: _PLACEHOLDER_MAP["@"] + "voice" + m.group(2),
160
+ result,
161
+ flags=re.MULTILINE,
162
+ )
163
+
164
+ if "headings" in patterns:
165
+ # Headings at line start: #, ##, ###
166
+ result = re.sub(
167
+ r"^(#{1,6})(\s)",
168
+ lambda m: _PLACEHOLDER_MAP["#"] * len(m.group(1)) + m.group(2),
169
+ result,
170
+ flags=re.MULTILINE,
171
+ )
172
+
173
+ if "emphasis" in patterns:
174
+ # Strong emphasis: **text**
175
+ result = re.sub(
176
+ r"\*\*([^*]+)\*\*",
177
+ lambda m: _PLACEHOLDER_MAP["*"] * 2
178
+ + m.group(1)
179
+ + _PLACEHOLDER_MAP["*"] * 2,
180
+ result,
181
+ )
182
+ # Moderate emphasis: *text*
183
+ result = re.sub(
184
+ r"\*([^*\n]+)\*",
185
+ lambda m: _PLACEHOLDER_MAP["*"] + m.group(1) + _PLACEHOLDER_MAP["*"],
186
+ result,
187
+ )
188
+ # Reduced emphasis/pitch: _text_ (but not in middle of words)
189
+ result = re.sub(
190
+ r"(?<!\w)_([^_\n]+)_(?!\w)",
191
+ lambda m: _PLACEHOLDER_MAP["_"] + m.group(1) + _PLACEHOLDER_MAP["_"],
192
+ result,
193
+ )
194
+
195
+ if "annotations" in patterns:
196
+ # Annotations: [text](params) - replace the brackets
197
+ result = re.sub(
198
+ r"\[([^\]]+)\]\(([^)]+)\)",
199
+ lambda m: _PLACEHOLDER_MAP["["]
200
+ + m.group(1)
201
+ + _PLACEHOLDER_MAP["]"]
202
+ + "("
203
+ + m.group(2)
204
+ + ")",
205
+ result,
206
+ )
207
+
208
+ if "breaks" in patterns:
209
+ # Breaks: ...n, ...w, ...c, ...s, ...p, ...500ms, ...5s
210
+ result = re.sub(
211
+ r"\.\.\.((?:[nwcsp]|\d+(?:ms|s))(?:\s|$))",
212
+ lambda m: _PLACEHOLDER_MAP["."] * 3 + m.group(1),
213
+ result,
214
+ )
215
+
216
+ if "marks" in patterns:
217
+ # Marks: @word (but not @voice which is handled above)
218
+ # Use word boundary to avoid matching @domain in emails
219
+ result = re.sub(
220
+ r"(?<!\w)@(?!voice)(\w+)",
221
+ lambda m: _PLACEHOLDER_MAP["@"] + m.group(1),
222
+ result,
223
+ )
224
+
225
+ if "prosody_shorthand" in patterns:
226
+ # Prosody shorthand - paired characters around text
227
+ # Double character versions first
228
+ result = re.sub(
229
+ r"~~([^~\n]+)~~",
230
+ lambda m: _PLACEHOLDER_MAP["~"] * 2
231
+ + m.group(1)
232
+ + _PLACEHOLDER_MAP["~"] * 2,
233
+ result,
234
+ )
235
+ result = re.sub(
236
+ r"\+\+([^+\n]+)\+\+",
237
+ lambda m: _PLACEHOLDER_MAP["+"] * 2
238
+ + m.group(1)
239
+ + _PLACEHOLDER_MAP["+"] * 2,
240
+ result,
241
+ )
242
+ result = re.sub(
243
+ r"--([^-\n]+)--",
244
+ lambda m: _PLACEHOLDER_MAP["-"] * 2
245
+ + m.group(1)
246
+ + _PLACEHOLDER_MAP["-"] * 2,
247
+ result,
248
+ )
249
+ result = re.sub(
250
+ r"<<([^<\n]+)<<",
251
+ lambda m: _PLACEHOLDER_MAP["<"] * 2
252
+ + m.group(1)
253
+ + _PLACEHOLDER_MAP["<"] * 2,
254
+ result,
255
+ )
256
+ result = re.sub(
257
+ r">>([^>\n]+)>>",
258
+ lambda m: _PLACEHOLDER_MAP[">"] * 2
259
+ + m.group(1)
260
+ + _PLACEHOLDER_MAP[">"] * 2,
261
+ result,
262
+ )
263
+ result = re.sub(
264
+ r"\^\^([^^|\n]+)\^\^",
265
+ lambda m: _PLACEHOLDER_MAP["^"] * 2
266
+ + m.group(1)
267
+ + _PLACEHOLDER_MAP["^"] * 2,
268
+ result,
269
+ )
270
+ result = re.sub(
271
+ r"__([^_\n]+)__",
272
+ lambda m: _PLACEHOLDER_MAP["_"] * 2
273
+ + m.group(1)
274
+ + _PLACEHOLDER_MAP["_"] * 2,
275
+ result,
276
+ )
277
+
278
+ # Single character versions
279
+ result = re.sub(
280
+ r"~([^~\n]+)~",
281
+ lambda m: _PLACEHOLDER_MAP["~"] + m.group(1) + _PLACEHOLDER_MAP["~"],
282
+ result,
283
+ )
284
+ result = re.sub(
285
+ r"\+([^+\n]+)\+",
286
+ lambda m: _PLACEHOLDER_MAP["+"] + m.group(1) + _PLACEHOLDER_MAP["+"],
287
+ result,
288
+ )
289
+ result = re.sub(
290
+ r"-([^-\n]+)-",
291
+ lambda m: _PLACEHOLDER_MAP["-"] + m.group(1) + _PLACEHOLDER_MAP["-"],
292
+ result,
293
+ )
294
+ result = re.sub(
295
+ r"<([^<\n]+)<",
296
+ lambda m: _PLACEHOLDER_MAP["<"] + m.group(1) + _PLACEHOLDER_MAP["<"],
297
+ result,
298
+ )
299
+ result = re.sub(
300
+ r">([^>\n]+)>",
301
+ lambda m: _PLACEHOLDER_MAP[">"] + m.group(1) + _PLACEHOLDER_MAP[">"],
302
+ result,
303
+ )
304
+ result = re.sub(
305
+ r"\^([^^\n]+)\^",
306
+ lambda m: _PLACEHOLDER_MAP["^"] + m.group(1) + _PLACEHOLDER_MAP["^"],
307
+ result,
308
+ )
309
+
310
+ return result
311
+
312
+
313
+ def unescape_ssmd_syntax(text: str) -> str:
314
+ """Remove placeholder escaping from SSMD syntax.
315
+
316
+ This is used internally to replace placeholders with original characters
317
+ after TTS processing.
318
+
319
+ Args:
320
+ text: Text with placeholder-escaped SSMD syntax
321
+
322
+ Returns:
323
+ Text with placeholders replaced by original characters
324
+
325
+ Example:
326
+ >>> unescape_ssmd_syntax("This \ue000word\ue000 is escaped")
327
+ 'This *word* is escaped'
328
+ """
329
+ result = text
330
+ # Replace all placeholders with their original characters
331
+ for placeholder, original in _REVERSE_PLACEHOLDER_MAP.items():
332
+ result = result.replace(placeholder, original)
333
+ return result
ssmd/xsampa_to_ipa.txt ADDED
@@ -0,0 +1,174 @@
1
+ a a
2
+ b b
3
+ b_< ɓ
4
+ c c
5
+ d d
6
+ d` ɖ
7
+ d_< ɗ
8
+ e e
9
+ f f
10
+ g ɡ
11
+ g_< ɠ
12
+ h h
13
+ h\ ɦ
14
+ i i
15
+ j j
16
+ j\ ʝ
17
+ k k
18
+ l l
19
+ l` ɭ
20
+ l\ ɺ
21
+ m m
22
+ n n
23
+ n` ɳ
24
+ o o
25
+ p p
26
+ p\ ɸ
27
+ q q
28
+ r r
29
+ r` ɽ
30
+ r\ ɹ
31
+ r\` ɻ
32
+ s s
33
+ s` ʂ
34
+ s\ ɕ
35
+ t t
36
+ t` ʈ
37
+ u u
38
+ v v
39
+ v\ ʋ
40
+ P ʋ
41
+ w w
42
+ x x
43
+ x\ ɧ
44
+ y y
45
+ z z
46
+ z` ʐ
47
+ z\ ʑ
48
+ A ɑ
49
+ B β
50
+ B\ ʙ
51
+ C ç
52
+ D ð
53
+ E ɛ
54
+ F ɱ
55
+ G ɣ
56
+ G\ ɢ
57
+ G\_< ʛ
58
+ H ɥ
59
+ H\ ʜ
60
+ I ɪ
61
+ I\ ɪ̈
62
+ I\ ɨ̞
63
+ J ɲ
64
+ J\ ɟ
65
+ J\_< ʄ
66
+ K ɬ
67
+ K\ ɮ
68
+ L ʎ
69
+ L\ ʟ
70
+ M ɯ
71
+ M\ ɰ
72
+ N ŋ
73
+ N\ ɴ
74
+ O ɔ
75
+ O\ ʘ
76
+ P ʋ
77
+ v\ ʋ
78
+ Q ɒ
79
+ R ʁ
80
+ R\ ʀ
81
+ S ʃ
82
+ T θ
83
+ U ʊ
84
+ U\ ʊ̈
85
+ U\ ʉ̞
86
+ V ʌ
87
+ W ʍ
88
+ X χ
89
+ X\ ħ
90
+ Y ʏ
91
+ Z ʒ
92
+ . .
93
+ " ˈ
94
+ % ˌ
95
+ ' ʲ
96
+ _j ʲ
97
+ : ː
98
+ :\ ˑ
99
+ @ ə
100
+ @\ ɘ
101
+ { æ
102
+ } ʉ
103
+ 1 ɨ
104
+ 2 ø
105
+ 3 ɜ
106
+ 3\ ɞ
107
+ 4 ɾ
108
+ 5 ɫ
109
+ 6 ɐ
110
+ 7 ɤ
111
+ 8 ɵ
112
+ 9 œ
113
+ & ɶ
114
+ ? ʔ
115
+ ?\ ʕ
116
+ <\ ʢ
117
+ >\ ʡ
118
+ ^ ꜛ
119
+ ! ꜜ
120
+ !\ ǃ
121
+ | |
122
+ |\ ǀ
123
+ || ‖
124
+ |\|\ ǁ
125
+ =\ ǂ
126
+ -\ ‿
127
+ _" ̈
128
+ _+ ̟
129
+ _- ̠
130
+ _/ ̌
131
+ _0 ̥
132
+ = ̩
133
+ _= ̩
134
+ _> ʼ
135
+ _?\ ˤ
136
+ _\ ̂
137
+ _^ ̯
138
+ _} ̚
139
+ ` ˞
140
+ ~ ̃
141
+ _~ ̃
142
+ _A ̘
143
+ _a ̺
144
+ _B ̏
145
+ _B_L ᷅
146
+ _c ̜
147
+ _d ̪
148
+ _e ̴
149
+ _F ̂
150
+ _G ˠ
151
+ _H ́
152
+ _H_T ᷄
153
+ _h ʰ
154
+ _j ʲ
155
+ ' ʲ
156
+ _k ̰
157
+ _L ̀
158
+ _l ˡ
159
+ _M ̄
160
+ _m ̻
161
+ _N ̼
162
+ _n ⁿ
163
+ _O ̹
164
+ _o ̞
165
+ _q ̙
166
+ _R ̌
167
+ _R_F ᷈
168
+ _r ̝
169
+ _T ̋
170
+ _t ̤
171
+ _v ̬
172
+ _w ʷ
173
+ _X ̆
174
+ _x ̽