scriptconv 0.0.4a6__tar.gz → 0.0.4a8__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 (95) hide show
  1. {scriptconv-0.0.4a6/scriptconv.egg-info → scriptconv-0.0.4a8}/PKG-INFO +8 -4
  2. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/README.md +7 -3
  3. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/diacritics.py +80 -24
  4. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/registry.py +7 -3
  5. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/version.py +1 -1
  6. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8/scriptconv.egg-info}/PKG-INFO +8 -4
  7. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_diacritics.py +106 -11
  8. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_diacritics_graph.py +46 -0
  9. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/LICENSE +0 -0
  10. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/pyproject.toml +0 -0
  11. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/requirements.txt +0 -0
  12. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/__init__.py +0 -0
  13. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/__main__.py +0 -0
  14. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/cangjie.py +0 -0
  15. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/conventions.py +0 -0
  16. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/data/__init__.py +0 -0
  17. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
  18. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/graph.py +0 -0
  19. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/notation.py +0 -0
  20. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/__init__.py +0 -0
  21. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/__init__.py +0 -0
  22. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/bw2ipa.py +0 -0
  23. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/hangul2ipa.py +0 -0
  24. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/aspiration.csv +0 -0
  25. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/assimilation.csv +0 -0
  26. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/double_coda.csv +0 -0
  27. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/hanja.tsv +0 -0
  28. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/ipa.csv +0 -0
  29. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/neutralization.csv +0 -0
  30. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/tensification.csv +0 -0
  31. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/ko_tables/yale.csv +0 -0
  32. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/__init__.py +0 -0
  33. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/codeswitch.py +0 -0
  34. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/diacritize.py +0 -0
  35. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/dialectal.py +0 -0
  36. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/english_g2p.py +0 -0
  37. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/espeak.py +0 -0
  38. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/frontend.py +0 -0
  39. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/levantine_g2p.py +0 -0
  40. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/normalize.py +0 -0
  41. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/shami/phoneme_inventory.py +0 -0
  42. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_thirdparty/zh_num.py +0 -0
  43. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/__init__.py +0 -0
  44. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/kog2p/LICENSE.md +0 -0
  45. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/kog2p/__init__.py +0 -0
  46. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/kog2p/rulebook.txt +0 -0
  47. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/LICENSE.md +0 -0
  48. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/__init__.py +0 -0
  49. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/buck/__init__.py +0 -0
  50. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/buck/phonetise_buckwalter.py +0 -0
  51. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/buck/symbols.py +0 -0
  52. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/buck/tokenization.py +0 -0
  53. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/num2words.py +0 -0
  54. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/_vendored/mantoq/unicode_symbol2label.py +0 -0
  55. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/ar.py +0 -0
  56. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/base.py +0 -0
  57. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/en.py +0 -0
  58. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/enums.py +0 -0
  59. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/eu.py +0 -0
  60. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/fa.py +0 -0
  61. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/gl.py +0 -0
  62. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/he.py +0 -0
  63. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/ja.py +0 -0
  64. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/ko.py +0 -0
  65. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/mul.py +0 -0
  66. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/mwl.py +0 -0
  67. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/o2ipa.py +0 -0
  68. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/pt.py +0 -0
  69. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/shami.py +0 -0
  70. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/vi.py +0 -0
  71. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/phonemizers/zh.py +0 -0
  72. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/py.typed +0 -0
  73. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/readings.py +0 -0
  74. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/scripts.py +0 -0
  75. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv/translit.py +0 -0
  76. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv.egg-info/SOURCES.txt +0 -0
  77. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv.egg-info/dependency_links.txt +0 -0
  78. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv.egg-info/requires.txt +0 -0
  79. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/scriptconv.egg-info/top_level.txt +0 -0
  80. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/setup.cfg +0 -0
  81. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_arpa_stress.py +0 -0
  82. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_cangjie.py +0 -0
  83. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_cli.py +0 -0
  84. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_conventions.py +0 -0
  85. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_errors_policy.py +0 -0
  86. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_examples.py +0 -0
  87. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_graph.py +0 -0
  88. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_notation.py +0 -0
  89. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_phonemizers_base.py +0 -0
  90. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_phonemizers_cjk_ar.py +0 -0
  91. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_readings.py +0 -0
  92. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_readings_zh.py +0 -0
  93. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_scripts.py +0 -0
  94. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_scripts_stressonnx_compat.py +0 -0
  95. {scriptconv-0.0.4a6 → scriptconv-0.0.4a8}/tests/test_translit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.4a6
