scriptconv 0.0.4a5__tar.gz → 0.0.4a7__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 (97) hide show
  1. {scriptconv-0.0.4a5/scriptconv.egg-info → scriptconv-0.0.4a7}/PKG-INFO +8 -4
  2. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/README.md +7 -3
  3. scriptconv-0.0.4a7/scriptconv/diacritics.py +367 -0
  4. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/base.py +4 -177
  5. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/registry.py +2 -5
  6. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/version.py +1 -1
  7. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7/scriptconv.egg-info}/PKG-INFO +8 -4
  8. scriptconv-0.0.4a7/tests/test_diacritics.py +296 -0
  9. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_diacritics_graph.py +1 -2
  10. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_phonemizers_base.py +0 -100
  11. scriptconv-0.0.4a5/scriptconv/diacritics.py +0 -162
  12. scriptconv-0.0.4a5/tests/test_diacritics.py +0 -90
  13. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/LICENSE +0 -0
  14. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/pyproject.toml +0 -0
  15. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/requirements.txt +0 -0
  16. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/__init__.py +0 -0
  17. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/__main__.py +0 -0
  18. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/cangjie.py +0 -0
  19. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/conventions.py +0 -0
  20. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/data/__init__.py +0 -0
  21. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
  22. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/graph.py +0 -0
  23. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/notation.py +0 -0
  24. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/__init__.py +0 -0
  25. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/__init__.py +0 -0
  26. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/bw2ipa.py +0 -0
  27. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/hangul2ipa.py +0 -0
  28. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/aspiration.csv +0 -0
  29. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/assimilation.csv +0 -0
  30. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/double_coda.csv +0 -0
  31. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/hanja.tsv +0 -0
  32. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/ipa.csv +0 -0
  33. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/neutralization.csv +0 -0
  34. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/tensification.csv +0 -0
  35. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/ko_tables/yale.csv +0 -0
  36. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/__init__.py +0 -0
  37. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/codeswitch.py +0 -0
  38. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/diacritize.py +0 -0
  39. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/dialectal.py +0 -0
  40. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/english_g2p.py +0 -0
  41. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/espeak.py +0 -0
  42. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/frontend.py +0 -0
  43. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/levantine_g2p.py +0 -0
  44. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/normalize.py +0 -0
  45. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/shami/phoneme_inventory.py +0 -0
  46. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_thirdparty/zh_num.py +0 -0
  47. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/__init__.py +0 -0
  48. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/kog2p/LICENSE.md +0 -0
  49. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/kog2p/__init__.py +0 -0
  50. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/kog2p/rulebook.txt +0 -0
  51. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/LICENSE.md +0 -0
  52. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/__init__.py +0 -0
  53. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/buck/__init__.py +0 -0
  54. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/buck/phonetise_buckwalter.py +0 -0
  55. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/buck/symbols.py +0 -0
  56. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/buck/tokenization.py +0 -0
  57. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/num2words.py +0 -0
  58. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/_vendored/mantoq/unicode_symbol2label.py +0 -0
  59. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/ar.py +0 -0
  60. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/en.py +0 -0
  61. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/enums.py +0 -0
  62. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/eu.py +0 -0
  63. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/fa.py +0 -0
  64. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/gl.py +0 -0
  65. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/he.py +0 -0
  66. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/ja.py +0 -0
  67. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/ko.py +0 -0
  68. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/mul.py +0 -0
  69. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/mwl.py +0 -0
  70. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/o2ipa.py +0 -0
  71. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/pt.py +0 -0
  72. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/shami.py +0 -0
  73. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/vi.py +0 -0
  74. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/phonemizers/zh.py +0 -0
  75. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/py.typed +0 -0
  76. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/readings.py +0 -0
  77. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/scripts.py +0 -0
  78. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv/translit.py +0 -0
  79. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv.egg-info/SOURCES.txt +0 -0
  80. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv.egg-info/dependency_links.txt +0 -0
  81. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv.egg-info/requires.txt +0 -0
  82. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/scriptconv.egg-info/top_level.txt +0 -0
  83. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/setup.cfg +0 -0
  84. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_arpa_stress.py +0 -0
  85. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_cangjie.py +0 -0
  86. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_cli.py +0 -0
  87. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_conventions.py +0 -0
  88. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_errors_policy.py +0 -0
  89. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_examples.py +0 -0
  90. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_graph.py +0 -0
  91. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_notation.py +0 -0
  92. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_phonemizers_cjk_ar.py +0 -0
  93. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_readings.py +0 -0
  94. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_readings_zh.py +0 -0
  95. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_scripts.py +0 -0
  96. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_scripts_stressonnx_compat.py +0 -0
  97. {scriptconv-0.0.4a5 → scriptconv-0.0.4a7}/tests/test_translit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.4a5
3
+ Version: 0.0.4a7
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
@@ -373,9 +373,13 @@ Two design points worth knowing:
373
373
  phonemizing; that needs language resources scriptconv doesn't ship. Pass
374
374
  `normalizer=` (a `(text, lang) -> str` callable) to run yours inside the
