scriptconv 0.0.4a3__tar.gz → 0.0.4a4__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 (94) hide show
  1. {scriptconv-0.0.4a3/scriptconv.egg-info → scriptconv-0.0.4a4}/PKG-INFO +12 -1
  2. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/README.md +11 -0
  3. scriptconv-0.0.4a4/scriptconv/diacritics.py +153 -0
  4. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/base.py +2 -2
  5. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/registry.py +4 -1
  6. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/version.py +1 -1
  7. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4/scriptconv.egg-info}/PKG-INFO +12 -1
  8. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv.egg-info/SOURCES.txt +2 -0
  9. scriptconv-0.0.4a4/tests/test_diacritics_graph.py +104 -0
  10. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/LICENSE +0 -0
  11. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/pyproject.toml +0 -0
  12. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/requirements.txt +0 -0
  13. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/__init__.py +0 -0
  14. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/__main__.py +0 -0
  15. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/cangjie.py +0 -0
  16. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/conventions.py +0 -0
  17. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/data/__init__.py +0 -0
  18. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
  19. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/graph.py +0 -0
  20. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/notation.py +0 -0
  21. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/__init__.py +0 -0
  22. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/__init__.py +0 -0
  23. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/bw2ipa.py +0 -0
  24. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/hangul2ipa.py +0 -0
  25. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/aspiration.csv +0 -0
  26. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/assimilation.csv +0 -0
  27. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/double_coda.csv +0 -0
  28. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/hanja.tsv +0 -0
  29. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/ipa.csv +0 -0
  30. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/neutralization.csv +0 -0
  31. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/tensification.csv +0 -0
  32. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/ko_tables/yale.csv +0 -0
  33. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/__init__.py +0 -0
  34. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/codeswitch.py +0 -0
  35. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/diacritize.py +0 -0
  36. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/dialectal.py +0 -0
  37. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/english_g2p.py +0 -0
  38. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/espeak.py +0 -0
  39. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/frontend.py +0 -0
  40. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/levantine_g2p.py +0 -0
  41. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/normalize.py +0 -0
  42. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/shami/phoneme_inventory.py +0 -0
  43. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_thirdparty/zh_num.py +0 -0
  44. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/__init__.py +0 -0
  45. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/kog2p/LICENSE.md +0 -0
  46. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/kog2p/__init__.py +0 -0
  47. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/kog2p/rulebook.txt +0 -0
  48. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/LICENSE.md +0 -0
  49. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/__init__.py +0 -0
  50. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/buck/__init__.py +0 -0
  51. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/buck/phonetise_buckwalter.py +0 -0
  52. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/buck/symbols.py +0 -0
  53. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/buck/tokenization.py +0 -0
  54. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/num2words.py +0 -0
  55. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/_vendored/mantoq/unicode_symbol2label.py +0 -0
  56. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/ar.py +0 -0
  57. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/en.py +0 -0
  58. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/enums.py +0 -0
  59. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/eu.py +0 -0
  60. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/fa.py +0 -0
  61. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/gl.py +0 -0
  62. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/he.py +0 -0
  63. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/ja.py +0 -0
  64. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/ko.py +0 -0
  65. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/mul.py +0 -0
  66. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/mwl.py +0 -0
  67. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/o2ipa.py +0 -0
  68. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/pt.py +0 -0
  69. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/shami.py +0 -0
  70. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/vi.py +0 -0
  71. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/phonemizers/zh.py +0 -0
  72. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/py.typed +0 -0
  73. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/readings.py +0 -0
  74. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/scripts.py +0 -0
  75. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv/translit.py +0 -0
  76. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv.egg-info/dependency_links.txt +0 -0
  77. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv.egg-info/requires.txt +0 -0
  78. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/scriptconv.egg-info/top_level.txt +0 -0
  79. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/setup.cfg +0 -0
  80. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_arpa_stress.py +0 -0
  81. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_cangjie.py +0 -0
  82. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_cli.py +0 -0
  83. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_conventions.py +0 -0
  84. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_errors_policy.py +0 -0
  85. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_examples.py +0 -0
  86. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_graph.py +0 -0
  87. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_notation.py +0 -0
  88. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_phonemizers_base.py +0 -0
  89. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_phonemizers_cjk_ar.py +0 -0
  90. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_readings.py +0 -0
  91. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_readings_zh.py +0 -0
  92. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_scripts.py +0 -0
  93. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_scripts_stressonnx_compat.py +0 -0
  94. {scriptconv-0.0.4a3 → scriptconv-0.0.4a4}/tests/test_translit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.4a3
