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/__init__.py +189 -0
- ssmd/_version.py +34 -0
- ssmd/capabilities.py +277 -0
- ssmd/document.py +918 -0
- ssmd/formatter.py +244 -0
- ssmd/parser.py +1049 -0
- ssmd/parser_types.py +41 -0
- ssmd/py.typed +0 -0
- ssmd/segment.py +720 -0
- ssmd/sentence.py +270 -0
- ssmd/ssml_conversions.py +124 -0
- ssmd/ssml_parser.py +599 -0
- ssmd/types.py +122 -0
- ssmd/utils.py +333 -0
- ssmd/xsampa_to_ipa.txt +174 -0
- ssmd-0.5.3.dist-info/METADATA +1210 -0
- ssmd-0.5.3.dist-info/RECORD +20 -0
- ssmd-0.5.3.dist-info/WHEEL +5 -0
- ssmd-0.5.3.dist-info/licenses/LICENSE +21 -0
- ssmd-0.5.3.dist-info/top_level.txt +1 -0
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 ̽
|