euskaphone 0.1.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euskaphone/__init__.py +122 -0
- euskaphone/abbreviations.py +87 -0
- euskaphone/accent.py +181 -0
- euskaphone/codeswitch.py +316 -0
- euskaphone/data/abbreviations.tsv +35 -0
- euskaphone/data/langdetect/en.json.gz +0 -0
- euskaphone/data/langdetect/es.json.gz +0 -0
- euskaphone/data/langdetect/eu.json.gz +0 -0
- euskaphone/data/langdetect/fr.json.gz +0 -0
- euskaphone/data/lexicons/eu_hitz_overlay.tsv +40 -0
- euskaphone/data/lexicons/eu_toponyms.tsv +129 -0
- euskaphone/data/pitch_accent/northern_bizkaian.tsv +15 -0
- euskaphone/data/units.tsv +25 -0
- euskaphone/dates.py +128 -0
- euskaphone/langdetect.py +183 -0
- euskaphone/lattice_core.py +128 -0
- euskaphone/lexicons.py +115 -0
- euskaphone/normalize.py +49 -0
- euskaphone/number_utils.py +290 -0
- euskaphone/numverbal.py +67 -0
- euskaphone/pitch_accent.py +210 -0
- euskaphone/plugin.py +44 -0
- euskaphone/registry.py +169 -0
- euskaphone/romans.py +158 -0
- euskaphone/units.py +144 -0
- euskaphone/version.py +10 -0
- euskaphone-0.1.0a2.dist-info/METADATA +220 -0
- euskaphone-0.1.0a2.dist-info/RECORD +32 -0
- euskaphone-0.1.0a2.dist-info/WHEEL +5 -0
- euskaphone-0.1.0a2.dist-info/entry_points.txt +2 -0
- euskaphone-0.1.0a2.dist-info/licenses/LICENSE +204 -0
- euskaphone-0.1.0a2.dist-info/top_level.txt +1 -0
euskaphone/__init__.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""euskaphone — dialect-aware Basque phonemization on the orthography2ipa lattice.
|
|
2
|
+
|
|
3
|
+
The phonemizer drives the shared orthography2ipa candidate lattice: a dialect is
|
|
4
|
+
an orthography2ipa Basque lect spec, and the lattice — the spec's grapheme
|
|
5
|
+
table, ``allophones`` and cross-word sandhi — produces the dialect's phonology
|
|
6
|
+
directly. euskaphone contributes the stages orthography2ipa leaves to the
|
|
7
|
+
caller: vigesimal (base-20) number verbalization, Spanish/French code-switch
|
|
8
|
+
handling with nativization onto the Basque inventory, and a proper-name/loan
|
|
9
|
+
lexicon hook. Basque orthography is near-phonemic, so — by design — there is no
|
|
10
|
+
homograph subsystem. See :mod:`euskaphone.lattice_core`.
|
|
11
|
+
"""
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from euskaphone.version import __version__
|
|
15
|
+
from euskaphone.lattice_core import phonemize as _phonemize, register_lexicon
|
|
16
|
+
from euskaphone.lexicons import (
|
|
17
|
+
apply_builtins, register_hitz_overlay, register_toponyms,
|
|
18
|
+
)
|
|
19
|
+
from euskaphone.pitch_accent import (
|
|
20
|
+
AccentClass, PitchAccentLexicon, annotate_sentence as _annotate_pitch,
|
|
21
|
+
default_lexicon,
|
|
22
|
+
)
|
|
23
|
+
from euskaphone.registry import (
|
|
24
|
+
DEFAULT_DIALECT, dialect_aliases, is_supported as _is_supported,
|
|
25
|
+
list_dialects, resolve_lect,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
#: The lects whose lexical pitch accent euskaphone can annotate. The shipped
|
|
29
|
+
#: cited lexicon documents the Northern Bizkaian sub-area of Biscayan.
|
|
30
|
+
_PITCH_ACCENT_LECTS = ("eu-x-bizkaiera",)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EuskaPhonemizer:
|
|
34
|
+
"""Dialect-aware Basque phonemization on the orthography2ipa lattice.
|
|
35
|
+
|
|
36
|
+
The eight Basque lects orthography2ipa ships are reachable by their BCP-47
|
|
37
|
+
codes ("eu", "eu-x-bizkaiera", "eu-x-zuberera", …) or by a human-readable
|
|
38
|
+
alias ("batua", "biscayan", "souletin"); the full list comes from
|
|
39
|
+
:func:`euskaphone.list_dialects`.
|
|
40
|
+
|
|
41
|
+
Built-in lexicons (:mod:`euskaphone.lexicons`) are wired at construction:
|
|
42
|
+
the Euskaltzaindia toponym seed is registered by default (``toponyms=True``,
|
|
43
|
+
opt-out) unless a caller already registered a lexicon for ``eu``; the HiTZ
|
|
44
|
+
proper-noun overlay is opt-in (``lexicon="hitz"``) and carries a benchmark
|
|
45
|
+
circularity caveat.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, toponyms: bool = True,
|
|
49
|
+
lexicon: Optional[str] = None) -> None:
|
|
50
|
+
apply_builtins(toponyms=toponyms, lexicon=lexicon)
|
|
51
|
+
|
|
52
|
+
def phonemize_sentence(self, sentence: str, dialect: str = DEFAULT_DIALECT,
|
|
53
|
+
contact: str = "auto",
|
|
54
|
+
pitch_accent: bool = False) -> str:
|
|
55
|
+
"""Phonemize ``sentence`` for the ``dialect`` lect.
|
|
56
|
+
|
|
57
|
+
Parameters:
|
|
58
|
+
sentence: Input text to phonemize.
|
|
59
|
+
dialect: A lect code or human-readable alias (see
|
|
60
|
+
:func:`euskaphone.list_dialects` /
|
|
61
|
+
:func:`euskaphone.dialect_aliases`). Defaults to Standard Batua.
|
|
62
|
+
contact: Embedded-language policy — ``"auto"`` (detect and classify
|
|
63
|
+
each contact word per-word among es/fr/en, unclassified words
|
|
64
|
+
falling to the dialect's side), ``"es"``, ``"fr"``, ``"en"`` or
|
|
65
|
+
``"none"`` (no code-switching).
|
|
66
|
+
pitch_accent: When ``True``, annotate lexical Northern Bizkaian pitch
|
|
67
|
+
accent (:mod:`euskaphone.pitch_accent`) — a mark on the accented
|
|
68
|
+
syllable of each lexically accented word, nothing on unaccented
|
|
69
|
+
or unknown words. Opt-in and only defined for Biscayan
|
|
70
|
+
(``eu-x-bizkaiera`` / ``biscayan``); a ``ValueError`` is raised
|
|
71
|
+
for any other lect. Annotation is word-level, so cross-word
|
|
72
|
+
sandhi is not applied in this mode.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Space-separated IPA for each word.
|
|
76
|
+
"""
|
|
77
|
+
if pitch_accent:
|
|
78
|
+
lect = resolve_lect(dialect)
|
|
79
|
+
if lect not in _PITCH_ACCENT_LECTS:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"pitch_accent is only defined for Biscayan "
|
|
82
|
+
f"({', '.join(_PITCH_ACCENT_LECTS)}); got {dialect!r} "
|
|
83
|
+
f"({lect}). Coverage is scoped to the Northern Bizkaian "
|
|
84
|
+
f"sub-area Hualde documents.")
|
|
85
|
+
return _annotate_pitch(
|
|
86
|
+
sentence,
|
|
87
|
+
lambda tok: _phonemize(tok, dialect, contact))
|
|
88
|
+
return _phonemize(sentence, dialect, contact)
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def is_supported(dialect: str) -> bool:
|
|
92
|
+
"""Whether ``dialect`` names a known Basque lect."""
|
|
93
|
+
return _is_supported(dialect)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
"EuskaPhonemizer",
|
|
98
|
+
"register_lexicon",
|
|
99
|
+
"register_toponyms",
|
|
100
|
+
"register_hitz_overlay",
|
|
101
|
+
"list_dialects",
|
|
102
|
+
"dialect_aliases",
|
|
103
|
+
"resolve_lect",
|
|
104
|
+
"AccentClass",
|
|
105
|
+
"PitchAccentLexicon",
|
|
106
|
+
"default_lexicon",
|
|
107
|
+
"__version__",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
ph = EuskaPhonemizer()
|
|
113
|
+
sentences = [
|
|
114
|
+
"Gaur 1936ko gerra ikasi dugu.",
|
|
115
|
+
"Hogeita hamaika katu zuri ikusi ditut.",
|
|
116
|
+
"Bilbon 20:00etan hasiko da kontzertua.",
|
|
117
|
+
]
|
|
118
|
+
for s in sentences:
|
|
119
|
+
print(s)
|
|
120
|
+
for code in ["eu", "eu-x-bizkaiera", "eu-x-zuberera"]:
|
|
121
|
+
print(f"{code} → {ph.phonemize_sentence(s, code)}")
|
|
122
|
+
print("######")
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Abbreviation (laburdura) expansion — the pre-lattice normalizer stage.
|
|
2
|
+
|
|
3
|
+
Written Basque is full of period-marked abbreviations that a grapheme-to-phoneme
|
|
4
|
+
lattice would mis-read letter by letter (``etab.`` is not "e-t-a-b", it is
|
|
5
|
+
``eta abar``). This stage expands a curated, **cited** list of abbreviations to
|
|
6
|
+
their full spoken form *before* the lattice — and before the Roman-numeral stage
|
|
7
|
+
— so the trailing periods that mark an abbreviation are consumed here and never
|
|
8
|
+
confused with the Roman-numeral ordinal period (``XX.``, see
|
|
9
|
+
:mod:`euskaphone.romans`).
|
|
10
|
+
|
|
11
|
+
The list itself is data, not code: it lives in ``data/abbreviations.tsv`` and is
|
|
12
|
+
extended by adding a cited row, never by editing this module.
|
|
13
|
+
|
|
14
|
+
Sources
|
|
15
|
+
-------
|
|
16
|
+
* **Euskaltzaindia**, **Araua 196**, EBO (I) *"Laburtzapenak: laburdurak eta
|
|
17
|
+
siglak"* — the Academy's norm fixing the standard abbreviation forms and the
|
|
18
|
+
rule that a laburdura is written with a period and read as the full word.
|
|
19
|
+
* **EIMA / Basque Government**, J. R. Zubimendi, *Ortotipografia* (Eusko
|
|
20
|
+
Jaurlaritza, 2004), "Laburdurak" — spells the same list plus the ``K.a.`` /
|
|
21
|
+
``K.o.`` era abbreviations.
|
|
22
|
+
(``papers/iberian/eima_ortotipografia_zubimendi.pdf``)
|
|
23
|
+
* **Euskalterm**, *Laburtzapenen Hiztegia* (Gasteiz, 2010) — the exhaustive
|
|
24
|
+
reference dictionary of Basque abbreviations.
|
|
25
|
+
(``papers/iberian/euskalterm_laburtzapenen_hiztegia_2010.pdf``)
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import os
|
|
30
|
+
from typing import Dict
|
|
31
|
+
|
|
32
|
+
_DATA = os.path.join(os.path.dirname(__file__), "data", "abbreviations.tsv")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _load(path: str = _DATA) -> Dict[str, str]:
|
|
36
|
+
"""Load the abbreviation → expansion table (case-folded keys)."""
|
|
37
|
+
table: Dict[str, str] = {}
|
|
38
|
+
with open(path, encoding="utf-8") as fh:
|
|
39
|
+
for line in fh:
|
|
40
|
+
line = line.rstrip("\n")
|
|
41
|
+
if not line or line.startswith("#"):
|
|
42
|
+
continue
|
|
43
|
+
parts = line.split("\t")
|
|
44
|
+
if len(parts) < 2:
|
|
45
|
+
continue
|
|
46
|
+
abbr, expansion = parts[0], parts[1]
|
|
47
|
+
if abbr and expansion:
|
|
48
|
+
table[abbr.lower()] = expansion
|
|
49
|
+
return table
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#: abbreviation (as written, with its period) → full spoken expansion.
|
|
53
|
+
ABBREVIATIONS: Dict[str, str] = _load()
|
|
54
|
+
|
|
55
|
+
#: The bare (period-stripped) keys, for matching a token whose trailing period
|
|
56
|
+
#: the tokenizer may have split off (``etab`` as well as ``etab.``).
|
|
57
|
+
_BARE = {k.rstrip("."): v for k, v in ABBREVIATIONS.items()}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def expand_token(token: str) -> str:
|
|
61
|
+
"""Expand one *token* if it is a known abbreviation, else return it as-is.
|
|
62
|
+
|
|
63
|
+
Both the written form with its period (``etab.``) and the bare form
|
|
64
|
+
(``etab``) resolve; multi-dot abbreviations (``K.a.``, ``h.d.``) are matched
|
|
65
|
+
whole. Matching is case-insensitive but the lookup is exact on the letters.
|
|
66
|
+
"""
|
|
67
|
+
key = token.lower()
|
|
68
|
+
if key in ABBREVIATIONS:
|
|
69
|
+
return ABBREVIATIONS[key]
|
|
70
|
+
bare = key.rstrip(".")
|
|
71
|
+
if bare in _BARE:
|
|
72
|
+
return _BARE[bare]
|
|
73
|
+
return token
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def expand_abbreviations(text: str) -> str:
|
|
77
|
+
"""Expand every known abbreviation token in *text*.
|
|
78
|
+
|
|
79
|
+
Runs word by word so surrounding punctuation and spacing are preserved for
|
|
80
|
+
tokens that are not abbreviations. Multi-word expansions are inserted inline
|
|
81
|
+
(``etab.`` → ``eta abar``) and flow on to the pronunciation lattice as
|
|
82
|
+
ordinary Basque words.
|
|
83
|
+
"""
|
|
84
|
+
out = []
|
|
85
|
+
for word in text.split():
|
|
86
|
+
out.append(expand_token(word))
|
|
87
|
+
return " ".join(out)
|
euskaphone/accent.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Accent forcing: re-transcribe or *respell* Batua text toward a target lect.
|
|
2
|
+
|
|
3
|
+
Two modes, one entry point :func:`force_accent`:
|
|
4
|
+
|
|
5
|
+
* ``mode="ipa"`` — the direct view. The text is transcribed straight through the
|
|
6
|
+
target lect's lattice, so the return is the target dialect's own IPA (the
|
|
7
|
+
"IPA delta" against Batua).
|
|
8
|
+
|
|
9
|
+
* ``mode="respell"`` — the verification-gated respeller, ported from tugaphone's
|
|
10
|
+
accent-forcing architecture. It rewrites the *Batua orthography* using Basque
|
|
11
|
+
spelling conventions that push a **Batua** reader toward the target
|
|
12
|
+
pronunciation (``z``→``s`` for the peninsular seseo merger, ``il``→``ill`` for
|
|
13
|
+
palatalisation, ``h``-insertion for continental aspiration, …). Every edit is
|
|
14
|
+
**verification-gated**: an edit is kept only if re-transcribing the respelled
|
|
15
|
+
text *through the base (Batua) lattice* moves the result measurably closer to
|
|
16
|
+
the target lect's IPA. An edit the base lattice cannot cash out — Batua has a
|
|
17
|
+
silent ``h`` and no ``ü``, so continental aspiration and Souletin front
|
|
18
|
+
rounding are simply not recoverable through it — is rejected, and the honest
|
|
19
|
+
consequence is a near-zero respell gain for exactly those features.
|
|
20
|
+
|
|
21
|
+
The gate is what keeps respell honest: it never invents an orthography whose
|
|
22
|
+
Batua reading does not actually approximate the target. :func:`respell_report`
|
|
23
|
+
exposes the per-edit accounting so a benchmark can publish the real ceiling.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from typing import Callable, List, NamedTuple, Optional
|
|
29
|
+
|
|
30
|
+
from euskaphone.lattice_core import engine
|
|
31
|
+
from euskaphone.registry import resolve_lect
|
|
32
|
+
|
|
33
|
+
#: The base reading lect: respelled orthography is scored by *this* lattice.
|
|
34
|
+
BASE_LECT = "eu"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _transcribe(text: str, lect: str) -> str:
|
|
38
|
+
"""Pure-lattice transcription (no code-switch, no number stage)."""
|
|
39
|
+
return engine(lect).transcribe(text)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _levenshtein(a: str, b: str) -> int:
|
|
43
|
+
d = list(range(len(b) + 1))
|
|
44
|
+
for i in range(1, len(a) + 1):
|
|
45
|
+
prev, d[0] = d[0], i
|
|
46
|
+
for j in range(1, len(b) + 1):
|
|
47
|
+
cur = d[j]
|
|
48
|
+
d[j] = min(d[j] + 1, d[j - 1] + 1, prev + (a[i - 1] != b[j - 1]))
|
|
49
|
+
prev = cur
|
|
50
|
+
return d[len(b)]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _per(ref: str, hyp: str) -> float:
|
|
54
|
+
r, h = ref.replace(" ", ""), hyp.replace(" ", "")
|
|
55
|
+
return _levenshtein(r, h) / max(1, len(r))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RespellRule(NamedTuple):
|
|
59
|
+
"""One candidate orthographic edit, tried against the verification gate."""
|
|
60
|
+
|
|
61
|
+
name: str
|
|
62
|
+
#: What phonological delta the edit chases (documentation only).
|
|
63
|
+
rationale: str
|
|
64
|
+
#: Rewrites Batua orthography toward the target spelling convention.
|
|
65
|
+
apply: Callable[[str], str]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _sub(pattern: str, repl: str) -> Callable[[str], str]:
|
|
69
|
+
rx = re.compile(pattern)
|
|
70
|
+
return lambda s: rx.sub(repl, s)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
#: The respell rule set. Each rule is a *proposal*; the verification gate decides
|
|
74
|
+
#: per text and per target whether it survives. Rules whose delta the Batua
|
|
75
|
+
#: lattice cannot represent (``h``-insertion, front rounding) are proposed too —
|
|
76
|
+
#: they are simply gated out, which is how the honest ceiling is measured.
|
|
77
|
+
RESPELL_RULES: List[RespellRule] = [
|
|
78
|
+
RespellRule(
|
|
79
|
+
"seseo_z_to_s",
|
|
80
|
+
"peninsular seseo: laminal /s̻/ (⟨z⟩) merges into apical /s̺/ (⟨s⟩)",
|
|
81
|
+
_sub(r"z", "s"),
|
|
82
|
+
),
|
|
83
|
+
RespellRule(
|
|
84
|
+
"palatal_il",
|
|
85
|
+
"expressive/contextual palatalisation ⟨il⟩ → /ʎ/ (spelled ⟨ill⟩)",
|
|
86
|
+
_sub(r"il", "ill"),
|
|
87
|
+
),
|
|
88
|
+
RespellRule(
|
|
89
|
+
"palatal_in",
|
|
90
|
+
"expressive/contextual palatalisation ⟨in⟩ → /ɲ/ (spelled ⟨iñ⟩)",
|
|
91
|
+
_sub(r"in", "iñ"),
|
|
92
|
+
),
|
|
93
|
+
RespellRule(
|
|
94
|
+
"aspiration_h",
|
|
95
|
+
"continental aspiration: prothetic ⟨h⟩ on vowel-initial words",
|
|
96
|
+
_sub(r"(?<![^\W\d_])(?=[aeiouAEIOU])", "h"),
|
|
97
|
+
),
|
|
98
|
+
RespellRule(
|
|
99
|
+
"front_rounding_u_ue",
|
|
100
|
+
"Souletin front rounding: ⟨u⟩ → ⟨ü⟩ [y]",
|
|
101
|
+
_sub(r"u", "ü"),
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RespellResult(NamedTuple):
|
|
107
|
+
"""The outcome of a respell pass over one text against one target."""
|
|
108
|
+
|
|
109
|
+
source: str
|
|
110
|
+
respelled: str
|
|
111
|
+
target_ipa: str
|
|
112
|
+
base_ipa: str
|
|
113
|
+
respelled_ipa: str
|
|
114
|
+
#: PER of the base reading before respelling; the starting distance.
|
|
115
|
+
per_before: float
|
|
116
|
+
#: PER after keeping every surviving edit; ``per_before - per_after`` is gain.
|
|
117
|
+
per_after: float
|
|
118
|
+
#: Names of the rules the verification gate accepted, in order.
|
|
119
|
+
accepted: List[str]
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def gain(self) -> float:
|
|
123
|
+
"""PER reduction the respelling bought (>= 0)."""
|
|
124
|
+
return self.per_before - self.per_after
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def respell_report(text: str, dialect: str,
|
|
128
|
+
rules: Optional[List[RespellRule]] = None,
|
|
129
|
+
eps: float = 1e-9) -> RespellResult:
|
|
130
|
+
"""Greedy, verification-gated respell of ``text`` toward ``dialect``.
|
|
131
|
+
|
|
132
|
+
For each rule in turn, the candidate respelling is scored by transcribing it
|
|
133
|
+
through the **base** (Batua) lattice and measuring PER against the target
|
|
134
|
+
lect's own transcription of the original text. The edit is kept only if it
|
|
135
|
+
strictly lowers that PER; otherwise it is discarded. The result carries the
|
|
136
|
+
full accounting for benchmarking.
|
|
137
|
+
"""
|
|
138
|
+
rules = RESPELL_RULES if rules is None else rules
|
|
139
|
+
target = resolve_lect(dialect)
|
|
140
|
+
target_ipa = _transcribe(text, target)
|
|
141
|
+
base_ipa = _transcribe(text, BASE_LECT)
|
|
142
|
+
|
|
143
|
+
current = text
|
|
144
|
+
current_per = _per(target_ipa, base_ipa)
|
|
145
|
+
per_before = current_per
|
|
146
|
+
accepted: List[str] = []
|
|
147
|
+
|
|
148
|
+
for rule in rules:
|
|
149
|
+
candidate = rule.apply(current)
|
|
150
|
+
if candidate == current:
|
|
151
|
+
continue
|
|
152
|
+
cand_ipa = _transcribe(candidate, BASE_LECT)
|
|
153
|
+
cand_per = _per(target_ipa, cand_ipa)
|
|
154
|
+
if cand_per < current_per - eps:
|
|
155
|
+
current, current_per = candidate, cand_per
|
|
156
|
+
accepted.append(rule.name)
|
|
157
|
+
|
|
158
|
+
return RespellResult(
|
|
159
|
+
source=text,
|
|
160
|
+
respelled=current,
|
|
161
|
+
target_ipa=target_ipa,
|
|
162
|
+
base_ipa=base_ipa,
|
|
163
|
+
respelled_ipa=_transcribe(current, BASE_LECT),
|
|
164
|
+
per_before=per_before,
|
|
165
|
+
per_after=current_per,
|
|
166
|
+
accepted=accepted,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def force_accent(text: str, dialect: str, mode: str = "ipa") -> str:
|
|
171
|
+
"""Force ``text`` toward ``dialect``.
|
|
172
|
+
|
|
173
|
+
``mode="ipa"`` returns the target lect's IPA directly. ``mode="respell"``
|
|
174
|
+
returns Batua orthography rewritten (verification-gated) so a Batua reader
|
|
175
|
+
approximates the target pronunciation.
|
|
176
|
+
"""
|
|
177
|
+
if mode == "ipa":
|
|
178
|
+
return _transcribe(text, resolve_lect(dialect))
|
|
179
|
+
if mode == "respell":
|
|
180
|
+
return respell_report(text, dialect).respelled
|
|
181
|
+
raise ValueError(f"unknown mode {mode!r}; expected 'ipa' or 'respell'")
|