3
+ Version: 0.0.4a4
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
@@ -391,6 +391,17 @@ p = GraphemePhonemizer()
391
391
  p.add_diacritics("Tenho muita sede hoje.", "pt") # 'Tenho muita sêde hoje.' (thirst)
392
392
  ```
393
393
 
394
+ Diacritization also joins the graph, like phonemization, via
395
+ `scriptconv.diacritics.register` — a `text -> text-diacritized` edge, opt-in,
396
+ with `text -> ipa` routing unchanged:
397
+
398
+ ```python
399
+ from scriptconv import diacritics
400
+ g = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
401
+ g.convert("Tenho muita sede hoje.", "text", "text-diacritized", lang="pt")
402
+ # 'Tenho muita sêde hoje.'
403
+ ```
404
+
394
405
  Stress is unwritten or under-marked in all 26 covered languages; East Slavic
395
406
  is the clearest case, where unstressed vowels also reduce (e.g. Russian
396
407
  о→[ɐ]/[ə]), so a missing mark corrupts more than prosody there. stressonnx is
@@ -265,6 +265,17 @@ p = GraphemePhonemizer()
265
265
  p.add_diacritics("Tenho muita sede hoje.", "pt") # 'Tenho muita sêde hoje.' (thirst)
266
266
  ```
267
267
 
268
+ Diacritization also joins the graph, like phonemization, via
269
+ `scriptconv.diacritics.register` — a `text -> text-diacritized` edge, opt-in,
270
+ with `text -> ipa` routing unchanged:
271
+
272
+ ```python
273
+ from scriptconv import diacritics
274
+ g = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
275
+ g.convert("Tenho muita sede hoje.", "text", "text-diacritized", lang="pt")
276
+ # 'Tenho muita sêde hoje.'
277
+ ```
278
+
268
279
  Stress is unwritten or under-marked in all 26 covered languages; East Slavic
269
280
  is the clearest case, where unstressed vowels also reduce (e.g. Russian
270
281
  о→[ɐ]/[ə]), so a missing mark corrupts more than prosody there. stressonnx is