3
+ Version: 0.0.4a8
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:
@@ -51,6 +51,10 @@ tracks which backend/model produced the marks: overlay backends (phonikud,
51
51
  tashkeel, stressonnx) are strippable; spelling-integral backends (bifonia) are
52
52
  not.
53
53
  """
54
+ import os
55
+ import tempfile
56
+ import urllib.request
57
+ from pathlib import Path
54
58
  from typing import Optional
55
59
 
56
60
  from scriptconv.graph import Edge
@@ -119,20 +123,61 @@ _DEFAULT_DIACRITIZER_MODEL = "rawi-ensemble"
119
123
  _PHONIKUD_CACHE: dict = {} # resolved model path/string -> Phonikud instance
120
124
  _TASHKEEL_CACHE: dict = {} # model name -> text2tashkeel Diacritizer
121
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"
122
131
 
123
- def _phonikud(phonikud_model):
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):
124
168
  """Lazily build (and cache) the phonikud Phonikud instance used for Hebrew.
125
169
 
126
- ``phonikud_model`` is a local path to a phonikud ONNX model, or a
127
- zero-arg callable resolving one lazily; scriptconv never downloads —
128
- the consumer resolves the file. Install with ``pip install
129
- scriptconv[he]`` (or ``pip install phonikud-onnx``)."""
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``)."""
130
178
  model = phonikud_model() if callable(phonikud_model) else phonikud_model
131
179
  if not model:
132
- raise ValueError(
133
- "Hebrew diacritization needs a local phonikud ONNX model: "
134
- "pass phonikud_model=<path> (scriptconv never downloads "
135
- "models; obtain one from the phonikud-onnx release)")
180
+ model = _default_phonikud_model()
136
181
  if model not in _PHONIKUD_CACHE:
137
182
  try:
138
183
  from phonikud_onnx import Phonikud
@@ -200,21 +245,27 @@ def _sense_diacritics_pt(text: str) -> str:
200
245
  return add_extra_diacritics(text)
201
246
 
202
247
 
203
- def diacritize(text: str, lang: str = "und", model=None,
204
- phonikud_model=None, diacritizer_model=None, **_) -> str:
248
+ def diacritize(text: str, lang: str = "und", diacritizer_model=None,
249
+ **kwargs) -> str:
205
250
  """Add pronunciation-disambiguating diacritics to *text* for *lang*.
206
251
 
207
- Four backends, each restoring information ordinary orthography omits but
208
- downstream G2P needs:
209
-
210
- - Hebrew (``he``) — niqqud via phonikud (``phonikud_model=``).
211
- - Arabic (``ar``) — tashkeel via text2tashkeel (``[tashkeel]``,
212
- ``model=``/``diacritizer_model=``).
252
+ ``diacritizer_model=`` is the one model knob for the diacritizer edge — the
253
+ model for whichever backend the language routes to (there is one such knob,
254
+ parallel to ``phonemizer_model=`` on the ``text -> ipa`` phonemizer edge):
255
+
256
+ - Hebrew (``he``) — niqqud via phonikud; ``diacritizer_model`` is the
257
+ phonikud ONNX path (or a zero-arg callable). Optional: when omitted the
258
+ small public phonikud model is auto-downloaded and cached
259
+ (``$SCRIPTCONV_CACHE``/``$XDG_CACHE_HOME``).
260
+ - Arabic (``ar``) — tashkeel via text2tashkeel (``[tashkeel]``);
261
+ ``diacritizer_model`` is the text2tashkeel model name (defaults to
262
+ ``rawi-ensemble``).
213
263
  - East Slavic, Bulgarian/Macedonian/Slovene, Latvian, Armenian, Georgian,
214
264
  and Turkic/Caucasian languages (``STRESS_LANGS``, 26 stressonnx tags) —
215
- word stress via stressonnx (``[stress]``); stress is unwritten or
216
- under-marked in these languages, and in East Slavic unstressed vowels
217
- also reduce, so a missing mark can corrupt more than prosody.
265
+ word stress via stressonnx (``[stress]``); ``diacritizer_model`` is the
266
+ stressonnx model. Stress is unwritten or under-marked in these languages,
267
+ and in East Slavic unstressed vowels also reduce, so a missing mark can
268
+ corrupt more than prosody.
218
269
  - European Portuguese (``pt``/``pt-PT``, never ``pt-BR``) —
219
270
  heterophonic-homograph sense diacritics via bifonia (``[pt]``);
220
271
  ordinary Portuguese orthographic marks that any downstream G2P reads
@@ -224,13 +275,17 @@ def diacritize(text: str, lang: str = "und", model=None,
224
275
  ``ImportError`` naming its extra when the optional dependency is missing
225
276
  — scriptconv never installs anything on the caller's behalf.
226
277
  """
