Metadata-Version: 2.4
Name: euskaphone
Version: 0.2.0a3
Summary: Dialect-aware Basque (Euskara) text-to-IPA phonemizer over the orthography2ipa lattice
Author-email: JarbasAi <jarbasai@mailfence.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/TigreGotico/euskaphone
Project-URL: Repository, https://github.com/TigreGotico/euskaphone
Keywords: basque,euskara,phonemizer,g2p,ipa,phonetics,tts,nlp
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Natural Language :: Basque
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: orthography2ipa>=7.28.0a1
Requires-Dist: ovos-number-parser>=0.6.0a1
Provides-Extra: langdetect
Requires-Dist: markovonnx>=1.0.1; extra == "langdetect"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: markovonnx>=1.0.1; extra == "test"
Dynamic: license-file

# euskaphone

Dialect-aware Basque (Euskara) text-to-IPA phonemizer — a TTS frontend built on
the shared [orthography2ipa](https://github.com/TigreGotico/orthography2ipa)
pronunciation lattice. euskaphone is the Basque sibling of
[tugaphone](https://github.com/TigreGotico/tugaphone) (Portuguese) and
[mwl_phonemizer](https://github.com/TigreGotico/mwl_phonemizer) (Mirandese), and
is offered to the Basque research and speech-technology community as an open,
inspectable, source-cited pipeline.

A dialect *is* an orthography2ipa Basque lect spec: the spec's grapheme table,
allophony and cross-word sandhi produce the dialect's phonology directly — the
apical/laminal sibilant contrast, the affricates, Souletin aspiration and `/y/`.
euskaphone adds only the stages orthography2ipa leaves to the caller.

## Install

```bash
pip install euskaphone
```

## Quickstart

```python
from euskaphone import EuskaPhonemizer

ph = EuskaPhonemizer()
ph.phonemize_sentence("Zazpi katu zuri ikusi ditut.")          # Batua (default)
# 's̻as̻pi katu s̻uɾi ikus̺i ditut'

ph.phonemize_sentence("Hotza egiten du gaur mendian.", "souletin")
# 'hots̻a eɡiten dy ɡauɾ mendian'   (Souletin: aspiration, /y/)
```

## Dialects

Eight lects, selectable by BCP-47 code or by a human-readable alias:

| code | dialect | aliases |
|------|---------|---------|
| `eu` | Standard Basque | `batua`, `standard` |
| `eu-x-bizkaiera` | Biscayan | `biscayan`, `western` |
| `eu-x-gipuzkera` | Gipuzkoan | `guipuzcoan`, `central` |
| `eu-x-lapurtera` | Lapurdian | `labourdin` |
| `eu-x-nafarra-garaia` | High Navarrese | `high-navarrese` |
| `eu-x-nafarra-beherea` | Low Navarrese | `low-navarrese` |
| `eu-x-zuberera` | Souletin | `souletin`, `xiberotarra` |
| `eu-x-erronkariera` | Roncalese (†) | `roncalese` |

## The three things euskaphone adds

### 1. Vigesimal number normalization (the flagship)

Basque cardinals are **base-20**: `hogei` (20), `berrogei` (40 = 2×20),
`hirurogei` (60), `laurogei` (80); 30/50/70/90 are the preceding twenty *plus
ten*; and the copulative `eta` ("and") contracts to `-ta` on the twenties.

```python
from euskaphone.number_utils import BasqueNumberParser, normalize_numbers

p = BasqueNumberParser("eu")
p.cardinal(31)     # 'hogeita hamaika'   (20-and-11)
p.cardinal(1936)   # 'mila bederatziehun eta hogeita hamasei'
p.ordinal(16)      # 'hamaseigarren'

# case-suffixed numerals and clock time in running text (declension on the
# last element of the numeral phrase)
normalize_numbers("1936ko gerra", "eu")   # '… hogeita hamaseiko gerra'
normalize_numbers("20:00etan", "eu")      # 'hogeietan'
```

Every base form is cited to **Euskaltzaindia Araua 7** (cardinals) and
**Araua 18** (ordinals), and each is tagged attested-in-source vs derived-by-rule
in `euskaphone.number_utils` (the `ATTESTED` / `DERIVED` sets). One notable
correction the sources force: Araua 7 records `bost` (not `bortz`) for Zuberoa
and most of Low Navarre, so euskaphone keeps `bost` everywhere by default and
offers the `bortz` series only for Lapurdian — null beats a wrong dialect swap.
See [`docs/numbers.md`](docs/numbers.md).

### 2. Code-switch handling

Real Basque text embeds Spanish (Hegoalde), French (Iparralde) and — in the
tech/music/media register — English. Word-level detection identifies the embedded
material, classifies each contact word **per word** among `es`/`fr`/`en`, routes
it through the orthography2ipa `es-ES`/`fr-FR`/`en-US` lattice, and **nativizes**
the result onto the Basque inventory (project every foreign phone onto its
nearest Basque phone — never drop, always project). The `contact` parameter is
`auto` (per-word detection), `es`, `fr`, `en`, or `none`.

Detection prefers a small bundled **char-Markov language detector** (Basque /
Spanish / French / English, ~180 KB total, scored by `markovonnx`; install with
`euskaphone[langdetect]`) and falls back to an orthographic heuristic when the
models are unavailable. Basque is the in-language default: a word is routed out
of Basque only when a foreign model beats the Basque model by a clear margin (and
short, plausibly-native words need orthographic corroboration), so a weak signal
never misroutes a native word.

```python
ph.phonemize_sentence("Madrilen Plaza Mayor ikusi dut.", "eu")
# '… plas̻a majoɾ …'  (Spanish routed + nativized: no θ, no stress)

ph.phonemize_sentence("Streaming plataforma berria erabili dugu.")
# 'strimin platafoɾma …'  (English routed + nativized onto the 5-vowel system)

ph.phonemize_sentence("Maison Rouge etxean.", "souletin")
# continental dialect → French contact by default
```

See [`docs/codeswitch.md`](docs/codeswitch.md).

### 3. Lexicon hook + shipped lexicons

`register_lexicon(dialect, source)` registers a proper-name/loan pronunciation
table (the orthography2ipa lexicon contract: a `word<TAB>ipa` path, URL or
`hf://` id); a covered word bypasses the lattice.

Two lexicons ship built-in: a curated **Euskaltzaindia toponym seed** (on by
default, `EuskaPhonemizer(toponyms=False)` to opt out) and an opt-in **HiTZ
proper-noun overlay** (`EuskaPhonemizer(lexicon="hitz")`) that carries a
benchmark circularity caveat. A caller's own lexicon always wins.
See [`docs/lexicons.md`](docs/lexicons.md).

### 4. Orthographic normalization

Before the lattice runs, everything written as a symbol, abbreviation, Roman
numeral or numeric date is spelled into Basque words
(`euskaphone.normalize.normalize_text`, wired in as the orthography2ipa
`normalize` stage):

```python
from euskaphone.normalize import normalize_text

normalize_text("XX. mendean %20 igo da", "eu")
# 'hogeigarren mendean ehuneko hogei igo da'
normalize_text("Luis XIV.a errege zen", "eu")     # '… hamalaugarrena …'
normalize_text("Sarrera €5eko da", "eu")          # '… bost euroko …'
normalize_text("15/01/2024", "eu")   # 'bi mila eta hogeita lauko urtarrilaren hamabostean'
```

Roman-numeral ordinals (`XX.` → `hogeigarren`) and monarch numerals
(`Luis XIV.a` → `hamalaugarrena`), a cited abbreviation list
(`etab.` → `eta abar`), unit/currency/percent symbols with Basque word order and
declension (`%5` and `5%` both → `ehuneko bost`), and numeric dates — each cited
to Euskaltzaindia (Araua 18/37/196/197) and the EIMA *Ortotipografia* guide. The
abbreviation and unit tables are expandable data files
(`euskaphone/data/*.tsv`). See [`docs/normalization.md`](docs/normalization.md).

### No homograph subsystem — by design

Basque orthography is near-phonemic: a grapheme's reading is essentially fixed
and sense-independent. Unlike tugaphone's Portuguese (which needs `bifonia` to
resolve heterophonic homographs), euskaphone deliberately ships **no** homograph
stage. This is a scope decision, documented so it is not mistaken for a gap.

## OVOS plugin

```python
from euskaphone.plugin import EuskaphoneG2PPlugin
EuskaphoneG2PPlugin("souletin").transcribe("kaixo mundua")
```

Registered under the `opm.g2p` entry-point group as `euskaphone`.

## How it measures up

euskaphone drives the shared orthography2ipa lattice, so it is only as accurate
as that lattice — these numbers measure the lattice, honestly labelled. Run them
with `python scripts/benchmark.py [--hitz]`.

| gold | kind | figure |
|------|------|--------|
| orthography2ipa eu gold (166 rows, 8 lects) | **regression fixture** (same lattice made it) | PER `0.0000` — proves the lattice is untouched, *not* accuracy |
| [WikiPron](https://github.com/CUNY-CL/wikipron) `eus_latn` broad | independent word gold | PER `0.0749`, word-acc `0.4563` (full set, 20,115 words) |
| [HiTZ/EHU](https://huggingface.co/datasets/HiTZ/wikipedia_basque_ipa) Wikipedia G2P | independent cross-engine (Univ. of the Basque Country) | PER `0.1384` folded (3k-sentence sample) |

The WikiPron and HiTZ figures are the honest ones: a pure lattice with no
lexicon, on proper-name-heavy and raw-Wikipedia text, where transcription
conventions and multiple valid pronunciations account for much of the gap. That
gap is exactly what the lexicon hook exists to close. Measured against the
comparable open tools on the same full WikiPron set (segment-level protocol,
stress stripped — see `docs/benchmarks.md`): euskaphone PER `0.1009` / word-acc
`0.4298`, espeak-ng `eu` `0.1704` / `0.1750` (espyak, the pure-Python espeak
port, scores identically), ahotts-g2p `0.3360` / `0.0003`
(ahotts's figure is dominated by its non-IPA symbol conventions). **AhoTTS**
covers Standard Batua only; **espeak-ng**'s
`eu` voice is single-dialect and rule-thin — euskaphone's contribution is the
eight-lect coverage, the sourced vigesimal numerals, and the code-switch
nativization, over a lattice shared across the whole Iberian family.

## License

Apache-2.0.