375
375
  pipeline; without it, text is phonemized as-is.
376
- - **Model-backed engines never download.** ByT5/Charsiu require an explicit
377
- local `model=` path; resolving and caching model files is the caller's
378
- concern.
376
+ - **Large/licensed model-backed engines never download.** ByT5/Charsiu
377
+ require an explicit local `model=` path; resolving and caching those model
378
+ files is the caller's concern. Small, known-good, unencumbered models are
379
+ the exception: the Hebrew phonikud diacritizer auto-provisions its ONNX
380
+ model to a cache dir on first use (`phonikud_model=` still overrides it
381
+ with a path or callable; cache location via `SCRIPTCONV_CACHE`/
382
+ `XDG_CACHE_HOME`).
379
383
 
380
384
  **Pre-G2P disambiguation.** `add_diacritics(text, lang, model=None)` restores
381
385
  information ordinary orthography omits but a G2P needs, before phonemization:
@@ -246,9 +246,13 @@ Two design points worth knowing:
246
246
  phonemizing; that needs language resources scriptconv doesn't ship. Pass
247
247
  `normalizer=` (a `(text, lang) -> str` callable) to run yours inside the
248
248
  pipeline; without it, text is phonemized as-is.
249
- - **Model-backed engines never download.** ByT5/Charsiu require an explicit
250
- local `model=` path; resolving and caching model files is the caller's
251
- concern.
249
+ - **Large/licensed model-backed engines never download.** ByT5/Charsiu
250
+ require an explicit local `model=` path; resolving and caching those model
251
+ files is the caller's concern. Small, known-good, unencumbered models are
252
+ the exception: the Hebrew phonikud diacritizer auto-provisions its ONNX
253
+ model to a cache dir on first use (`phonikud_model=` still overrides it
254
+ with a path or callable; cache location via `SCRIPTCONV_CACHE`/
255
+ `XDG_CACHE_HOME`).
252
256
 
253
257
  **Pre-G2P disambiguation.** `add_diacritics(text, lang, model=None)` restores
254
258
  information ordinary orthography omits but a G2P needs, before phonemization:
@@ -0,0 +1,367 @@
1
+ """Diacritization: mechanism, dispatch, and graph extension.
2
+
3
+ This module OWNS diacritization — lang→backend routing, lazy/cached backend
4
+ loading (phonikud, text2tashkeel, stressonnx, bifonia), and the overlay-strip
5
+ helpers. :mod:`scriptconv.phonemizers` (``BasePhonemizer`` and subclasses)
6
+ knows nothing about diacritics: phonemization (orthography → sound) and
7
+ diacritization (a text → text graph transform disambiguating pronunciation
8
+ before G2P) are separate concerns.
9
+
10
+ Diacritization (Arabic tashkeel, Hebrew niqqud, East-Slavic/Turkic/Caucasian
11
+ word stress, European-Portuguese homograph sense marks) is, architecturally,
12
+ just another transform between text representations: it maps the ``"text"``
13
+ node to a lang-contextual ``"text-diacritized"`` node. Like
14
+ :func:`scriptconv.phonemizers.register` it is opt-in — ``DEFAULT_GRAPH`` stays
15
+ free of it until a caller extends a graph::
16
+
17
+ from scriptconv.graph import DEFAULT_GRAPH
18
+ from scriptconv import diacritics, phonemizers
19
+ g = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
20
+
21
+ g.convert("замок стоит на горе", "text", "text-diacritized", lang="ru")
22
+ # 'за́мок сто́ит на горе́' — just the diacritized text
23
+
24
+ g.convert("Tenho muita sede hoje.", "text", "text-diacritized", lang="pt")
25
+ # 'Tenho muita sêde hoje.'
26
+
27
+ Routing ``"text" -> "ipa"`` still takes the direct phonemization edge by
28
+ default: the diacritization edge is model-based (``lossless=False``), so it
29
+ never out-prices a direct phonemization, and enabling this extension does not
30
+ silently change phonemization output. The diacritized route is taken only
31
+ when a caller asks for ``"text-diacritized"`` explicitly.
32
+
33
+ A future per-engine *stance* would push this further into topology rather than
34
+ a flag: an engine that *requires* vocalized input would carry only a
35
+ ``"text-diacritized" -> "ipa"`` edge (forcing the detour), one that *forbids*
36
+ diacritics would carry only ``"text" -> "ipa"`` (making the detour
37
+ unroutable), and a tolerant engine (e.g. arbtok, which self-vocalizes) carries
38
+ both. The strip direction (``"text-diacritized" -> "text"``) is registered too, but
39
+ gated to languages whose marks are removable *overlay* diacritics —
40
+ Arabic/Hebrew vocalization and East-Slavic/Turkic/Caucasian stress. Strip
41
+ removes only the specific overlay codepoints each backend adds (combining
42
+ acute/grave for stress, tashkeel for Arabic, niqqud for Hebrew) — never a
43
+ blanket combining-mark filter — so precomposed native letters survive
44
+ (Cyrillic й/ё, Latvian macrons, Azerbaijani ç/ö, Arabic hamza carriers أ إ آ ؤ
45
+ ئ). The gate uses exact primary-subtag matching, so Aragonese (``arg``),
46
+ Herero (``her``), Mapudungun (``arn``) etc. are never misread as Arabic/Hebrew.
47
+ Languages whose diacritics are native orthography (European Portuguese, via
48
+ bifonia) refuse the strip with :class:`ValueError` instead of silently
49
+ corrupting the spelling (``café`` must never become ``cafe``). This split
50
+ tracks which backend/model produced the marks: overlay backends (phonikud,
51
+ tashkeel, stressonnx) are strippable; spelling-integral backends (bifonia) are
52
+ not.
53
+ """
54
+ import os
55
+ import tempfile
56
+ import urllib.request
57
+ from pathlib import Path
58
+ from typing import Optional
59
+
60
+ from scriptconv.graph import Edge
61
+ from scriptconv.phonemizers.base import _primary_subtag
62
+
63
+ #: The lang-contextual node produced by diacritization. Like ``"text"`` it is
64
+ #: meaningful only with ``lang=`` context and exists only in opted-in graphs.
65
+ DIACRITIZED = "text-diacritized"
66
+
67
+ # Across East Slavic, Bulgarian/Macedonian/Slovene, Latvian, Armenian,
68
+ # Georgian, and several Turkic/Caucasian languages, lexical word stress is
69
+ # free (not fixed to a syllable) and ordinary orthography leaves it unwritten
70
+ # or under-marked. The clearest case is East Slavic: stress is also mobile
71
+ # (it shifts between forms of the same word) and unstressed vowels *reduce*
72
+ # — Russian unstressed "о" surfaces as [ɐ] or [ə] depending on distance from
73
+ # the stress, not [o] — so a wrong or missing mark there corrupts the vowel
74
+ # quality of the whole word, not just its prosody. Other families in this set
75
+ # don't necessarily reduce vowels, but still need the mark for correct stress
76
+ # placement and prosody. stressonnx restores it as a combining acute (U+0301)
77
+ # after the stressed vowel, covering 26 BCP-47 tags across these families
78
+ # (24 primary subtags; Azerbaijani and Uzbek each have Cyrillic/Latin script
79
+ # variants routed by the full tag).
80
+ STRESS_LANGS = {
81
+ "az", "ba", "be", "bg", "cv", "hy", "ka", "kbd", "kjh", "kk", "ky", "lv",
82
+ "mdf", "mk", "myv", "ru", "sah", "sl", "tg", "tt", "udm", "uk", "uz", "xal",
83
+ }
84
+
85
+
86
+ def _is_european_portuguese(lang: str) -> bool:
87
+ """True for European Portuguese (``pt`` or a ``pt-PT`` region tag).
88
+
89
+ False for Brazilian Portuguese (``pt-BR``) and everything else — the two
90
+ varieties' vowel systems differ, and bifonia's open/closed diacritics are
91
+ only valid for European Portuguese phonology.
92
+ """
93
+ norm = lang.lower().replace("_", "-")
94
+ return norm == "pt" or norm == "pt-pt"
95
+
96
+
97
+ def _diacritizer_family(lang: str) -> Optional[str]:
98
+ """Which diacritization backend family handles *lang*, or ``None``.
99
+
100
+ Single source of truth for lang→backend routing: both the forward
101
+ dispatch (:func:`diacritize`) and the strip direction (:func:`_overlay_marks`)
102
+ resolve through this, so the two can never disagree about which language
103
+ uses which backend. Returns one of ``"he"`` (niqqud), ``"ar"`` (tashkeel),
104
+ ``"stress"`` (stressonnx), ``"pt"`` (bifonia sense diacritics), or
105
+ ``None``. Uses exact primary-subtag matching (never ``startswith``), so
106
+ Aragonese (``arg``), Herero (``her``) and Mapudungun (``arn``) are never
107
+ misread as Arabic/Hebrew.
108
+ """
109
+ p = _primary_subtag(lang)
110
+ if p == "he":
111
+ return "he"
112
+ if p == "ar":
113
+ return "ar"
114
+ if p in STRESS_LANGS:
115
+ return "stress"
116
+ if _is_european_portuguese(lang):
117
+ return "pt"
118
+ return None
119
+
120
+
121
+ _DEFAULT_DIACRITIZER_MODEL = "rawi-ensemble"
122
+
123
+ _PHONIKUD_CACHE: dict = {} # resolved model path/string -> Phonikud instance
124
+ _TASHKEEL_CACHE: dict = {} # model name -> text2tashkeel Diacritizer
125
+
126
+ # phonikud is a small (~100MB), public, unlicensed-restriction ONNX model, so
127
+ # — unlike the large/licensed models behind mul.py's ByT5/Charsiu backends —
128
+ # scriptconv auto-provisions it: no local path is required unless the caller
129
+ # wants to override the cache (e.g. an air-gapped host).
130
+ _PHONIKUD_URL = "https://huggingface.co/thewh1teagle/phonikud-onnx/resolve/main/phonikud-1.0.int8.onnx"
131
+
132
+
133
+ def _default_phonikud_model() -> str:
134
+ """Resolve (downloading and caching on first use if needed) the path to
135
+ the default phonikud ONNX model.
136
+
137
+ Cache directory: ``<base>/scriptconv/phonikud`` where ``<base>`` is
138
+ ``$SCRIPTCONV_CACHE`` if set, else ``$XDG_CACHE_HOME`` (default
139
+ ``~/.cache``) — i.e. ``~/.cache/scriptconv/phonikud`` by default. The
140
+ download is written to a temp file
141
+ in the same directory and atomically moved into place via
142
+ :func:`os.replace`, so a failed or interrupted download never leaves a
143
+ partial file at the destination path.
144
+ """
145
+ base = os.environ.get("SCRIPTCONV_CACHE") or os.environ.get(
146
+ "XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
147
+ cache_dir = Path(base) / "scriptconv" / "phonikud"
148
+ dest = cache_dir / "phonikud-1.0.int8.onnx"
149
+ if not dest.is_file():
150
+ cache_dir.mkdir(parents=True, exist_ok=True)
151
+ fd, tmp_path = tempfile.mkstemp(dir=str(cache_dir), prefix=".phonikud-", suffix=".tmp")
152
+ try:
153
+ with os.fdopen(fd, "wb") as tmp_f, urllib.request.urlopen(_PHONIKUD_URL) as resp:
154
+ while True:
155
+ chunk = resp.read(1024 * 1024)
156
+ if not chunk:
157
+ break
158
+ tmp_f.write(chunk)
159
+ os.replace(tmp_path, dest)
160
+ except BaseException:
161
+ if os.path.exists(tmp_path):
162
+ os.remove(tmp_path)
163
+ raise
164
+ return str(dest)
165
+
166
+
167
+ def _phonikud(phonikud_model=None):
168
+ """Lazily build (and cache) the phonikud Phonikud instance used for Hebrew.
169
+
170
+ ``phonikud_model`` is optional: a local path to a phonikud ONNX model, or
171
+ a zero-arg callable resolving one lazily. When omitted, scriptconv
172
+ auto-provisions the small public phonikud model, downloading it once into
173
+ a cache dir (see :func:`_default_phonikud_model`) and reusing it on
174
+ subsequent calls. Pass an explicit path/callable to override — e.g. to
175
+ point at a model already on disk, or on an air-gapped host. Install
176
+ phonikud-onnx with ``pip install scriptconv[he]`` (or ``pip install
177
+ phonikud-onnx``)."""
178
+ model = phonikud_model() if callable(phonikud_model) else phonikud_model
179
+ if not model:
180
+ model = _default_phonikud_model()
181
+ if model not in _PHONIKUD_CACHE:
182
+ try:
183
+ from phonikud_onnx import Phonikud
184
+ except ImportError:
185
+ raise ImportError(
186
+ "Hebrew diacritization needs phonikud-onnx — install "
187
+ "with `pip install scriptconv[he]`") from None
188
+ _PHONIKUD_CACHE[model] = Phonikud(model)
189
+ return _PHONIKUD_CACHE[model]
190
+
191
+
192
+ def _tashkeel(model: Optional[str] = None):
193
+ """Lazily build (and cache) the text2tashkeel Diacritizer used for Arabic.
194
+
195
+ text2tashkeel is a dependency of the ``[ar]`` extra; it restores hamza and the
196
+ dagger alef in addition to the standard marks. Install with
197
+ ``pip install scriptconv[tashkeel]`` (or ``pip install text2tashkeel``)."""
198
+ model = model or _DEFAULT_DIACRITIZER_MODEL
199
+ if model not in _TASHKEEL_CACHE:
200
+ try:
201
+ from text2tashkeel import Diacritizer
202
+ except ImportError as e:
203
+ raise ImportError(
204
+ "Arabic diacritization requires the text2tashkeel package: "
205
+ "pip install scriptconv[tashkeel] (or pip install text2tashkeel)"
206
+ ) from e
207
+ _TASHKEEL_CACHE[model] = Diacritizer(model)
208
+ return _TASHKEEL_CACHE[model]
209
+
210
+
211
+ def _stress(text: str, lang: str, model: Optional[str] = None) -> str:
212
+ """Word-stress restoration via stressonnx, for the 26 language tags
213
+ it covers (see ``STRESS_LANGS``) — East Slavic, Bulgarian/Macedonian/
214
+ Slovene, Latvian, Armenian, Georgian, and Turkic/Caucasian languages.
215
+
216
+ stressonnx is not on PyPI yet; install straight from source. Install
217
+ with ``pip install scriptconv[stress]`` (or ``pip install
218
+ stressonnx``)."""
219
+ try:
220
+ from stressonnx import stress
221
+ except ImportError as e:
222
+ raise ImportError(
223
+ "stress restoration requires the stressonnx package: "
224
+ "pip install scriptconv[stress] (or pip install stressonnx)"
225
+ ) from e
226
+ return stress(text, lang, model=model)
227
+
228
+
229
+ def _sense_diacritics_pt(text: str) -> str:
230
+ """European-Portuguese heterophonic-homograph sense diacritics via bifonia.
231
+
232
+ Rewrites homographs whose pronunciation depends on meaning (e.g.
233
+ "sede" thirst/closed vs. seat/open) with an explicit open/closed
234
+ vowel diacritic. These are ordinary Portuguese orthographic marks,
235
+ chosen so any downstream G2P — rule-based, neural, or espeak —
236
+ reads them correctly. Install with ``pip install scriptconv[pt]``
237
+ (or ``pip install bifonia``)."""
238
+ try:
239
+ from bifonia import add_extra_diacritics
240
+ except ImportError as e:
241
+ raise ImportError(
242
+ "European-Portuguese sense diacritics require the bifonia package: "
243
+ "pip install scriptconv[pt] (or pip install bifonia)"
244
+ ) from e
245
+ return add_extra_diacritics(text)
246
+
247
+
248
+ def diacritize(text: str, lang: str = "und", model=None,
249
+ phonikud_model=None, diacritizer_model=None, **_) -> str:
250
+ """Add pronunciation-disambiguating diacritics to *text* for *lang*.
251
+
252
+ Four backends, each restoring information ordinary orthography omits but
253
+ downstream G2P needs:
254
+
255
+ - Hebrew (``he``) — niqqud via phonikud. ``phonikud_model=`` is optional:
256
+ omitted, the small public phonikud ONNX model is auto-downloaded and
257
+ cached (``$SCRIPTCONV_CACHE``/``$XDG_CACHE_HOME``); pass a path or
258
+ zero-arg callable to override.
259
+ - Arabic (``ar``) — tashkeel via text2tashkeel (``[tashkeel]``,
260
+ ``model=``/``diacritizer_model=``).
261
+ - East Slavic, Bulgarian/Macedonian/Slovene, Latvian, Armenian, Georgian,
262
+ and Turkic/Caucasian languages (``STRESS_LANGS``, 26 stressonnx tags) —
263
+ word stress via stressonnx (``[stress]``); stress is unwritten or
264
+ under-marked in these languages, and in East Slavic unstressed vowels
265
+ also reduce, so a missing mark can corrupt more than prosody.
266
+ - European Portuguese (``pt``/``pt-PT``, never ``pt-BR``) —
267
+ heterophonic-homograph sense diacritics via bifonia (``[pt]``);
268
+ ordinary Portuguese orthographic marks that any downstream G2P reads
269
+ correctly.
270
+
271
+ Unrecognized languages are returned unchanged. Each backend raises
272
+ ``ImportError`` naming its extra when the optional dependency is missing
273
+ — scriptconv never installs anything on the caller's behalf.
274
+ """
275
+ family = _diacritizer_family(lang)
276
+ if family == "he":
277
+ return _phonikud(phonikud_model).add_diacritics(text)
278
+ if family == "ar":
279
+ return _tashkeel(model or diacritizer_model).diacritize(text)
280
+ if family == "stress":
281
+ return _stress(text, lang, model)
282
+ if family == "pt":
283
+ return _sense_diacritics_pt(text)
284
+ return text
285
+
286
+
287
+ # Combining marks each diacritization backend overlays onto the bare text.
288
+ # Strip removes ONLY these codepoints — never a blanket combining-mark filter,
289
+ # which (via NFD) would decompose and destroy precomposed native letters:
290
+ # Cyrillic й/ё, Latvian ī, Azerbaijani ç/ö, and Arabic hamza carriers أ إ آ ؤ ئ.
291
+ _STRESS_MARKS = frozenset({0x0300, 0x0301}) # combining grave / acute
292
+ # U+0300 is included defensively alongside the U+0301 that stressonnx
293
+ # actually emits, in case any backend/locale marks secondary stress with a
294
+ # grave instead of an acute; harmless to strip since native precomposed
295
+ # letters (e.g. Cyrillic й/ё) are unaffected either way.
296
+ _ARABIC_MARKS = frozenset(range(0x064B, 0x0660)) | frozenset({0x0670}) # tashkeel + dagger alef
297
+ _HEBREW_MARKS = (frozenset(range(0x05B0, 0x05BE))
298
+ | frozenset({0x05BF, 0x05C1, 0x05C2, 0x05C4, 0x05C5, 0x05C7})) # niqqud
299
+
300
+
301
+ def _overlay_marks(lang: str) -> "frozenset | None":
302
+ """The overlay codepoints for *lang*'s diacritization backend, or None.
303
+
304
+ Resolves the backend family through :func:`_diacritizer_family` (the shared
305
+ lang→backend routing), so this can never disagree with
306
+ :func:`diacritize` about which language a backend owns.
307
+ Only *overlay* families are strippable: ``"pt"`` (bifonia sense marks are
308
+ native orthography) and ``None`` both return None.
309
+ """
310
+ family = _diacritizer_family(lang)
311
+ if family == "stress":
312
+ return _STRESS_MARKS
313
+ if family == "ar":
314
+ return _ARABIC_MARKS
315
+ if family == "he":
316
+ return _HEBREW_MARKS
317
+ return None
318
+
319
+
320
+ def _supports_strip(lang: str) -> bool:
321
+ """True for languages whose diacritics are removable overlays (stress marks,
322
+ Arabic tashkeel, Hebrew niqqud) rather than native orthography."""
323
+ return _overlay_marks(lang) is not None
324
+
325
+
326
+ def strip_diacritics(text: str, lang: str = "und", **_) -> str:
327
+ """Remove the overlay diacritics *lang*'s backend adds, recovering the bare
328
+ text WITHOUT touching native letters.
329
+
330
+ Removes only the specific overlay codepoints (combining acute/grave for
331
+ stress, tashkeel for Arabic, niqqud for Hebrew) — precomposed native
332
+ letters (Cyrillic й/ё, Latvian ī, Azerbaijani ç, Arabic hamza carriers) are
333
+ left intact. Raises :class:`ValueError` for languages whose diacritics are
334
+ part of the native orthography (e.g. European Portuguese), where any removal
335
+ would corrupt the spelling.
336
+ """
337
+ marks = _overlay_marks(lang)
338
+ if marks is None:
339
+ raise ValueError(
340
+ f"cannot strip diacritics for lang={lang!r}: its diacritics are "
341
+ "part of the native orthography, so removal would corrupt the "
342
+ "spelling. strip is defined only for overlay diacritics "
343
+ "(Arabic/Hebrew vocalization, East-Slavic/Turkic stress).")
344
+ return "".join(c for c in text if ord(c) not in marks)
345
+
346
+
347
+ def register(graph) -> None:
348
+ """Opt-in graph integration: add the diacritize/strip edge pair.
349
+
350
+ ``text -> text-diacritized`` is model-based (``lossless=False``), so a
351
+ direct ``text -> ipa`` phonemization always out-prices the detour —
352
+ enabling this extension is safe and non-invasive. Pair with
353
+ :func:`scriptconv.phonemizers.register` to make
354
+ ``"text-diacritized" -> "ipa"`` reachable.
355
+
356
+ ``text-diacritized -> text`` (:func:`strip_diacritics`) is lossless and
357
+ cheap, but gated: it raises for languages whose diacritics are native
358
+ orthography rather than removable overlays.
359
+ """
360
+ graph.register(
361
+ Edge("text", DIACRITIZED,
362
+ lambda text, lang="und", model=None, **c: diacritize(text, lang, model, **c),
363
+ lossless=False))
364
+ graph.register(
365
+ Edge(DIACRITIZED, "text",
366
+ lambda text, lang="und", **_: strip_diacritics(text, lang),
367
+ lossless=True))
@@ -34,163 +34,25 @@ RawPhonemizedChunks = List[Tuple[str, str, bool]]
34
34
 
35
35
  PhonemizedChunks = list[list[str]]
36
36
 
37
- # Across East Slavic, Bulgarian/Macedonian/Slovene, Latvian, Armenian,
38
- # Georgian, and several Turkic/Caucasian languages, lexical word stress is
39
- # free (not fixed to a syllable) and ordinary orthography leaves it unwritten
40
- # or under-marked. The clearest case is East Slavic: stress is also mobile
41
- # (it shifts between forms of the same word) and unstressed vowels *reduce*
42
- # — Russian unstressed "о" surfaces as [ɐ] or [ə] depending on distance from
43
- # the stress, not [o] — so a wrong or missing mark there corrupts the vowel
44
- # quality of the whole word, not just its prosody. Other families in this set
45
- # don't necessarily reduce vowels, but still need the mark for correct stress
46
- # placement and prosody. stressonnx restores it as a combining acute (U+0301)
47
- # after the stressed vowel, covering 26 BCP-47 tags across these families
48
- # (24 primary subtags; Azerbaijani and Uzbek each have Cyrillic/Latin script
49
- # variants routed by the full tag).
50
- STRESS_LANGS = {
51
- "az", "ba", "be", "bg", "cv", "hy", "ka", "kbd", "kjh", "kk", "ky", "lv",
52
- "mdf", "mk", "myv", "ru", "sah", "sl", "tg", "tt", "udm", "uk", "uz", "xal",
53
- }
54
-
55
37
 
56
38
  def _primary_subtag(lang: str) -> str:
57
39
  """Lowercase, ``_``→``-`` normalized primary language subtag.
58
40
 
59
- Used for exact-match routing (``STRESS_LANGS`` membership) rather than
60
- ``str.startswith``, so e.g. Berber (``ber``) never false-matches
61
- Belarusian (``be``).
41
+ Used for exact-match language routing (e.g. registry lang defaults,
42
+ diacritization backend selection) rather than ``str.startswith``, so
43
+ e.g. Berber (``ber``) never false-matches Belarusian (``be``).
62
44
  """
63
45
  return lang.lower().replace("_", "-").split("-")[0]
64
46
 
65
47
 
66
- def _is_european_portuguese(lang: str) -> bool:
67
- """True for European Portuguese (``pt`` or a ``pt-PT`` region tag).
68
-
69
- False for Brazilian Portuguese (``pt-BR``) and everything else — the two
70
- varieties' vowel systems differ, and bifonia's open/closed diacritics are
71
- only valid for European Portuguese phonology.
72
- """
73
- norm = lang.lower().replace("_", "-")
74
- return norm == "pt" or norm == "pt-pt"
75
-
76
-
77
- def _diacritizer_family(lang: str) -> Optional[str]:
78
- """Which diacritization backend family handles *lang*, or ``None``.
79
-
80
- Single source of truth for lang→backend routing: both the forward
81
- dispatch (:meth:`BasePhonemizer.add_diacritics`) and the strip direction
82
- (:func:`scriptconv.diacritics._overlay_marks`) resolve through this, so the
83
- two can never disagree about which language uses which backend. Returns one
84
- of ``"he"`` (niqqud), ``"ar"`` (tashkeel), ``"stress"`` (stressonnx), ``"pt"``
85
- (bifonia sense diacritics), or ``None``. Uses exact primary-subtag matching
86
- (never ``startswith``), so Aragonese (``arg``), Herero (``her``) and
87
- Mapudungun (``arn``) are never misread as Arabic/Hebrew.
88
- """
89
- p = _primary_subtag(lang)
90
- if p == "he":
91
- return "he"
92
- if p == "ar":
93
- return "ar"
94
- if p in STRESS_LANGS:
95
- return "stress"
96
- if _is_european_portuguese(lang):
97
- return "pt"
98
- return None
99
-
100
-
101
48
  class BasePhonemizer(metaclass=abc.ABCMeta):
102
49
  def __init__(self, alphabet: Alphabet = Alphabet.UNICODE,
103
- diacritizer_model: str = "rawi-ensemble",
104
- normalizer: Optional[Callable[[str, str], str]] = None,
105
- phonikud_model: Optional[str] = None):
50
+ normalizer: Optional[Callable[[str, str], str]] = None):
106
51
  super().__init__()