@@ -0,0 +1,153 @@
1
+ """Diacritization as a graph extension — parallel to :mod:`scriptconv.phonemizers`.
2
+
3
+ Diacritization (Arabic tashkeel, Hebrew niqqud, East-Slavic/Turkic/Caucasian
4
+ word stress, European-Portuguese homograph sense marks) is, architecturally,
5
+ just another transform between text representations: it maps the ``"text"``
6
+ node to a lang-contextual ``"text-diacritized"`` node. Like
7
+ :func:`scriptconv.phonemizers.register` it is opt-in — ``DEFAULT_GRAPH`` stays
8
+ free of it until a caller extends a graph::
9
+
10
+ from scriptconv.graph import DEFAULT_GRAPH
11
+ from scriptconv import diacritics, phonemizers
12
+ g = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
13
+
14
+ g.convert("замок стоит на горе", "text", "text-diacritized", lang="ru")
15
+ # 'за́мок сто́ит на горе́' — just the diacritized text
16
+
17
+ g.convert("Tenho muita sede hoje.", "text", "text-diacritized", lang="pt")
18
+ # 'Tenho muita sêde hoje.'
19
+
20
+ Routing ``"text" -> "ipa"`` still takes the direct phonemization edge by
21
+ default: the diacritization edge is model-based (``lossless=False``), so it
22
+ never out-prices a direct phonemization, and enabling this extension does not
23
+ silently change phonemization output. The diacritized route is taken only
24
+ when a caller asks for ``"text-diacritized"`` explicitly.
25
+
26
+ A future per-engine *stance* would push this further into topology rather than
27
+ a flag: an engine that *requires* vocalized input would carry only a
28
+ ``"text-diacritized" -> "ipa"`` edge (forcing the detour), one that *forbids*
29
+ diacritics would carry only ``"text" -> "ipa"`` (making the detour
30
+ unroutable), and a tolerant engine (e.g. arbtok, which self-vocalizes) carries
31
+ both. The strip direction (``"text-diacritized" -> "text"``) is registered too, but
32
+ gated to languages whose marks are removable *overlay* diacritics —
33
+ Arabic/Hebrew vocalization and East-Slavic/Turkic/Caucasian stress. Strip
34
+ removes only the specific overlay codepoints each backend adds (combining
35
+ acute/grave for stress, tashkeel for Arabic, niqqud for Hebrew) — never a
36
+ blanket combining-mark filter — so precomposed native letters survive
37
+ (Cyrillic й/ё, Latvian macrons, Azerbaijani ç/ö, Arabic hamza carriers أ إ آ ؤ
38
+ ئ). The gate uses exact primary-subtag matching, so Aragonese (``arg``),
39
+ Herero (``her``), Mapudungun (``arn``) etc. are never misread as Arabic/Hebrew.
40
+ Languages whose diacritics are native orthography (European Portuguese, via
41
+ bifonia) refuse the strip with :class:`ValueError` instead of silently
42
+ corrupting the spelling (``café`` must never become ``cafe``). This split
43
+ tracks which backend/model produced the marks: overlay backends (phonikud,
44
+ tashkeel, stressonnx) are strippable; spelling-integral backends (bifonia) are
45
+ not.
46
+ """
47
+ from scriptconv.graph import Edge
48
+ from scriptconv.phonemizers.base import STRESS_LANGS, _primary_subtag
49
+
50
+ #: The lang-contextual node produced by diacritization. Like ``"text"`` it is
51
+ #: meaningful only with ``lang=`` context and exists only in opted-in graphs.
52
+ DIACRITIZED = "text-diacritized"
53
+
54
+ _DEFAULT_PHONEMIZER = None
55
+
56
+
57
+ def _get_phonemizer(phonikud_model=None):
58
+ global _DEFAULT_PHONEMIZER
59
+ if phonikud_model:
60
+ from scriptconv.phonemizers.base import GraphemePhonemizer
61
+ return GraphemePhonemizer(phonikud_model=phonikud_model)
62
+ if _DEFAULT_PHONEMIZER is None:
63
+ from scriptconv.phonemizers.base import GraphemePhonemizer
64
+ _DEFAULT_PHONEMIZER = GraphemePhonemizer()
65
+ return _DEFAULT_PHONEMIZER
66
+
67
+
68
+ def diacritize(text: str, lang: str = "und", model=None, **context) -> str:
69
+ """Add pronunciation-disambiguating diacritics to *text* for *lang*.
70
+
71
+ Thin wrapper over :meth:`BasePhonemizer.add_diacritics` — the single
72
+ dispatch (he→phonikud, ar→tashkeel, ru/uk/be/…→stressonnx, pt→bifonia).
73
+ Reuses a cached default phonemizer so repeated calls don't reload models; a
74
+ per-call Hebrew ``phonikud_model`` path (via context) builds a fresh one.
75
+ """
76
+ return _get_phonemizer(context.get("phonikud_model")).add_diacritics(text, lang, model)
77
+
78
+
79
+ # Combining marks each diacritization backend overlays onto the bare text.
80
+ # Strip removes ONLY these codepoints — never a blanket combining-mark filter,
81
+ # which (via NFD) would decompose and destroy precomposed native letters:
82
+ # Cyrillic й/ё, Latvian ī, Azerbaijani ç/ö, and Arabic hamza carriers أ إ آ ؤ ئ.
83
+ _STRESS_MARKS = frozenset({0x0300, 0x0301}) # combining grave / acute
84
+ _ARABIC_MARKS = frozenset(range(0x064B, 0x0660)) | frozenset({0x0670}) # tashkeel + dagger alef
85
+ _HEBREW_MARKS = (frozenset(range(0x05B0, 0x05BE))
86
+ | frozenset({0x05BF, 0x05C1, 0x05C2, 0x05C4, 0x05C5, 0x05C7})) # niqqud
87
+
88
+
89
+ def _overlay_marks(lang: str) -> "frozenset | None":
90
+ """The overlay codepoints for *lang*'s diacritization backend, or None.
91
+
92
+ Uses exact primary-subtag matching (never ``startswith``) so Aragonese
93
+ (``arg``), Herero (``her``), Mapudungun (``arn``) etc. are NOT misread as
94
+ Arabic/Hebrew and stripped.
95
+ """
96
+ p = _primary_subtag(lang)
97
+ if p in STRESS_LANGS:
98
+ return _STRESS_MARKS
99
+ if p == "ar":
100
+ return _ARABIC_MARKS
101
+ if p == "he":
102
+ return _HEBREW_MARKS
103
+ return None
104
+
105
+
106
+ def _supports_strip(lang: str) -> bool:
107
+ """True for languages whose diacritics are removable overlays (stress marks,
108
+ Arabic tashkeel, Hebrew niqqud) rather than native orthography."""
109
+ return _overlay_marks(lang) is not None
110
+
111
+
112
+ def strip_diacritics(text: str, lang: str = "und", **_) -> str:
113
+ """Remove the overlay diacritics *lang*'s backend adds, recovering the bare
114
+ text WITHOUT touching native letters.
115
+
116
+ Removes only the specific overlay codepoints (combining acute/grave for
117
+ stress, tashkeel for Arabic, niqqud for Hebrew) — precomposed native
118
+ letters (Cyrillic й/ё, Latvian ī, Azerbaijani ç, Arabic hamza carriers) are
119
+ left intact. Raises :class:`ValueError` for languages whose diacritics are
120
+ part of the native orthography (e.g. European Portuguese), where any removal
121
+ would corrupt the spelling.
122
+ """
123
+ marks = _overlay_marks(lang)
124
+ if marks is None:
125
+ raise ValueError(
126
+ f"cannot strip diacritics for lang={lang!r}: its diacritics are "
127
+ "part of the native orthography, so removal would corrupt the "
128
+ "spelling. strip is defined only for overlay diacritics "
129
+ "(Arabic/Hebrew vocalization, East-Slavic/Turkic stress).")
130
+ return "".join(c for c in text if ord(c) not in marks)
131
+
132
+
133
+ def register(graph) -> None:
134
+ """Opt-in graph integration: add the diacritize/strip edge pair.
135
+
136
+ ``text -> text-diacritized`` is model-based (``lossless=False``), so a
137
+ direct ``text -> ipa`` phonemization always out-prices the detour —
138
+ enabling this extension is safe and non-invasive. Pair with
139
+ :func:`scriptconv.phonemizers.register` to make
140
+ ``"text-diacritized" -> "ipa"`` reachable.
141
+
142
+ ``text-diacritized -> text`` (:func:`strip_diacritics`) is lossless and
143
+ cheap, but gated: it raises for languages whose diacritics are native
144
+ orthography rather than removable overlays.
145
+ """
146
+ graph.register(
147
+ Edge("text", DIACRITIZED,
148
+ lambda text, lang="und", model=None, **c: diacritize(text, lang, model, **c),
149
+ lossless=False))
150
+ graph.register(
151
+ Edge(DIACRITIZED, "text",
152
+ lambda text, lang="und", **_: strip_diacritics(text, lang),
153
+ lossless=True))
@@ -199,9 +199,9 @@ class BasePhonemizer(metaclass=abc.ABCMeta):
199
199
  ``ImportError`` naming its extra when the optional dependency is