278
+ # one knob for the diacritizer edge; fold the pre-unification kwargs so
279
+ # older callers keep working (phonikud_model was the Hebrew-only name).
280
+ diacritizer_model = (diacritizer_model or kwargs.get("phonikud_model")
281
+ or kwargs.get("model"))
227
282
  family = _diacritizer_family(lang)
228
283
  if family == "he":
229
- return _phonikud(phonikud_model).add_diacritics(text)
284
+ return _phonikud(diacritizer_model).add_diacritics(text)
230
285
  if family == "ar":
231
- return _tashkeel(model or diacritizer_model).diacritize(text)
286
+ return _tashkeel(diacritizer_model).diacritize(text)
232
287
  if family == "stress":
233
- return _stress(text, lang, model)
288
+ return _stress(text, lang, diacritizer_model)
234
289
  if family == "pt":
235
290
  return _sense_diacritics_pt(text)
236
291
  return text
@@ -311,7 +366,8 @@ def register(graph) -> None:
311
366
  """
312
367
  graph.register(
313
368
  Edge("text", DIACRITIZED,
314
- lambda text, lang="und", model=None, **c: diacritize(text, lang, model, **c),
369
+ lambda text, lang="und", diacritizer_model=None, **c:
370
+ diacritize(text, lang, diacritizer_model=diacritizer_model, **c),
315
371
  lossless=False))
316
372
  graph.register(
317
373
  Edge(DIACRITIZED, "text",
@@ -231,9 +231,13 @@ def register(graph) -> None:
231
231
  from scriptconv.graph import Edge
232
232
 
233
233
  def _text_to_ipa(text: str, lang: str = "und",
234
- override: Optional[Phonemizer] = None, **_):
235
- return phonemizer_for_lang(lang, Alphabet.IPA,
236
- override).phonemize_string(text, lang)
234
+ override: Optional[Phonemizer] = None,
235
+ phonemizer_model: Optional[str] = None, **_):
236
+ # phonemizer_model is the model knob for this (phonemizer) edge —
237
+ # forwarded to model-backed phonemizers (ByT5/Charsiu/…); parallel to
238
+ # diacritizer_model on the text -> text-diacritized edge.
239
+ return phonemizer_for_lang(lang, Alphabet.IPA, override,
240
+ phonemizer_model).phonemize_string(text, lang)
237
241
 
238
242
  graph.register(Edge("text", "ipa", _text_to_ipa, lossless=False))
239
243
  graph.register(Edge("text-diacritized", "ipa", _text_to_ipa, lossless=False))
@@ -2,7 +2,7 @@
2
2
  VERSION_MAJOR = 0
3
3
  VERSION_MINOR = 0
4
4
  VERSION_BUILD = 4
5
- VERSION_ALPHA = 6
5
+ VERSION_ALPHA = 8
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.4a6
3
+ Version: 0.0.4a8
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:
@@ -3,6 +3,8 @@
3
3
  No optional backend is required — strip_diacritics/_overlay_marks/_supports_strip
4
4
  never load a phonemizer model.
5
5
  """
