scriptconv 0.0.3a9__tar.gz → 0.0.3a11__tar.gz

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.
Files changed (37) hide show
  1. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/PKG-INFO +2 -2
  2. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/README.md +1 -1
  3. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/notation.py +45 -12
  4. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/scripts.py +38 -3
  5. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/version.py +1 -1
  6. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv.egg-info/PKG-INFO +2 -2
  7. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv.egg-info/SOURCES.txt +2 -0
  8. scriptconv-0.0.3a11/tests/test_arpa_stress.py +88 -0
  9. scriptconv-0.0.3a11/tests/test_scripts_stressonnx_compat.py +47 -0
  10. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/LICENSE +0 -0
  11. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/pyproject.toml +0 -0
  12. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/requirements.txt +0 -0
  13. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/__init__.py +0 -0
  14. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/__main__.py +0 -0
  15. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/cangjie.py +0 -0
  16. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/conventions.py +0 -0
  17. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/data/__init__.py +0 -0
  18. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
  19. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/graph.py +0 -0
  20. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/py.typed +0 -0
  21. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/readings.py +0 -0
  22. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv/translit.py +0 -0
  23. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv.egg-info/dependency_links.txt +0 -0
  24. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv.egg-info/requires.txt +0 -0
  25. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/scriptconv.egg-info/top_level.txt +0 -0
  26. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/setup.cfg +0 -0
  27. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_cangjie.py +0 -0
  28. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_cli.py +0 -0
  29. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_conventions.py +0 -0
  30. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_errors_policy.py +0 -0
  31. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_examples.py +0 -0
  32. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_graph.py +0 -0
  33. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_notation.py +0 -0
  34. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_readings.py +0 -0
  35. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_readings_zh.py +0 -0
  36. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_scripts.py +0 -0
  37. {scriptconv-0.0.3a9 → scriptconv-0.0.3a11}/tests/test_translit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.3a9
3
+ Version: 0.0.3a11
4
4
  Summary: Zero-dependency script & phoneme-notation core — ISO-15924 detection & metadata, IPA↔ARPABET/X-SAMPA/Lexique/Kirshenbaum/Cotovía/RFE, Buckwalter↔Arabic, Hangul→jamo, kana
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/TigreGotico/scriptconv
@@ -210,7 +210,7 @@ below describes the per-converter default.
210
210
 
211
211
  | Notation | `to_ipa` → `from_ipa` round-trip | `from_ipa` → `to_ipa` round-trip | Unknown-token behaviour |
212
212
  |----------|----------------------------------|----------------------------------|-------------------------|
