Metadata-Version: 2.4
Name: latin-g2p
Version: 0.2.0
Summary: Latin grapheme-to-phonology analysis utilities
Author: shawnlee222
License-Expression: GPL-3.0-only
Project-URL: Repository, https://github.com/shawnlee222/latin-g2p
Project-URL: Issues, https://github.com/shawnlee222/latin-g2p/issues
Project-URL: Changelog, https://github.com/shawnlee222/latin-g2p/blob/main/CHANGELOG.md
Keywords: Latin,G2P,IPA,phonology,prosody,scansion
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Text Processing :: Linguistic
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE.md
Provides-Extra: test
Requires-Dist: coverage[toml]>=7.0; extra == "test"
Requires-Dist: mypy>=1.10; extra == "test"
Requires-Dist: pytest>=7.0; extra == "test"
Requires-Dist: ruff>=0.8; extra == "test"
Dynamic: license-file

# latin-g2p

[![CI](https://github.com/shawnlee222/latin-g2p/actions/workflows/ci.yml/badge.svg)](https://github.com/shawnlee222/latin-g2p/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/latin-g2p.svg)](https://pypi.org/project/latin-g2p/)
[![Python](https://img.shields.io/pypi/pyversions/latin-g2p.svg)](https://pypi.org/project/latin-g2p/)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)

Latin grapheme-to-phonology analysis utilities.

The public entry point is `latin_g2p.analyze(text)`. It normalizes the input,
segments it, resolves consonantal `i` and lexical `ui`, resolves `u/v` rules
including canonical `qu` and special `u`, groups Latin diphthongs and selected
lexical exceptions, syllabifies each word, assigns syllable weight and stress,
maps the result to IPA, and applies optional phonetic realization profiles.

```python
from latin_g2p import analyze

result = analyze("cuique")

[segment.graphemes for segment in result.segments]
# ["c", "ui", "qu", "e"]

[
    "".join(
        segment.graphemes
        for segment in syllable.segments
        if segment.emits_grapheme
    )
    for syllable in result.syllables
]
# ["cui", "que"]

result.get_ipa_string()
# "ˈkui̯.kʷɛ"
```

Public `Segment`, `Syllable`, `Word`, and `G2PResult` objects are immutable and
use tuple-backed collections. Intentional changes go through a detached mutable
graph and are validated when frozen again:

```python
editable = result.editable_copy()
editable.words[0].syllables[0].is_heavy = False
edited = editable.freeze()
```

The original `result` remains unchanged. The full reference and validation
contract is documented in [`docs/data_model.md`](docs/data_model.md).

Ordered batch analysis accepts any iterable of strings and returns a tuple of
ordinary `G2PResult` objects:

```python
from latin_g2p import analyze_many

results = analyze_many(text for text in ("arma", "maior", "suāvis"))
tuple(result.get_ipa_string() for result in results)
# ("ˈar.ma", "ˈmaj.jɔr", "ˈs̠waː.wɪs̠")
```

`analyze_many()` preserves input order and each result's warnings and selected
profile. It is fail-fast: invalid item types and analysis errors stop at the
first failing item without consuming later generator values. Passing one
string instead of an iterable of strings raises `TypeError` explicitly.

Known prose phrase or breath boundaries can be supplied without inserting
pronunciation symbols into the Latin text:

```python
from latin_g2p import ProsodicBreak, analyze

result = analyze(
    "Proximō diē īnstitūtō suō Caesar ē castrīs redūxit",
    prosodic_breaks=(ProsodicBreak(after_word_index=3, kind="breath"),),
)
```

Every connected-speech rule stops at that boundary, while the typed label is
preserved for downstream TTS conditioning. See
[`docs/prosodic_breaks.md`](docs/prosodic_breaks.md).

Analyzed results support a stable versioned dictionary and canonical JSON
representation:

```python
result = analyze("arma")
payload = result.to_dict()
payload["schema_version"]  # "1.7"
result.to_json()           # compact, sorted-key, Unicode-preserving JSON
```

Rule traces are opt-in and record both normalized and source spans:

```python
result = analyze("maior", trace=True)
[(event.rule_id, event.input_units, event.output_units) for event in result.traces]
# includes ("I_WEIGHT_BEARING_J", ("i",), ("jj",))
```

Installing the package also provides a CLI:

```bash
latin-g2p "arma virumque"
printf 'maior\nsuāvis\n' | latin-g2p --output-format jsonl
latin-g2p --trace --output-format jsonl maior
```

Plain text, JSONL, and TSV input/output contracts and pronunciation options are
documented in `docs/cli.md`.

Source-aligned canonical, historical, and conservative OCR normalization is
available independently from pronunciation profiles:

```python
from latin_g2p import NormalizationConfig

result = analyze(
    "Ænēās",
    normalization=NormalizationConfig(profile="historical"),
)

folded = analyze(
    "arma uirumque cano",
    normalization=NormalizationConfig(
        uv_input_style="all_u",
        uv_backend="collatinus",
    ),
)
folded.normalized_text  # "arma virumque cano"
```

See [`docs/normalization.md`](docs/normalization.md) for span and exact
replacement contracts and [`docs/uv_restoration.md`](docs/uv_restoration.md)
for backend design and the pinned UD comparison.

Optional profile settings can be passed with `G2PConfig`:

```python
from latin_g2p import G2PConfig, analyze

config = G2PConfig(rhotic_realization="singleton_tap")
analyze("rosa mare terra", config=config).get_ipa_string()
# "ˈɾɔ.s̠a ˈma.ɾɛ ˈt̪ɛr.ra"

config = G2PConfig(dark_l="narrow")
analyze("sōl altus", config=config).get_ipa_string()
# "ˈs̠oːˈɫ‿aɫ.t̪ʊs̠"

config = G2PConfig(nasal_coarticulation="on")
analyze("tam pater", config=config).get_ipa_string()
# "ˈt̪ãm ˈpa.t̪ɛr"

config = G2PConfig(cross_word_n_assimilation="broad")
analyze("Amazon Mezentius", config=config).get_ipa_string()
# broad connected-speech assimilation is explicit; the default narrow policy
# retains the content-word-final n in Amazon

config = G2PConfig(elision="on", est_prodelision="on")
analyze("bonum est", config=config).get_ipa_string()
# "ˈbɔ.nũ‿s̠t̪"

config = G2PConfig(final_m_vowel_length="short")
analyze("bonum", config=config).get_ipa_string()
# "ˈbɔ.nũ"

config = G2PConfig(final_m_strong_weakening="on")
analyze("tam pater", config=config).get_ipa_string()
# "ˈt̪ã ˈpa.t̪ɛr"

config = G2PConfig(enclitic_stress="pre_enclitic")
analyze("armaque", config=config).get_ipa_string()
# "arˈma.kʷɛ"

config = G2PConfig(enclitic_stress="host_secondary")
analyze("armaque", config=config).get_ipa_string()
# "ˈarˌma.kʷɛ"

config = G2PConfig(
    est_prodelision="on",
    final_m_vowel_length="long",
)
analyze("bonum est", config=config).get_ipa_string()
# "ˈbɔ.nũː‿s̠t̪"

from latin_g2p import analyze_variants

config = G2PConfig(elision="optional", est_prodelision="optional")
[
    result.get_ipa_string()
    for result in analyze_variants("multa amīca bona est", config=config)
]
# [
#   "ˈmʊɫ.t̪a aˈmiː.ka ˈbɔ.na ˈɛs̠t̪",
#   "ˈmʊɫ.t̪a aˈmiː.ka ˈbɔ.na‿s̠t̪",
#   "ˈmʊɫ.t̪‿aˈmiː.ka ˈbɔ.na ˈɛs̠t̪",
#   "ˈmʊɫ.t̪‿aˈmiː.ka ˈbɔ.na‿s̠t̪",
# ]

config = G2PConfig(
    simplify_w=True,
    simplify_h=True,
    uniform_vowel_quality="broad",
    simplify_z=True,
)
analyze("quod Phasis dominus gaza", config=config).get_ipa_string()
# "ˈkwod̪ ˈpha.s̠is̠ ˈd̪o.mi.nus̠ ˈɡa.za"

config = G2PConfig.simplified()
analyze("quod Phasis dominus gaza", config=config).get_ipa_string()
# "ˈkwod ˈpha.sis ˈdo.mi.nus ˈɡa.za"

config = G2PConfig.for_profile("classical_narrow")
analyze("sōl amor est tam pater", config=config).get_ipa_string()
# "ˈs̠oːˈl‿a.mɔˈr‿ɛs̠t̪ ˈt̪am ˈpa.t̪ɛr"

from latin_g2p import DEFAULT_PRONUNCIATION_PROFILE, PRONUNCIATION_PROFILES

DEFAULT_PRONUNCIATION_PROFILE  # "classical_narrow"
PRONUNCIATION_PROFILES
# ("classical_broad", "classical_middle", "classical_narrow", "simplified_tts")

config = G2PConfig.for_profile(
    "classical_middle",
    est_prodelision="optional",
)
result = analyze("bonum est", config=config)
result.pronunciation_profile
# "classical_middle"
```

Named profiles control pronunciation realization only. Global `h` realization
and its default connected-speech behavior belong to those profiles:
`classical_broad` and `simplified_tts` suppress plain Latin `h`;
`classical_middle` pronounces it in isolation but drops it in connected speech;
and `classical_narrow` retains it. A `connected_h="retain" | "drop"` override is
valid only when `h_realization="pronounced"`. Elision/prodelision, cross-word
resyllabification, enclitic accent policy, and input strictness remain explicit
independent options. `Word.syllables` preserves lexical syllabification;
`surface_syllables` and `prosodic_groups` record the connected surface
structure. `ipa_linking_marker="undertie" | "none"` controls whether connected
lexical boundaries are rendered with IPA `‿` or concatenated. The structured
boundary remains available in either mode; `simplified_tts` selects `none`.
All named profiles select `elision_realization="classical_conditioned"` when
elision is enabled, so `simplified_tts` initially follows the same
quantity-conditioned synaloepha policy and preserves non-syllabic or nasalized
transitions. Explicit `"residual_vowel"` instead retains every elided left
vowel as an extra-short non-syllabic transition: `[V̯̆]` for oral nuclei and
`[Ṽ̯̆]` before suppressed final `m`, while retaining `[j/w]` and `[j̃/w̃]`
for high-vowel glide environments. `"simple"` keeps oral high-vowel `[j]/[w]`
glides but deletes residues that require combining diacritics.
`"full_deletion"` remains available as a compatibility override.

### Input policy and normalized text

The accepted core alphabet is ASCII `a-z` plus `ā ē ī ō ū ȳ` after input
normalization. Circumflexes are normalized to macrons, breves and diaereses on
Latin vowels are removed, `j` is folded to `i`, and `v` with a macron is
normalized to `ū`. Other alphabetic scripts and unsupported Latin letters are
not assigned Latin phonology.

`G2PConfig.input_policy` controls unsupported alphabetic tokens:

- `passthrough` (default) preserves the complete token as `UNRESOLVED`.
- `warn` also attaches a structured warning to `result.warnings`.
- `strict` raises `UnsupportedAlphabeticError`.

```python
from latin_g2p import G2PConfig, analyze

result = analyze("Arma 한글", G2PConfig(input_policy="warn"))
result.source_text       # "Arma 한글"
result.normalized_text   # "arma 한글"
result.original          # "arma 한글" (compatibility alias)
result.get_ipa_string()  # "ˈar.ma 한글"
result.warnings[0].text  # "한글"
```

An internal phonological unit without an explicit IPA mapping raises
`UnknownPhonemeError`; it is never emitted as if it were valid IPA.

## Status

Core G2P v0.1 scope is implemented:

- NFC normalization, lowercasing, `j -> i`, and `v̄ -> ū`
- explicit all-u/all-v restoration with Collatinus candidates or LatinCy rules
- consonantal `i` and lexical `ui`
- canonical `qu`, canonical `v`, and Collatinus-derived special `u` behavior
- aspirated consonant digraph grouping: `ch`, `ph`, `rh`, `th`
- default diphthong grouping: `ae`, `au`, `oe`, `ei`
- lexical `eu` diphthongs for reviewed keys only
- lexical split exceptions for reviewed default-diphthong hiatus
- stable result-local Segment IDs assigned after all merge and expansion stages
- singular `mapped_ipa` and `surface_ipa` fragments on analyzed Segments
- word and syllable objects for downstream stress and phonology work
- syllable weight by long vowel, diphthong / lexical nucleus, and closed syllable
- Latin stress annotation by the penultimate rule, with configurable enclitic
  accentuation and secondary-stress output
- Classical Latin IPA mapping and syllable-aware IPA string output
- phonetic realization for final `m`, `ns` / `nf`, `n` place assimilation,
  `gn`, retracted Latin `/s/`, and reviewed lexical `s` voicing
- optional trill-versus-singleton-tap rhotic realization
- optional nasal coarticulation before retained lexical and assimilated nasal
  consonants
- optional general elision and `est`/`es` prodelision
- optional simplified output for labiovelars, aspirates, and short vowel quality
- `G2PConfig.simplified()` preset for the current minimal simplified output
- ordered, generator-compatible `analyze_many()` batch analysis

Detailed diphthong policy, including reviewed lexical `eu`, is tracked in
`docs/diphthongs.md`. Current syllable-boundary policy is tracked in
`docs/syllabification.md`. Current IPA output policy is tracked in
`docs/ipa.md`. Shared lexical evidence fields and their separation from
pronunciation policy are documented in `docs/evidence.md`. The result graph and
copy-on-transform contract are documented in `docs/data_model.md`. Serialization
schema v1 is documented in `docs/serialization.md`.

Known boundaries:

- Source-to-normalized character span mapping is not implemented yet; warning
  spans and current `Segment.start`/`end` values refer to normalized text.
- Historical spelling normalization, OCR/LLM cleanup, and morphology-aware
  prefix restoration are outside the core G2P module.
- TTS-specific inventories remain downstream concerns. Revision-pinned
  Kokoro/Piper tokenizer auditing and a research metrical-conditioning sidecar
  are documented in [`docs/tts_compatibility.md`](docs/tts_compatibility.md);
  the current `simplified_tts` profile still only simplifies the existing IPA
  output policy and is not a universal backend adapter.
- Lexical exception tables intentionally use reviewed exact keys instead of
  broad heuristics.

## Installation

Install the published package from PyPI:

```powershell
python -m pip install latin-g2p
```

For an editable checkout, run from the repository root:

```powershell
python -m pip install -e .
```

## Development

Install test dependencies and run the suite:

```powershell
python -m pip install -e ".[test]"
ruff check .
ruff format --check .
mypy
coverage run -m pytest -q
coverage report
```

The quality configuration targets Python 3.11 syntax, runs mypy in strict mode,
and enforces at least 94% branch coverage over `latin_g2p`. Python 3.11, 3.12,
3.13, and 3.14 are the currently supported and CI-tested versions. Newer Python
versions may work under the `>=3.11` package metadata but are not supported
until they enter the CI matrix.

GitHub Actions runs the version matrix, Ruff, mypy, branch coverage, sdist and
wheel builds, performance regression thresholds, packaged-TSV inspection, and
a clean-wheel installation smoke test. The packaging smoke test runs outside
the checkout so it cannot accidentally import the source tree instead of the
installed wheel. Benchmark methodology and local commands are documented in
[`docs/performance.md`](docs/performance.md).

Repeated normalized words reuse a bounded process-local analysis cache while
phrase-sensitive surface rules are recalculated for each context. Cache scope,
limits, and invalidation are documented in [`docs/cache.md`](docs/cache.md).

Scansion is an explicit downstream layer rather than an implicit property of
ordinary G2P. `build_scansion_input(result, meter="hexameter")` aligns lexical
syllables, connected surface syllables, and metrical options without changing
IPA, lexical weight, or stress. The structures and current solver scope are
documented in [`docs/scansion.md`](docs/scansion.md).

`scan(line, meter="hexameter")` additionally explores optional elision and
`est/es` contraction, generates rule-licensed quantity candidates, and returns
ranked six-foot solutions. It returns an empty tuple rather than lengthening
unlicensed syllables to force a match. Reviewed Virgilian occurrences are
licensed by an exact-line evidence inventory; other exceptional lengthening
must be licensed through `ScansionOverrides`.

Selected scans can be grouped into multi-line recording or synthesis samples
with `build_verse_feeder_sample()`. The same manifest exposes an inline
special-token sequence and token-aligned metrical feature rows for embedding-
conditioned models; see [`docs/verse_feeder.md`](docs/verse_feeder.md).

The test suite currently contains 889 tests.

## Release Notes

See `CHANGELOG.md`.

## Roadmap

Project-wide reliability, data-model, pronunciation-profile, and deployment
improvements are tracked in [`ROADMAP.md`](ROADMAP.md). Detailed rule research
and lexical review tasks remain in [`TODO.md`](TODO.md) and the topic documents
under [`docs/`](docs/).

External pronunciation datasets are evaluated as references rather than
assumed gold standards. See [`evaluation/README.md`](evaluation/README.md) for
the current Handbook of Latin Phonetics audit policy and tooling.

## License

This project is distributed under the GNU General Public License version 3. See
`LICENSE`.

The packaged special-`u` and u/v-restoration indexes contain generated records
derived from Collatinus data. The pinned LatinCy fallback rules are used under
the MIT License. See `NOTICE.md`, `docs/normalization.md`, and
`docs/u_v_rules.md` for attribution and data-policy notes.