107
52
  self.alphabet = alphabet
108
53
  # optional (text, lang) -> str hook run before chunking; see module
109
54
  # docstring — scriptconv performs no normalization of its own
110
55
  self.normalizer = normalizer
111
- # local path to a phonikud ONNX model (Hebrew diacritization), or a
112
- # zero-arg callable resolving one lazily; scriptconv never downloads
113
- # — the consumer resolves the file
114
- self.phonikud_model = phonikud_model
115
-
116
- # diacritizer model name, for languages that need one. Arabic uses
117
- # text2tashkeel; the default "rawi-ensemble" restores hamza and the dagger
118
- # alef in addition to the standard marks.
119
- self.diacritizer_model = diacritizer_model
120
- self._phonikud = None # hebrew only
121
- self._tashkeel: dict = {} # model name -> text2tashkeel Diacritizer
122
-
123
- @property
124
- def phonikud(self):
125
- if self._phonikud is None:
126
- model = self.phonikud_model() if callable(self.phonikud_model) \
127
- else self.phonikud_model
128
- if not model:
129
- raise ValueError(
130
- "Hebrew diacritization needs a local phonikud ONNX model: "
131
- "pass phonikud_model=<path> (scriptconv never downloads "
132
- "models; obtain one from the phonikud-onnx release)")
133
- try:
134
- from phonikud_onnx import Phonikud
135
- except ImportError:
136
- raise ImportError(
137
- "Hebrew diacritization needs phonikud-onnx — install "
138
- "with `pip install scriptconv[he]`") from None
139
- self._phonikud = Phonikud(model)
140
- return self._phonikud
141
-
142
- def tashkeel(self, model: Optional[str] = None):
143
- """Lazily build (and cache) the text2tashkeel Diacritizer used for Arabic.
144
-
145
- text2tashkeel is a dependency of the ``[ar]`` extra; it restores hamza and the
146
- dagger alef in addition to the standard marks. Install with
147
- ``pip install scriptconv[tashkeel]`` (or ``pip install text2tashkeel``)."""
148
- model = model or self.diacritizer_model
149
- if model not in self._tashkeel:
150
- try:
151
- from text2tashkeel import Diacritizer
152
- except ImportError as e:
153
- raise ImportError(
154
- "Arabic diacritization requires the text2tashkeel package: "
155
- "pip install scriptconv[tashkeel] (or pip install text2tashkeel)"
156
- ) from e
157
- self._tashkeel[model] = Diacritizer(model)
158
- return self._tashkeel[model]
159
-
160
- def _stress(self, text: str, lang: str, model: Optional[str] = None) -> str:
161
- """Word-stress restoration via stressonnx, for the 26 language tags
162
- it covers (see ``STRESS_LANGS``) — East Slavic, Bulgarian/Macedonian/
163
- Slovene, Latvian, Armenian, Georgian, and Turkic/Caucasian languages.
164
-
165
- stressonnx is not on PyPI yet; install straight from source. Install
166
- with ``pip install scriptconv[stress]`` (or ``pip install
167
- stressonnx``)."""
168
- try:
169
- from stressonnx import stress
170
- except ImportError as e:
171
- raise ImportError(
172
- "stress restoration requires the stressonnx package: "
173
- "pip install scriptconv[stress] (or pip install stressonnx)"
174
- ) from e
175
- return stress(text, lang, model=model)
176
-
177
- def _sense_diacritics_pt(self, text: str) -> str:
178
- """European-Portuguese heterophonic-homograph sense diacritics via bifonia.
179
-
180
- Rewrites homographs whose pronunciation depends on meaning (e.g.
181
- "sede" thirst/closed vs. seat/open) with an explicit open/closed
182
- vowel diacritic. These are ordinary Portuguese orthographic marks,
183
- chosen so any downstream G2P — rule-based, neural, or espeak —
184
- reads them correctly. Install with ``pip install scriptconv[pt]``
185
- (or ``pip install bifonia``)."""
186
- try:
187
- from bifonia import add_extra_diacritics
188
- except ImportError as e:
189
- raise ImportError(
190
- "European-Portuguese sense diacritics require the bifonia package: "
191
- "pip install scriptconv[pt] (or pip install bifonia)"
192
- ) from e
193
- return add_extra_diacritics(text)
194
56
 