6
+ import os
7
+
6
8
  import pytest
7
9
 
8
10
  from scriptconv.diacritics import (
@@ -90,27 +92,120 @@ def test_overlay_marks_none_for_native_orthography():
90
92
  assert _overlay_marks("en") is None
91
93
 
92
94
 
93
- class TestHebrewDiacritizerRequiresLocalModel:
94
- def test_hebrew_diacritizer_requires_local_model(self):
95
- from scriptconv.diacritics import diacritize
96
- with pytest.raises(ValueError) as ctx:
97
- diacritize("שלום", "he")
98
- assert "phonikud_model" in str(ctx.value)
95
+ class TestHebrewDiacritizerAutoProvisions:
96
+ def test_no_model_given_auto_provisions(self, monkeypatch):
97
+ from scriptconv import diacritics
98
+
99
+ monkeypatch.setitem(diacritics._PHONIKUD_CACHE, "/sentinel/path.onnx", object())
100
+ monkeypatch.setattr(diacritics, "_default_phonikud_model",
101
+ lambda: "/sentinel/path.onnx")
102
+ result = diacritics._phonikud(None)
103
+ assert result is diacritics._PHONIKUD_CACHE["/sentinel/path.onnx"]
104
+
105
+ def test_explicit_path_bypasses_provisioner(self, monkeypatch):
106
+ from scriptconv import diacritics
107
+
108
+ calls = []
109
+ monkeypatch.setattr(diacritics, "_default_phonikud_model",
110
+ lambda: calls.append(1) or "/should/not/be/used.onnx")
111
+
112
+ class FakePhonikud:
113
+ def __init__(self, model):
114
+ self.model = model
115
+
116
+ monkeypatch.setattr(diacritics, "_PHONIKUD_CACHE", {})
117
+ import sys
118
+ import types
119
+ fake_mod = types.ModuleType("phonikud_onnx")
120
+ fake_mod.Phonikud = FakePhonikud
121
+ monkeypatch.setitem(sys.modules, "phonikud_onnx", fake_mod)
122
+
123
+ result = diacritics._phonikud("/explicit/path.onnx")
124
+ assert result.model == "/explicit/path.onnx"
125
+ assert calls == [] # provisioner never invoked
99
126
 
100
127
 
101
128
  class TestPhonikudModelResolver:
102
- def test_callable_resolver_invoked_lazily(self):
103
- from scriptconv.diacritics import diacritize
129
+ def test_callable_resolver_invoked_lazily(self, monkeypatch):
130
+ from scriptconv import diacritics
131
+
104
132
  calls = []
105
133
 
106
134
  def resolver():
107
135
  calls.append(1)
108
- return "" # resolves to nothing -> still the explicit ValueError
136
+ return "/from/callable.onnx"
137
+
138
+ class FakePhonikud:
139
+ def __init__(self, model):
140
+ self.model = model
141
+
142
+ monkeypatch.setattr(diacritics, "_PHONIKUD_CACHE", {})
143
+ import sys
144
+ import types
145
+ fake_mod = types.ModuleType("phonikud_onnx")
146
+ fake_mod.Phonikud = FakePhonikud
147
+ monkeypatch.setitem(sys.modules, "phonikud_onnx", fake_mod)
109
148
 
110
149
  assert calls == [] # not resolved at construction
111
- with pytest.raises(ValueError):
112
- diacritize("שלום", "he", phonikud_model=resolver)
150
+ result = diacritics._phonikud(resolver)
113
151
  assert calls == [1]
152
+ assert result.model == "/from/callable.onnx"
153
+
154
+
155
+ class TestDefaultPhonikudModelProvisioning:
156
+ def test_downloads_to_cache_dir_and_returns_path(self, monkeypatch, tmp_path):
157
+ from scriptconv import diacritics
158
+
159
+ monkeypatch.setenv("SCRIPTCONV_CACHE", str(tmp_path))
160
+ calls = []
161
+
162
+ class FakeResponse:
163
+ def __enter__(self):
164
+ return self
165
+
166
+ def __exit__(self, *a):
167
+ return False
168
+
169
+ def read(self, n):
170
+ if calls and calls[-1] == "read-done":
171
+ return b""
172
+ calls.append("read-done")
173
+ return b"dummy-onnx-bytes"
174
+
175
+ def fake_urlopen(url):
176
+ calls.append(("urlopen", url))
177
+ return FakeResponse()
178
+
179
+ monkeypatch.setattr(diacritics.urllib.request, "urlopen", fake_urlopen)
180
+
181
+ path = diacritics._default_phonikud_model()
182
+ assert path == str(tmp_path / "scriptconv" / "phonikud" / "phonikud-1.0.int8.onnx")
183
+ assert os.path.isfile(path)
184
+
185
+ # idempotent: second call must not re-download
186
+ urlopen_calls_before = sum(1 for c in calls if isinstance(c, tuple))
187
+ diacritics._default_phonikud_model()
188
+ urlopen_calls_after = sum(1 for c in calls if isinstance(c, tuple))
189
+ assert urlopen_calls_before == urlopen_calls_after == 1
190
+
191
+ def test_failed_download_leaves_no_partial_file(self, monkeypatch, tmp_path):
192
+ from scriptconv import diacritics
193
+
194
+ monkeypatch.setenv("SCRIPTCONV_CACHE", str(tmp_path))
195
+
196
+ def failing_urlopen(url):
197
+ raise OSError("network unavailable")
198
+
199
+ monkeypatch.setattr(diacritics.urllib.request, "urlopen", failing_urlopen)
200
+
201
+ with pytest.raises(OSError):
202
+ diacritics._default_phonikud_model()
203
+
204
+ dest = tmp_path / "scriptconv" / "phonikud" / "phonikud-1.0.int8.onnx"
205
+ assert not dest.exists()
206
+ cache_dir = tmp_path / "scriptconv" / "phonikud"
207
+ if cache_dir.exists():
208
+ assert list(cache_dir.iterdir()) == []
114
209
 
115
210
 
116
211
  class TestEuropeanPortugueseSenseDiacritics:
@@ -101,3 +101,49 @@ class TestDiacriticsGraphExtension(unittest.TestCase):
101
101
 
102
102
  if __name__ == "__main__":
103
103
  unittest.main()
104
+
105
+
106
+ class TestEdgeModelKeys(unittest.TestCase):
107
+ """The two model-bearing edges each read their own context key: the
108
+ diacritizer edge (text -> text-diacritized) reads ``diacritizer_model``,
109
+ the phonemizer edge (text -> ipa) reads ``phonemizer_model``. Neither picks
110
+ up the other's key."""
111
+
112
+ def setUp(self):
113
+ self.graph = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
114
+
115
+ def test_diacritizer_edge_reads_diacritizer_model(self):
116
+ seen = {}
117
+ backend = types.SimpleNamespace(diacritize=lambda t: t)
118
+
119
+ def _spy(m=None):
120
+ seen["m"] = m
121
+ return backend
122
+
123
+ with mock.patch.object(diacritics, "_tashkeel", _spy):
124
+ self.graph.convert("نص", "text", "text-diacritized", lang="ar",
125
+ diacritizer_model="RAWI-X",
126
+ phonemizer_model="IGNORED")
127
+ self.assertEqual(seen["m"], "RAWI-X")
128
+
129
+ def test_phonemizer_edge_reads_phonemizer_model(self):
130
+ from scriptconv.phonemizers import registry
131
+ cap = {}
132
+
133
+ class _P:
134
+ def phonemize_string(self, t, l):
135
+ return "IPA"
136
+
137
+ def _spy(lang, alphabet, override=None, model=None, **k):
138
+ cap["model"] = model
139
+ return _P()
140
+
141
+ with mock.patch.object(registry, "phonemizer_for_lang", _spy):
142
+ self.graph.convert("hello", "text", "ipa", lang="en",
143
+ phonemizer_model="BYT5-CKPT",
144
+ diacritizer_model="IGNORED")
145
+ self.assertEqual(cap["model"], "BYT5-CKPT")
146
+
147
+
148
+ if __name__ == "__main__":
149
+ unittest.main()
File without changes
File without changes