200
200
  missing — scriptconv never installs anything on the caller's behalf.
201
201
  """
202
- if lang.startswith("he"):
202
+ if _primary_subtag(lang) == "he":
203
203
  return self.phonikud.add_diacritics(text)
204
- elif lang.startswith("ar"):
204
+ elif _primary_subtag(lang) == "ar":
205
205
  return self.tashkeel(model).diacritize(text)
206
206
  elif _primary_subtag(lang) in STRESS_LANGS:
207
207
  return self._stress(text, lang, model)
@@ -219,7 +219,9 @@ def register(graph) -> None:
219
219
  routing context, and present only in graphs that opted in (the
220
220
  :data:`scriptconv.graph.DEFAULT_GRAPH` stays orthography-only by
221
221
  design) — and one dispatching ``text -> ipa`` edge that resolves the
222
- per-language default (honouring an ``override=`` context key).
222
+ per-language default (honouring an ``override=`` context key). It also
223
+ accepts already-diacritized input via a ``"text-diacritized" -> "ipa"``
224
+ edge (the same phonemization; pair with :func:`scriptconv.diacritics.register`).
223
225
 
224
226
  Usage::
225
227
 
@@ -236,3 +238,4 @@ def register(graph) -> None:
236
238
  override).phonemize_string(text, lang)
237
239
 
238
240
  graph.register(Edge("text", "ipa", _text_to_ipa, lossless=False))
241
+ 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 = 3
5
+ VERSION_ALPHA = 4
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.4a3
3
+ Version: 0.0.4a4
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
@@ -391,6 +391,17 @@ p = GraphemePhonemizer()
391
391
  p.add_diacritics("Tenho muita sede hoje.", "pt") # 'Tenho muita sêde hoje.' (thirst)
392
392
  ```
393
393
 
394
+ Diacritization also joins the graph, like phonemization, via
395
+ `scriptconv.diacritics.register` — a `text -> text-diacritized` edge, opt-in,
396
+ with `text -> ipa` routing unchanged:
397
+
398
+ ```python
399
+ from scriptconv import diacritics
400
+ g = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
401
+ g.convert("Tenho muita sede hoje.", "text", "text-diacritized", lang="pt")
402
+ # 'Tenho muita sêde hoje.'
403
+ ```
404
+
394
405
  Stress is unwritten or under-marked in all 26 covered languages; East Slavic
395
406
  is the clearest case, where unstressed vowels also reduce (e.g. Russian
396
407
  о→[ɐ]/[ə]), so a missing mark corrupts more than prosody there. stressonnx is
@@ -6,6 +6,7 @@ scriptconv/__init__.py
6
6
  scriptconv/__main__.py
7
7
  scriptconv/cangjie.py
8
8
  scriptconv/conventions.py
9
+ scriptconv/diacritics.py
9
10
  scriptconv/graph.py
10
11
  scriptconv/notation.py
11
12
  scriptconv/py.typed
@@ -77,6 +78,7 @@ tests/test_arpa_stress.py
77
78
  tests/test_cangjie.py
78
79
  tests/test_cli.py
79
80
  tests/test_conventions.py
81
+ tests/test_diacritics_graph.py
80
82
  tests/test_errors_policy.py
81
83
  tests/test_examples.py
82
84
  tests/test_graph.py
@@ -0,0 +1,104 @@
1
+ import sys
2
+ import types
3
+ import unittest
4
+ from unittest import mock
5
+
6
+ from scriptconv.graph import DEFAULT_GRAPH
7
+ from scriptconv import diacritics, phonemizers
8
+
9
+
10
+ class TestDiacriticsGraphExtension(unittest.TestCase):
11
+ def setUp(self):
12
+ self.graph = DEFAULT_GRAPH.extend(diacritics.register).extend(phonemizers.register)
13
+
14
+ def test_default_graph_has_no_diacritized_node(self):
15
+ self.assertFalse(DEFAULT_GRAPH.can_convert("text", "text-diacritized"))
16
+
17
+ def test_extend_adds_diacritized_node(self):
18
+ self.assertTrue(self.graph.can_convert("text", "text-diacritized"))
19
+ self.assertTrue(self.graph.can_convert("text-diacritized", "ipa"))
20
+
21
+ def test_text_to_ipa_prefers_direct_route(self):
22
+ route = self.graph.route("text", "ipa")
23
+ self.assertEqual(len(route), 1)
24
+ edge = route[0]
25
+ self.assertEqual(edge.src, "text")
26
+ self.assertEqual(edge.dst, "ipa")
27
+
28
+ def test_portuguese_diacritization_via_graph(self):
29
+ out = self.graph.convert("Tenho muita sede hoje.", "text",
30
+ "text-diacritized", lang="pt")
31
+ self.assertEqual(out, "Tenho muita sêde hoje.")
32
+ out2 = self.graph.convert("A sede da empresa fica em Lisboa.", "text",
33
+ "text-diacritized", lang="pt")
34
+ self.assertEqual(out2, "A séde da empresa fica em Lisboa.")
35
+
36
+ def test_slavic_diacritization_routes_to_stressonnx_stub(self):
37
+ calls = []
38
+ mod = types.ModuleType("stressonnx")
39
+
40
+ def stress(text, lang, model=None):
41
+ calls.append((text, lang, model))
42
+ return "STRESSED"
43
+
44
+ mod.stress = stress
45
+ with mock.patch.dict(sys.modules, {"stressonnx": mod}):
46
+ out = self.graph.convert("замок стоит", "text",
47
+ "text-diacritized", lang="ru")
48
+ self.assertEqual(out, "STRESSED")
49
+ self.assertEqual(calls, [("замок стоит", "ru", None)])
50
+
51
+ def test_strip_recovers_bare_russian(self):
52
+ out = self.graph.convert("за́мок", "text-diacritized", "text", lang="ru")
53
+ self.assertEqual(out, "замок")
54
+
55
+ def test_strip_recovers_bare_arabic(self):
56
+ vocalized = "مُحَمَّد"
57
+ bare = "".join(c for c in vocalized if not (0x064B <= ord(c) <= 0x065F
58
+ or ord(c) == 0x0670))
59
+ out = self.graph.convert(vocalized, "text-diacritized", "text", lang="ar")
60
+ self.assertEqual(out, bare)
61
+ self.assertEqual(out, "محمد")
62
+
63
+ def test_strip_refused_for_portuguese(self):
64
+ with self.assertRaises(ValueError) as ctx:
65
+ self.graph.convert("sêde", "text-diacritized", "text", lang="pt")
66
+ self.assertIn("native orthography", str(ctx.exception))
67
+
68
+ def test_strip_edge_does_not_change_text_to_ipa_route(self):
69
+ route = self.graph.route("text", "ipa")
70
+ self.assertEqual(len(route), 1)
71
+ edge = route[0]
72
+ self.assertEqual(edge.src, "text")
73
+ self.assertEqual(edge.dst, "ipa")
74
+
75
+ def test_strip_preserves_cyrillic_native_letters(self):
76
+ out = self.graph.convert("мой родно́й край", "text-diacritized",
77
+ "text", lang="ru")
78
+ self.assertEqual(out, "мой родной край")
79
+ out2 = self.graph.convert("ёлка", "text-diacritized", "text", lang="ru")
80
+ self.assertEqual(out2, "ёлка")
81
+
82
+ def test_strip_preserves_latin_stress_lang_diacritics(self):
83
+ # native macron (ī) plus an added combining acute overlay
84
+ out = self.graph.convert("Rī́ga", "text-diacritized", "text",
85
+ lang="lv")
86
+ self.assertEqual(out, "Rīga")
87
+ out2 = self.graph.convert("Rīga", "text-diacritized", "text", lang="lv")
88
+ self.assertEqual(out2, "Rīga")
89
+
90
+ def test_strip_preserves_arabic_hamza(self):
91
+ out = self.graph.convert("أَحْمَد", "text-diacritized", "text", lang="ar")
92
+ self.assertEqual(out, "أحمد")
93
+
94
+ def test_strip_refuses_aragonese(self):
95
+ with self.assertRaises(ValueError):
96
+ self.graph.convert("Cristián", "text-diacritized", "text", lang="arg")
97
+
98
+ def test_add_diacritics_does_not_misroute_herero(self):
99
+ from scriptconv.phonemizers.base import GraphemePhonemizer
100
+ self.assertEqual(GraphemePhonemizer().add_diacritics("teste", "her"), "teste")
101
+
102
+
103
+ if __name__ == "__main__":
104
+ unittest.main()
File without changes
File without changes