213
- | **ARPABET** | Exact for base symbols; **stress digits are dropped** and `AH0`↔`AX` (schwa) is not distinguished | **Lossy** — ARPABET is an English-only inventory, so any IPA symbol outside it becomes the *unknown* placeholder | `arpa_to_ipa`: passed through unchanged. `ipa_to_arpa`: diacritics and suprasegmentals (combining marks, length/stress modifiers) are dropped; other out-of-inventory symbols are replaced with `?` by default (`unknown=` param; `unknown=""` drops) |
213
+ | **ARPABET** | **Lossless with `stress=True`** (digits ↔ IPA `ˈ`/`ˌ` before the stressed vowel; residues: extended-ARPABET `AX` normalises to CMUdict's `AH0` spelling, and `AH0 R` fuses to the r-colored `AXR0` — stable from the IPA side). Default `stress=False` drops digits and merges `AH0`↔`AX` | **Lossy** — ARPABET is an English-only inventory, so any IPA symbol outside it becomes the *unknown* placeholder | `arpa_to_ipa`: passed through unchanged. `ipa_to_arpa`: diacritics and suprasegmentals are dropped; other out-of-inventory symbols follow `errors=` (default `"replace"` → `?`) |
214
214
  | **X-SAMPA** | Exact for all canonical symbols | Exact except aliases (`f\`→ɸ, `&`→æ) normalise to their canonical spelling | Passed through unchanged |
215
215
  | **Buckwalter ↔ Arabic** | Exact (the `^` shadda alias normalises to canonical `~`; precomposed lam-alef ligatures decompose to two chars, visually identical) | Exact | Passed through unchanged |
216
216
  | **Lexique ↔ IPA** | Exact except the `°`/`3` schwa pair (both → `ə`; reverse always → `°`) | Exact | Passed through unchanged |
@@ -177,7 +177,7 @@ below describes the per-converter default.
177
177
 
178
178
  | Notation | `to_ipa` → `from_ipa` round-trip | `from_ipa` → `to_ipa` round-trip | Unknown-token behaviour |
179
179
  |----------|----------------------------------|----------------------------------|-------------------------|
180
- | **ARPABET** | Exact for base symbols; **stress digits are dropped** and `AH0`↔`AX` (schwa) is not distinguished | **Lossy** — ARPABET is an English-only inventory, so any IPA symbol outside it becomes the *unknown* placeholder | `arpa_to_ipa`: passed through unchanged. `ipa_to_arpa`: diacritics and suprasegmentals (combining marks, length/stress modifiers) are dropped; other out-of-inventory symbols are replaced with `?` by default (`unknown=` param; `unknown=""` drops) |
180
+ | **ARPABET** | **Lossless with `stress=True`** (digits ↔ IPA `ˈ`/`ˌ` before the stressed vowel; residues: extended-ARPABET `AX` normalises to CMUdict's `AH0` spelling, and `AH0 R` fuses to the r-colored `AXR0` — stable from the IPA side). Default `stress=False` drops digits and merges `AH0`↔`AX` | **Lossy** — ARPABET is an English-only inventory, so any IPA symbol outside it becomes the *unknown* placeholder | `arpa_to_ipa`: passed through unchanged. `ipa_to_arpa`: diacritics and suprasegmentals are dropped; other out-of-inventory symbols follow `errors=` (default `"replace"` → `?`) |
181
181
  | **X-SAMPA** | Exact for all canonical symbols | Exact except aliases (`f\`→ɸ, `&`→æ) normalise to their canonical spelling | Passed through unchanged |
182
182
  | **Buckwalter ↔ Arabic** | Exact (the `^` shadda alias normalises to canonical `~`; precomposed lam-alef ligatures decompose to two chars, visually identical) | Exact | Passed through unchanged |
183
183
  | **Lexique ↔ IPA** | Exact except the `°`/`3` schwa pair (both → `ə`; reverse always → `°`) | Exact | Passed through unchanged |
@@ -157,6 +157,11 @@ _IPA_TO_ARPA.setdefault("g", "G")
157
157
  _IPA_ARPA_KEYS_SORTED = sorted(_IPA_TO_ARPA.keys(), key=len, reverse=True)
158
158
  _IPA_ARPA_RE = re.compile("|".join(re.escape(s) for s in _IPA_ARPA_KEYS_SORTED))
159
159
 
160
+ # ARPABET vowel class — the symbols that carry stress digits.
161
+ _ARPA_VOWELS = {"AO", "AA", "IY", "UW", "EH", "IH", "UH", "AH", "AE", "AX",
162
+ "EY", "AY", "OW", "AW", "OY", "ER", "AXR"}
163
+ _STRESS_MARKS = {"ˈ": "1", "ˌ": "2"} # IPA primary/secondary stress
164
+
160
165
 
161
166
 
162
167
  class UnknownSymbolError(ValueError):
@@ -187,12 +192,18 @@ def _unknown(symbol: str, position: int, notation: str, errors: str,
187
192
  raise ValueError(f"errors must be one of {_ERROR_POLICIES}, not {errors!r}")
188
193
 
189
194
 
190
- def arpa_to_ipa(arpa_sequence: str, errors: str = "pass") -> str:
195
+ def arpa_to_ipa(arpa_sequence: str, errors: str = "pass",
196
+ stress: bool = False) -> str:
191
197
  """Convert a space-separated ARPABET sequence to an IPA string.
192
198
 
193
- Stress digits are stripped. ``errors`` selects the unknown-token policy:
194
- ``"pass"`` (default) keeps the token, ``"replace"`` substitutes ``?``,
195
- ``"ignore"`` drops it, ``"strict"`` raises :class:`UnknownSymbolError`.
199
+ With ``stress=True``, ARPABET stress digits are preserved as IPA stress
200
+ marks placed immediately before the stressed vowel's symbol: ``AH1``
201
+ ``ˈʌ``, ``AH2`` ``ˌʌ``, ``AH0`` unmarked (a purely notational,
202
+ reversible convention — no syllabification is performed). The default
203
+ ``stress=False`` keeps the historical behavior of stripping the digits.
204
+ ``errors`` selects the unknown-token policy: ``"pass"`` (default) keeps
205
+ the token, ``"replace"`` substitutes ``?``, ``"ignore"`` drops it,
206
+ ``"strict"`` raises :class:`UnknownSymbolError`.
196
207
 
197
208
  Examples
198
209
  --------
@@ -202,6 +213,9 @@ def arpa_to_ipa(arpa_sequence: str, errors: str = "pass") -> str:
202
213
  tokens = arpa_sequence.strip().split()
203
214
  result = []
204
215
  for i, tok in enumerate(tokens):
216
+ mark = ""
217
+ if stress and tok[-1:] in ("1", "2") and tok[:-1] in _ARPA_VOWELS:
218
+ mark = "ˈ" if tok[-1] == "1" else "ˌ"
205
219
  ipa = _ARPA_TO_IPA.get(tok)
206
220
  if ipa is None:
207
221
  # strip trailing digit and retry
@@ -209,19 +223,24 @@ def arpa_to_ipa(arpa_sequence: str, errors: str = "pass") -> str:
209
223
  ipa = _ARPA_TO_IPA.get(stripped)
210
224
  if ipa is None:
211
225
  ipa = _unknown(tok, i, "arpa", errors)
212
- result.append(ipa)
226
+ mark = ""
227
+ result.append(mark + ipa)
213
228
  return "".join(result)
214
229
 
215
230
 
216
231
  def ipa_to_arpa(ipa_string: str, unknown: str = "?",
217
- errors: str = "replace") -> str:
232
+ errors: str = "replace", stress: bool = False) -> str:
218
233
  """Convert an IPA string to a space-separated ARPABET sequence.
219
234
 
220
- Matches longest IPA symbol first. ``errors`` selects the unknown-symbol
221
- policy (default ``"replace"``, preserving the historical behavior of
222
- substituting *unknown*, ``"?"``); ``unknown=""`` still drops silently.
223
- Combining marks and modifier letters qualify the preceding phoneme and
224
- are always dropped rather than treated as unknowns.
235
+ Matches longest IPA symbol first. With ``stress=True``, IPA stress
236
+ marks (``ˈ``/``ˌ``) preceding a vowel-class symbol are emitted as that
237
+ vowel's stress digit (``1``/``2``) and unmarked vowels receive ``0``
238
+ the reverse of :func:`arpa_to_ipa`'s ``stress=True`` convention.
239
+ ``errors`` selects the unknown-symbol policy (default ``"replace"``,
240
+ preserving the historical behavior of substituting *unknown*, ``"?"``);
241
+ ``unknown=""`` still drops silently. Combining marks and modifier
242
+ letters qualify the preceding phoneme and are always dropped rather
243
+ than treated as unknowns.
225
244
 
226
245
  Examples
227
246
  --------
@@ -231,11 +250,25 @@ def ipa_to_arpa(ipa_string: str, unknown: str = "?",
231
250
  '?'
232
251
  """
233
252
  tokens = []
253
+ pending_stress = ""
234
254
  pos = 0
235
255
  while pos < len(ipa_string):
256
+ if stress and ipa_string[pos] in _STRESS_MARKS:
257
+ pending_stress = _STRESS_MARKS[ipa_string[pos]]
258
+ pos += 1
259
+ continue
236
260
  m = _IPA_ARPA_RE.match(ipa_string, pos)
237
261
  if m:
238
- tokens.append(_IPA_TO_ARPA[m.group(0)])
262
+ tok = _IPA_TO_ARPA[m.group(0)]
263
+ if stress and tok in _ARPA_VOWELS:
264
+ # CMUdict spells schwa as AH0 (it has no AX), so stress-aware
265
+ # output uses AH for exact round-trips; extended-ARPABET AX
266
+ # input still normalizes to AH0 (documented residue).
267
+ if m.group(0) == "ə":
268
+ tok = "AH"
269
+ tok += pending_stress or "0"
270
+ pending_stress = ""
271
+ tokens.append(tok)
239
272
  pos = m.end()
240
273
  else:
241
274
  ch = ipa_string[pos]
@@ -652,6 +652,34 @@ _LANG_TO_SCRIPT: dict[str, str] = {
652
652
  "rw": "Latn", # Kinyarwanda
653
653
  "tk": "Latn", # Turkmen (Latin since 1993)
654
654
  "eo": "Latn", # Esperanto
655
+ # Languages with no ISO 639-1 code (keyed by their 639-3 code directly)
656
+ "kjh": "Cyrl", # Khakas
657
+ "xal": "Cyrl", # Kalmyk (Cyrillic since 1938)
658
+ "kbd": "Cyrl", # Kabardian
659
+ "erz": "Cyrl", # Erzya
660
+ "mdf": "Cyrl", # Moksha
661
+ }
662
+
663
+ # ISO 639-2/3 (incl. bibliographic variants) -> the 639-1 key used above.
664
+ # Reference: Library of Congress ISO 639-2 code list.
665
+ _ISO639_ALIASES: dict[str, str] = {
666
+ "rus": "ru", "ukr": "uk", "bel": "be", "bul": "bg", "srp": "sr",
667
+ "mkd": "mk", "mac": "mk", "kaz": "kk", "kir": "ky", "tgk": "tg",
668
+ "mon": "mn", "tat": "tt", "bak": "ba", "chv": "cv", "oss": "os",
669
+ "che": "ce", "hye": "hy", "arm": "hy", "kat": "ka", "geo": "ka",
670
+ "aze": "az", "uzb": "uz", "tuk": "tk", "eng": "en", "deu": "de",
671
+ "ger": "de", "fra": "fr", "fre": "fr", "spa": "es", "por": "pt",
672
+ "ita": "it", "nld": "nl", "dut": "nl", "ell": "el", "gre": "el",
673
+ "ara": "ar", "heb": "he", "zho": "zh", "chi": "zh", "jpn": "ja",
674
+ "kor": "ko", "tur": "tr", "fas": "fa", "per": "fa", "urd": "ur",
675
+ "hin": "hi", "tha": "th", "vie": "vi", "pol": "pl", "ces": "cs",
676
+ "cze": "cs", "slk": "sk", "slo": "sk", "ron": "ro", "rum": "ro",
677
+ "hun": "hu", "fin": "fi", "swe": "sv", "nor": "no", "dan": "da",
678
+ "isl": "is", "ice": "is", "mya": "my", "bur": "my", "khm": "km",
679
+ "lao": "lo", "amh": "am", "tam": "ta", "tel": "te", "ben": "bn",
680
+ "guj": "gu", "kan": "kn", "mal": "ml", "mar": "mr", "pan": "pa",
681
+ "sin": "si", "nep": "ne", "eus": "eu", "baq": "eu", "cat": "ca",
682
+ "glg": "gl", "cym": "cy", "wel": "cy", "gle": "ga", "epo": "eo",
655
683
  }
656
684
 
657
685
 
@@ -674,14 +702,21 @@ def lang_to_script(lang: str) -> Optional[str]:
674
702
  'Latn'
675
703
  """
676
704
  subtags = lang.replace("_", "-").split("-")
677
- # An explicit script subtag (4 letters, e.g. "Latn") overrides the
678
- # language's default script.
705
+ # An explicit script subtag overrides the language's default script:
706
+ # 4-letter ISO-15924 codes ("sr-Latn"), and the widespread informal
707
+ # 3-letter forms "cyr"/"lat" ("uzb_cyr", "aze_lat").
679
708
  for sub in subtags[1:]:
709
+ low = sub.lower()
680
710
  if len(sub) == 4 and sub.isalpha():
681
711
  resolved = normalize_script_tag(sub)
682
712
  if resolved is not None:
683
713
  return resolved
684
- return _LANG_TO_SCRIPT.get(subtags[0].lower())
714
+ elif low in ("cyr", "lat"):
715
+ return "Cyrl" if low == "cyr" else "Latn"
716
+ key = subtags[0].lower()
717
+ if key in _LANG_TO_SCRIPT:
718
+ return _LANG_TO_SCRIPT[key]
719
+ return _LANG_TO_SCRIPT.get(_ISO639_ALIASES.get(key, ""))
685
720
 
686
721
 
687
722
  # ---------------------------------------------------------------------------
@@ -2,7 +2,7 @@
2
2
  VERSION_MAJOR = 0
3
3
  VERSION_MINOR = 0
4
4
  VERSION_BUILD = 3
5
- VERSION_ALPHA = 9
5
+ VERSION_ALPHA = 11
6
6
  # END_VERSION_BLOCK
7
7
 
8
8
  VERSION_STR = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.3a9
3
+ Version: 0.0.3a11
4
4
  Summary: Zero-dependency script & phoneme-notation core — ISO-15924 detection & metadata, IPA↔ARPABET/X-SAMPA/Lexique/Kirshenbaum/Cotovía/RFE, Buckwalter↔Arabic, Hangul→jamo, kana
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/TigreGotico/scriptconv
@@ -210,7 +210,7 @@ below describes the per-converter default.
210
210
 
211
211
  | Notation | `to_ipa` → `from_ipa` round-trip | `from_ipa` → `to_ipa` round-trip | Unknown-token behaviour |
212
212
  |----------|----------------------------------|----------------------------------|-------------------------|
213
- | **ARPABET** | Exact for base symbols; **stress digits are dropped** and `AH0`↔`AX` (schwa) is not distinguished | **Lossy** — ARPABET is an English-only inventory, so any IPA symbol outside it becomes the *unknown* placeholder | `arpa_to_ipa`: passed through unchanged. `ipa_to_arpa`: diacritics and suprasegmentals (combining marks, length/stress modifiers) are dropped; other out-of-inventory symbols are replaced with `?` by default (`unknown=` param; `unknown=""` drops) |
213
+ | **ARPABET** | **Lossless with `stress=True`** (digits ↔ IPA `ˈ`/`ˌ` before the stressed vowel; residues: extended-ARPABET `AX` normalises to CMUdict's `AH0` spelling, and `AH0 R` fuses to the r-colored `AXR0` — stable from the IPA side). Default `stress=False` drops digits and merges `AH0`↔`AX` | **Lossy** — ARPABET is an English-only inventory, so any IPA symbol outside it becomes the *unknown* placeholder | `arpa_to_ipa`: passed through unchanged. `ipa_to_arpa`: diacritics and suprasegmentals are dropped; other out-of-inventory symbols follow `errors=` (default `"replace"` → `?`) |
214
214
  | **X-SAMPA** | Exact for all canonical symbols | Exact except aliases (`f\`→ɸ, `&`→æ) normalise to their canonical spelling | Passed through unchanged |
215
215
  | **Buckwalter ↔ Arabic** | Exact (the `^` shadda alias normalises to canonical `~`; precomposed lam-alef ligatures decompose to two chars, visually identical) | Exact | Passed through unchanged |
216
216
  | **Lexique ↔ IPA** | Exact except the `°`/`3` schwa pair (both → `ə`; reverse always → `°`) | Exact | Passed through unchanged |
@@ -20,6 +20,7 @@ scriptconv.egg-info/requires.txt
20
20
  scriptconv.egg-info/top_level.txt
21
21
  scriptconv/data/__init__.py
22
22
  scriptconv/data/cangjie5_tc.tsv.gz
23
+ tests/test_arpa_stress.py
23
24
  tests/test_cangjie.py
24
25
  tests/test_cli.py
25
26
  tests/test_conventions.py
@@ -30,4 +31,5 @@ tests/test_notation.py
30
31
  tests/test_readings.py
31
32
  tests/test_readings_zh.py
32
33
  tests/test_scripts.py
34
+ tests/test_scripts_stressonnx_compat.py
33
35
  tests/test_translit.py
@@ -0,0 +1,88 @@
1
+ """ARPABET stress preservation (stress=True) — reversible by construction."""
2
+ import random
3
+ import unittest
4
+
5
+ from scriptconv.notation import _ARPA_BASE, _ARPA_VOWELS, arpa_to_ipa, ipa_to_arpa
6
+
7
+
8
+ class TestStressForward(unittest.TestCase):
9
+ def test_marks_placed_before_stressed_vowel(self):
10
+ self.assertEqual(arpa_to_ipa("HH AH0 L OW1", stress=True), "həlˈoʊ")
11
+ self.assertEqual(arpa_to_ipa("K AE1 T", stress=True), "kˈæt")
12
+ self.assertEqual(arpa_to_ipa("AH2", stress=True), "ˌʌ")
13
+
14
+ def test_unstressed_and_consonants_unmarked(self):
15
+ self.assertEqual(arpa_to_ipa("AH0", stress=True), "ə")
16
+ self.assertEqual(arpa_to_ipa("K T", stress=True), "kt")
17
+
18
+ def test_default_still_strips(self):
19
+ self.assertEqual(arpa_to_ipa("HH AH0 L OW1"), "həloʊ")
20
+
21
+ def test_unknown_token_never_gets_a_mark(self):
22
+ self.assertEqual(arpa_to_ipa("ZZ1", stress=True), "ZZ1")
23
+
24
+
25
+ class TestStressReverse(unittest.TestCase):
26
+ def test_marks_become_digits_unmarked_vowels_get_zero(self):
27
+ self.assertEqual(ipa_to_arpa("həlˈoʊ", stress=True), "HH AH0 L OW1")
28
+ self.assertEqual(ipa_to_arpa("kˈæt", stress=True), "K AE1 T")
29
+ self.assertEqual(ipa_to_arpa("ˌʌ", stress=True), "AH2")
30
+
31
+ def test_schwa_spelled_ah0_like_cmudict(self):
32
+ self.assertEqual(ipa_to_arpa("ə", stress=True), "AH0")
33
+
34
+ def test_default_still_drops_marks(self):
35
+ self.assertEqual(ipa_to_arpa("həlˈoʊ"), "HH AX L OW")
36
+
37
+
38
+ class TestRoundTrip(unittest.TestCase):
39
+ def test_readme_example_exact(self):
40
+ x = "HH AH0 L OW1"
41
+ self.assertEqual(ipa_to_arpa(arpa_to_ipa(x, stress=True), stress=True), x)
42
+
43
+ def test_property_cmudict_legal_sequences(self):
44
+ """Random CMUdict-legal sequences round-trip exactly up to
45
+ IPA-equivalence: token sequences whose IPA concatenation is identical
46
+ (r-colored schwa, affricates, diphthong fusion — contiguous IPA has
47
+ no token boundary) may normalize to the fused spelling, and that
48
+ spelling must itself be IPA-stable. The overwhelming majority
49
+ round-trip byte-exactly."""
50
+ random.seed(20260721)
51
+ cons = [c for c in _ARPA_BASE
52
+ if c not in _ARPA_VOWELS and c not in ("AX", "AXR")]
53
+ vows = [v for v in _ARPA_VOWELS if v not in ("AX", "AXR")]
54
+ exact = fused = 0
55
+ for _ in range(400):
56
+ seq = []
57
+ for _ in range(random.randint(1, 7)):
58
+ if random.random() < 0.5:
59
+ seq.append(random.choice(cons))
60
+ else:
61
+ seq.append(random.choice(vows) + random.choice("012"))
62
+ s = " ".join(seq)
63
+ ipa = arpa_to_ipa(s, stress=True)
64
+ rt = ipa_to_arpa(ipa, stress=True)
65
+ if rt == s:
66
+ exact += 1
67
+ else:
68
+ fused += 1
69
+ # fusion residue must be IPA-stable
70
+ self.assertEqual(arpa_to_ipa(rt, stress=True), ipa, s)
71
+ self.assertGreater(exact, 350)
72
+ self.assertLess(fused, 50)
73
+
74
+ def test_documented_residues(self):
75
+ # extended-ARPABET AX normalizes to CMUdict's AH0 spelling
76
+ self.assertEqual(
77
+ ipa_to_arpa(arpa_to_ipa("AX", stress=True), stress=True), "AH0")
78
+ # schwa+R fuses to the r-colored vowel; stable from the IPA side
79
+ fused = ipa_to_arpa(arpa_to_ipa("AH0 R", stress=True), stress=True)
80
+ self.assertEqual(fused, "AXR0")
81
+ # affricate fusion exists in default mode too (contiguous IPA)
82
+ self.assertEqual(ipa_to_arpa(arpa_to_ipa("T SH")), "CH")
83
+ self.assertEqual(arpa_to_ipa(fused, stress=True),
84
+ arpa_to_ipa("AH0 R", stress=True))
85
+
86
+
87
+ if __name__ == "__main__":
88
+ unittest.main()
@@ -0,0 +1,47 @@
1
+ """Downstream compat: the script tags scriptconv returns are a wire format.
2
+
3
+ stressonnx (accentor.py) maps its supported languages to a private script
4
+ taxonomy that mirrors phoonnx's Alphabet enum. These fixtures pin, key by
5
+ key, that scriptconv resolves every one of those language codes to the
6
+ expected ISO-15924 tag — so stressonnx's migration to scriptconv is a pure
7
+ deletion, and future edits here cannot silently break it.
8
+ """
9
+ import pytest
10
+
11
+ from scriptconv import lang_to_script
12
+
13
+ # stressonnx LANG_SCRIPT, verbatim keys -> the ISO-15924 tag its Script
14
+ # enum value corresponds to (cyrillic->Cyrl, latin->Latn, armenian->Armn,
15
+ # georgian->Geor).
16
+ STRESSONNX_LANGS = {
17
+ "ru": "Cyrl", "ukr": "Cyrl", "bel": "Cyrl", "bel_simple": "Cyrl",
18
+ "kaz": "Cyrl", "tat": "Cyrl", "bak": "Cyrl", "chv": "Cyrl",
19
+ "sah": "Cyrl", "kir": "Cyrl", "kjh": "Cyrl", "tgk": "Cyrl",
20
+ "udm": "Cyrl", "xal": "Cyrl", "kbd": "Cyrl", "erz": "Cyrl",
21
+ "mdf": "Cyrl", "uzb_cyr": "Cyrl", "aze_cyr": "Cyrl",
22
+ "aze_lat": "Latn", "uzb_lat": "Latn",
23
+ "hye": "Armn",
24
+ "kat": "Geor",
25
+ }
26
+
27
+
28
+ @pytest.mark.parametrize("lang,expected", sorted(STRESSONNX_LANGS.items()))
29
+ def test_stressonnx_language_resolves(lang, expected):
30
+ assert lang_to_script(lang) == expected
31
+
32
+
33
+ def test_iso639_bibliographic_variants():
34
+ assert lang_to_script("ger") == lang_to_script("deu") == "Latn"
35
+ assert lang_to_script("gre") == lang_to_script("ell") == "Grek"
36
+ assert lang_to_script("arm") == lang_to_script("hye") == "Armn"
37
+
38
+
39
+ def test_explicit_script_subtag_still_authoritative():
40
+ assert lang_to_script("uzb-Latn") == "Latn"
41
+ assert lang_to_script("sr_lat") == "Latn"
42
+ assert lang_to_script("sr") == "Cyrl"
43
+
44
+
45
+ def test_unknown_language_stays_none():
46
+ assert lang_to_script("zzz") is None
47
+ assert lang_to_script("qqq_cyr") == "Cyrl" # variant subtag wins even unknown-lang
File without changes
File without changes