195
57
  @abc.abstractmethod
196
58
  def phonemize_string(self, text: str, lang: str) -> str:
@@ -199,41 +61,6 @@ class BasePhonemizer(metaclass=abc.ABCMeta):
199
61
  def phonemize_to_list(self, text: str, lang: str) -> List[str]:
200
62
  return list(self.phonemize_string(text, lang))
201
63
 
202
- def add_diacritics(self, text: str, lang: str,
203
- model: Optional[str] = None) -> str:
204
- """Disambiguate pronunciation before G2P by adding diacritics.
205
-
206
- Four backends, each restoring information ordinary orthography
207
- omits but downstream G2P needs:
208
-
209
- - Hebrew (``he``) — niqqud via phonikud (``phonikud_model=``).
210
- - Arabic (``ar``) — tashkeel via text2tashkeel (``[tashkeel]``).
211
- - East Slavic, Bulgarian/Macedonian/Slovene, Latvian, Armenian,
212
- Georgian, and Turkic/Caucasian languages (``STRESS_LANGS``, 26
213
- stressonnx tags) — word stress via stressonnx (``[stress]``);
214
- stress is unwritten or under-marked in these languages, and in
215
- East Slavic unstressed vowels also reduce, so a missing mark can
216
- corrupt more than prosody.
217
- - European Portuguese (``pt``/``pt-PT``, never ``pt-BR``) —
218
- heterophonic-homograph sense diacritics via bifonia (``[pt]``);
219
- ordinary Portuguese orthographic marks that any downstream G2P
220
- reads correctly.
221
-
222
- Unrecognized languages are returned unchanged. Each backend raises
223
- ``ImportError`` naming its extra when the optional dependency is
224
- missing — scriptconv never installs anything on the caller's behalf.
225
- """
226
- family = _diacritizer_family(lang)
227
- if family == "he":
228
- return self.phonikud.add_diacritics(text)
229
- if family == "ar":
230
- return self.tashkeel(model).diacritize(text)
231
- if family == "stress":
232
- return self._stress(text, lang, model)
233
- if family == "pt":
234
- return self._sense_diacritics_pt(text)
235
- return text
236
-
237
64
  def phonemize(self, text: str, lang: str) -> PhonemizedChunks:
238
65
  # PhonemizedChunks is list[list[str]]; empty text yields no
239
66
  # sentences. (Returning the raw (str, str, bool) tuple form here
@@ -102,10 +102,9 @@ def get_phonemizer(phonemizer: Phonemizer,
102
102
  """
103
103
  phonemizer = Phonemizer(phonemizer)
104
104
  cls = get_phonemizer_class(phonemizer)
105
- # normalizer/phonikud_model are plain BasePhonemizer attributes; set them
106
- # after construction so wrapper __init__ signatures stay untouched
105
+ # normalizer is a plain BasePhonemizer attribute; set it after
106
+ # construction so wrapper __init__ signatures stay untouched
107
107
  normalizer = kwargs.pop("normalizer", None)
108
- phonikud_model = kwargs.pop("phonikud_model", None)
109
108
  if phonemizer in (_P.BYT5, _P.CHARSIU, _P.DEEPPHONEMIZER):
110
109
  inst = cls(model, **kwargs)
111
110
  else:
@@ -124,8 +123,6 @@ def get_phonemizer(phonemizer: Phonemizer,
124
123
  inst = cls(**kwargs)
125
124
  if normalizer is not None:
126
125
  inst.normalizer = normalizer
127
- if phonikud_model is not None:
128
- inst.phonikud_model = phonikud_model
129
126
  return inst
130
127
 
131
128
 
@@ -2,7 +2,7 @@
2
2
  VERSION_MAJOR = 0
3
3
  VERSION_MINOR = 0
4
4
  VERSION_BUILD = 4
5
- VERSION_ALPHA = 5
5
+ VERSION_ALPHA = 7
6
6
  # END_VERSION_BLOCK
7
7
 
8
8
  VERSION_STR = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}"