indic-phonetic 0.1.0__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.
- indic_phonetic-0.1.0/.gitignore +8 -0
- indic_phonetic-0.1.0/CHANGELOG.md +26 -0
- indic_phonetic-0.1.0/LICENSE +21 -0
- indic_phonetic-0.1.0/PKG-INFO +138 -0
- indic_phonetic-0.1.0/PLAN.md +107 -0
- indic_phonetic-0.1.0/README.md +104 -0
- indic_phonetic-0.1.0/benchmarks/data/README.md +8 -0
- indic_phonetic-0.1.0/benchmarks/data/districts.csv +746 -0
- indic_phonetic-0.1.0/benchmarks/data/handwritten_variants.csv +113 -0
- indic_phonetic-0.1.0/benchmarks/data/people.csv +301 -0
- indic_phonetic-0.1.0/benchmarks/data/place_aliases.csv +36 -0
- indic_phonetic-0.1.0/benchmarks/results/latest.md +27 -0
- indic_phonetic-0.1.0/benchmarks/run.py +166 -0
- indic_phonetic-0.1.0/pyproject.toml +52 -0
- indic_phonetic-0.1.0/src/indic_phonetic/__init__.py +16 -0
- indic_phonetic-0.1.0/src/indic_phonetic/data/confusions.csv +22 -0
- indic_phonetic-0.1.0/src/indic_phonetic/distance.py +376 -0
- indic_phonetic-0.1.0/src/indic_phonetic/explain.py +165 -0
- indic_phonetic-0.1.0/src/indic_phonetic/match.py +111 -0
- indic_phonetic-0.1.0/src/indic_phonetic/py.typed +0 -0
- indic_phonetic-0.1.0/src/indic_phonetic/translit.py +254 -0
- indic_phonetic-0.1.0/tests/test_distance.py +50 -0
- indic_phonetic-0.1.0/tests/test_explain.py +43 -0
- indic_phonetic-0.1.0/tests/test_match.py +85 -0
- indic_phonetic-0.1.0/tests/test_names.py +72 -0
- indic_phonetic-0.1.0/tests/test_translit.py +60 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-11-13)
|
|
4
|
+
|
|
5
|
+
First release on PyPI.
|
|
6
|
+
|
|
7
|
+
- Web demo (`demo/app.py`, Gradio): match against districts, people or your own list; compare two names.
|
|
8
|
+
- New `explain(a, b)`: the steps and reasons behind a distance.
|
|
9
|
+
- New `Matcher` class: prepare references once, match many times. About 5× faster (25 ms → 5 ms against 780 places).
|
|
10
|
+
- Equal distances now keep the order of the reference list.
|
|
11
|
+
- Fix: anusvara before h is ng (सिंह = singh) and before labials m (चंपा = champa).
|
|
12
|
+
- Fix: ज्ञ is gy (ज्ञान = gyaan).
|
|
13
|
+
- Fix: Tamil ங்க and ஞ்ச no longer double the consonant (வெங்கடேஷ் = vengaḍesh, was venggaḍesh).
|
|
14
|
+
- New confusion row t/ḍ for Tamil ட romanised t (Natarajan, Venkatesh).
|
|
15
|
+
- Unsupported scripts raise `UnsupportedScriptError` instead of being silently dropped; `script_of` names them.
|
|
16
|
+
- Titles and honorifics are ignored (Shri, Smt, Dr, ji, sahab, bhai, திரு, श्री…); joined honorifics (Rameshbhai) and Devi/Kumar/Kumari are optional for a small penalty.
|
|
17
|
+
- Initials match words with the same first letter (R. Kumar / Ramesh Kumar).
|
|
18
|
+
- Names of up to 4 words are compared in any order (was: 2).
|
|
19
|
+
- `distance` is rounded to 6 decimal places.
|
|
20
|
+
- New `sound_range`; `best_match` pruning uses it and stays exact.
|
|
21
|
+
|
|
22
|
+
## 0.0.1 (not published)
|
|
23
|
+
|
|
24
|
+
- Extracted from Vaani: transliteration (Devanagari, Tamil → phonetic Roman) and feature-weighted phonetic distance.
|
|
25
|
+
- `best_match` with exact length-bound pruning and key-grouped aliases.
|
|
26
|
+
- Benchmark against Levenshtein and Metaphone.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aman Trivedi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: indic-phonetic
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Match Indian names and places by how they sound, across Devanagari, Tamil and Roman script
|
|
5
|
+
Project-URL: Homepage, https://github.com/trivedi-aman/indic-phonetic
|
|
6
|
+
Project-URL: Changelog, https://github.com/trivedi-aman/indic-phonetic/blob/main/CHANGELOG.md
|
|
7
|
+
Project-URL: Roadmap, https://github.com/trivedi-aman/indic-phonetic/blob/main/PLAN.md
|
|
8
|
+
Project-URL: Issues, https://github.com/trivedi-aman/indic-phonetic/issues
|
|
9
|
+
Author-email: Aman Trivedi <amantrivedi2003@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: fuzzy matching,hindi,indic,names,phonetic,speech,tamil,transliteration
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: Hindi
|
|
16
|
+
Classifier: Natural Language :: Marathi
|
|
17
|
+
Classifier: Natural Language :: Tamil
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Provides-Extra: bench
|
|
27
|
+
Requires-Dist: jellyfish>=1; extra == 'bench'
|
|
28
|
+
Requires-Dist: rapidfuzz>=3; extra == 'bench'
|
|
29
|
+
Provides-Extra: demo
|
|
30
|
+
Requires-Dist: gradio>=6; extra == 'demo'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# indic-phonetic
|
|
36
|
+
|
|
37
|
+
Match Indian names and places by how they **sound**, not how they are spelled, across Devanagari, Tamil and Roman script. Pure Python, no dependencies.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from indic_phonetic import distance, best_match, to_roman
|
|
41
|
+
|
|
42
|
+
to_roman("बाड़मेर") # 'baaṛmer'
|
|
43
|
+
to_roman("முருகன்") # 'murugan'
|
|
44
|
+
|
|
45
|
+
distance("Barmer", "बाड़मेर") # 0.4 same place
|
|
46
|
+
distance("Barmer", "Baadmer") # 0.6 same place, different spelling
|
|
47
|
+
distance("Mohan", "Rohan") # 1.7 different people
|
|
48
|
+
|
|
49
|
+
best_match("Rohun Varma", [("C1", "Rohan Verma"), ("C2", "Mohan Verma"), ("C3", "Sohan Verma")])
|
|
50
|
+
# [Match(key='C1', text='Rohan Verma', distance=1.7),
|
|
51
|
+
# Match(key='C3', text='Sohan Verma', distance=2.85),
|
|
52
|
+
# Match(key='C2', text='Mohan Verma', distance=3.4)]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install indic-phonetic
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Status: alpha (0.1).** The API may still change before 1.0. Known gaps and the roadmap are in [PLAN.md](https://github.com/trivedi-aman/indic-phonetic/blob/main/PLAN.md).
|
|
60
|
+
|
|
61
|
+
## Try it
|
|
62
|
+
|
|
63
|
+
From a clone of the repository:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e '.[demo]'
|
|
67
|
+
python demo/app.py # opens http://127.0.0.1:7860
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Type a name or place as it was heard, in any supported script, and see the closest matches from Indian districts or a sample list of people, why the top one matched, and what a voice agent might do with it (accept, read back, ask again). A second tab compares two names step by step.
|
|
71
|
+
|
|
72
|
+
## Why
|
|
73
|
+
|
|
74
|
+
Speech-to-text and hand-typed records spell Indian names in many ways: *Barmer / Baadmer / बाड़मेर*, *Lakshmi / Laxmi / லட்சுமி*, *Vikram / Wikram*. Edit distance counts every letter change as equally bad, so it is wrong in both directions:
|
|
75
|
+
|
|
76
|
+
- **Barmer** and **Baadmer** are the same place: a long vowel plus the retroflex flap ड़ spelled two ways. That should be cheap.
|
|
77
|
+
- **Mohan** and **Rohan** are different people: *m* and *r* sound nothing alike, even though only one letter differs. That should be expensive.
|
|
78
|
+
|
|
79
|
+
English phonetic codes (Soundex, Metaphone) follow English spelling rules and don't know about retroflex consonants, aspiration or the silent *a* in Hindi.
|
|
80
|
+
|
|
81
|
+
## How it works
|
|
82
|
+
|
|
83
|
+
1. **Transliterate** Devanagari and Tamil into one phonetic Roman form. Hindi drops the silent *a* (schwa deletion: मोहन → *mohan*). Tamil voices stops between vowels (முருகன் → *murugan*). Retroflex sounds stay marked (ṭ ḍ ṇ ḷ ṛ).
|
|
84
|
+
2. **Tokenize** into sounds, folding English-style spellings first (*x→ks*, *-ey→e*, doubled letters).
|
|
85
|
+
3. **Score substitutions by articulatory features**: place and manner of articulation, voicing and aspiration for consonants; height, backness and length for vowels. A swap between similar sounds is cheap, a swap between unrelated sounds is expensive.
|
|
86
|
+
4. **Override with known confusions** ([`data/confusions.csv`](https://github.com/trivedi-aman/indic-phonetic/blob/main/src/indic_phonetic/data/confusions.csv)): v/w, t/ṭ, ph/f, Tamil k/g, and so on. Each row says why it's there.
|
|
87
|
+
5. **Weighted edit distance** over the sounds. Dropping a silent *a* or an *h* costs little.
|
|
88
|
+
6. **Words as people say them.** Titles and honorifics are ignored (*Shri*, *Smt*, *Dr*, *ji*, *sahab*). A joined honorific (*Rameshbhai*) costs 0.1 to drop, and *Devi* or *Kumar* cost 0.5. An initial matches a word with the same first letter (*R. Kumar* / *Ramesh Kumar* = 0.3). Words may come in any order (*Kumar Ramesh Singh*) for 0.2.
|
|
89
|
+
|
|
90
|
+
`best_match` skips references whose length alone rules them out. It gives the same result as scoring every reference, with far fewer comparisons.
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
| | |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `distance(a, b) -> float` | Phonetic distance between two names, in any mix of scripts. 0 means the same sounds. |
|
|
97
|
+
| `Matcher(references)` | Prepare a reference list once. References are strings or `(key, text)` pairs; keys group aliases (*Bangalore*, *Bengaluru*) under one result. |
|
|
98
|
+
| `Matcher.best_match(heard, top=3) -> list[Match]` | Closest references, nearest first; equal distances keep list order. |
|
|
99
|
+
| `best_match(heard, references, top=3)` | Shortcut for a one-off list. |
|
|
100
|
+
| `explain(a, b) -> Explanation` | The steps behind a distance: each sound kept, swapped, added or dropped, with the reason from the confusion table, plus titles ignored, words left out, initials and reordering. `print()` it for a readable table. |
|
|
101
|
+
| `to_roman(text) -> str` | Devanagari / Tamil / Latin → phonetic Roman. |
|
|
102
|
+
| `plain(text) -> str` | Remove the retroflex marks: *baaṛmer* → *baarmer*. |
|
|
103
|
+
| `script_of(text) -> str` | `"devanagari"`, `"tamil"` or `"latin"`. |
|
|
104
|
+
| `tokenize(word) -> tuple[str, ...]` | The sounds the distance is computed over. |
|
|
105
|
+
|
|
106
|
+
The library does not decide whether a match is good enough, because that threshold depends on your application. A voice agent might accept a match below about 0.8 when it is at least 1.0 ahead of the next candidate, read back anything below 1.8, and ask again otherwise. Those values come from the project this library was extracted from; tune them on your own data.
|
|
107
|
+
|
|
108
|
+
## Benchmark
|
|
109
|
+
|
|
110
|
+
782 items: 300 synthetic people, and 745 Indian districts plus aliases. Run it with `python benchmarks/run.py`. Full results are in [benchmarks/results/latest.md](https://github.com/trivedi-aman/indic-phonetic/blob/main/benchmarks/results/latest.md).
|
|
111
|
+
|
|
112
|
+
| Source | Kind | Items | Levenshtein top-1 | Metaphone top-1 | indic-phonetic top-1 |
|
|
113
|
+
| --- | --- | --- | --- | --- | --- |
|
|
114
|
+
| handwritten | name | 62 | 96.8% | 96.8% | 100.0% |
|
|
115
|
+
| handwritten | place | 50 | 92.0% | 74.0% | 100.0% |
|
|
116
|
+
| native script | name | 254 | 97.6% | 97.6% | 100.0% |
|
|
117
|
+
| rule-generated | name | 246 | 99.6% | 96.3% | 100.0% |
|
|
118
|
+
| rule-generated | place | 170 | 97.6% | 90.0% | 100.0% |
|
|
119
|
+
|
|
120
|
+
**Read these numbers with care.** None of the items are real speech-to-text output yet. The rule-generated variants use the same confusions the engine models, and the handwritten ones were written by the author. Building a benchmark from data the engine wasn't designed around is the top item in [PLAN.md](https://github.com/trivedi-aman/indic-phonetic/blob/main/PLAN.md). Speed is still behind: about 5 ms per lookup against 780 places with a `Matcher`, compared with 0.1 ms for Levenshtein. A compiled core is planned.
|
|
121
|
+
|
|
122
|
+
## Scope
|
|
123
|
+
|
|
124
|
+
- **Scripts:** Devanagari (Hindi, Marathi, …), Tamil, Latin. Other scripts are not supported yet and raise `UnsupportedScriptError`.
|
|
125
|
+
- **Made for:** names of people and places, short spoken phrases. It is not a general transliterator: use [indic-transliteration](https://pypi.org/project/indic-transliteration/) or [IndicXlit](https://github.com/AI4Bharat/IndicXlit) for running text.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m venv .venv && source .venv/bin/activate
|
|
131
|
+
pip install -e '.[dev,bench]'
|
|
132
|
+
pytest # tests marked xfail are known gaps from PLAN.md
|
|
133
|
+
python benchmarks/run.py
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT. The district list in `benchmarks/data` is derived from India Post's public pincode directory. All people in `benchmarks/data/people.csv` are synthetic.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Plan
|
|
2
|
+
|
|
3
|
+
Where the library is going, in order. Tick items off as they land; each phase ends with a release.
|
|
4
|
+
|
|
5
|
+
The engine was extracted from Vaani, a multilingual voice agent, where it matched spoken customer names and districts. Vaani-specific code (confidence thresholds, pincode cross-checks, customer data) stayed behind.
|
|
6
|
+
|
|
7
|
+
## Phase 0: Extraction ✅ (0.0.1)
|
|
8
|
+
|
|
9
|
+
- [x] Copy `translit.py` and `phonetic.py` (now `distance.py`) out of Vaani; load `confusions.csv` as package data
|
|
10
|
+
- [x] Generic `best_match` with length-bound pruning, `(key, text)` pairs for aliases and ids
|
|
11
|
+
- [x] Tests ported; known gaps recorded as strict `xfail` tests
|
|
12
|
+
- [x] Benchmark against Levenshtein and Metaphone, reproducing Vaani's numbers
|
|
13
|
+
|
|
14
|
+
## Phase 1: Correctness fixes (0.1.0)
|
|
15
|
+
|
|
16
|
+
Each one has an `xfail` test. Remove the marker when it's fixed. 1.3, 1.5 and 1.11 carry over to 0.2.0: 1.3 and 1.5 need the phase 2 data to judge.
|
|
17
|
+
|
|
18
|
+
- [x] **1.1 Anusvara before ह / fricatives.** `सिंह` → `sinh`, should be `singh` (distance to *Singh* is 1.25 today). Anusvara takes the place of the following consonant: ng before velars and h, m before labials, n otherwise.
|
|
19
|
+
- [x] **1.2 ज्ञ.** `ज्ञान` → `jnaan`, distance 2.85 to *Gyan*. Map ज्ञ to `gy`. Marathi says `dny` (*Dnyaneshwar*), so either add a confusion row gy/dny or take a language hint. *Done for Hindi; Marathi `dny` left to the language hint (4.4).*
|
|
20
|
+
- [ ] **1.3 Final-syllable vowels in surnames.** *Patil* / *Patel* = 0.35, the one false accept in Vaani's eval. Options: weight vowel changes higher in the last syllable, or add confusion rows that make specific pairs more expensive. Check that this doesn't break *Gupta/Gupt* or *Amit/Amita*.
|
|
21
|
+
- [x] **1.4 Fail loudly on unsupported scripts.** Telugu, Kannada, Bengali, … are silently dropped today. Raise an error, or return a result that says what was dropped. *Now raises `UnsupportedScriptError` (a `ValueError`) naming the script.*
|
|
22
|
+
- [ ] **1.5 Audit `_fold`.** The English-spelling rules (`-ey`, `-ow`, `c→k`) were written for the names in Vaani. Collect counterexamples. Found so far: *Verma* / वर्मा = 0.95 (English writes the schwa as *e*; also *Varma*), so the same name spelled two ways only gets a read-back.
|
|
23
|
+
- [x] **1.6 Tamil nasal + stop clusters.** `வெங்கடேஷ்` → `venggaḍesh` (distance 0.95 to *Venkatesh*, should be near 0): ங் gives `ng` and the following க is voiced to `g` as well. Check ஞ்ச, ந்த, ண்ட, ம்ப too. *Fixed ங்க and ஞ்ச (the others were already right); added confusion row t/ḍ 0.30 for Tamil voiced ட written t, so Venkatesh/வெங்கடேஷ் is 0.55.*
|
|
24
|
+
- [x] **1.7 Titles and honorifics.** *Shri Mohan Lal* vs *Mohan Lal* was 2.3, worse than *Mohan* vs *Rohan* (1.7). Titles (Shri, Smt, Dr, திரு, …) and honorific words (ji, sahab, bhai, …) are now dropped; joined honorifics (*Rameshbhai*, *Hansaben*) cost 0.1 to drop; *Devi*, *Kumar*, *Kumari* cost 0.5 to drop. Joined *ji* is kept (*Balaji*, *Shivaji*).
|
|
25
|
+
- [x] **1.8 Word order beyond two words.** *Kumar Ramesh Singh* vs *Ramesh Kumar Singh* was 10.2. Names of up to 4 words are now compared in every order, with a 0.2 penalty for any reorder.
|
|
26
|
+
- [x] **1.9 Initials.** *R. Kumar* vs *Ramesh Kumar* was 2.7. An initial now matches a word starting with the same letter for 0.3 (1.5 otherwise); *R.K.* splits into two initials. `best_match` pruning uses length ranges so it stays exact.
|
|
27
|
+
- [x] **1.10 Round `distance`.** It returned 1.7999999999999998; now rounded to 6 places like `best_match`.
|
|
28
|
+
- [ ] **1.11 Property tests** (Hypothesis): symmetry, `distance(a, a) == 0`, `to_roman` never raises on random Unicode, `best_match` equals brute force on random lists. Also the safety net for the Rust kernel (3.4).
|
|
29
|
+
|
|
30
|
+
## Phase 2: Honest benchmark (0.2.0)
|
|
31
|
+
|
|
32
|
+
This phase decides whether the library is actually better, so it matters more than any feature.
|
|
33
|
+
|
|
34
|
+
- [ ] **2.1 Real speech-to-text data.** Record or collect 100+ spoken names and places (Hindi, Marathi, Tamil, Indian English), run them through one or two STT services (Sarvam, Google, Whisper), and keep the transcripts with the intended name. Store the transcripts, not the audio.
|
|
35
|
+
- [ ] **2.2 Real spelling variants.** Find a public source of name variants (electoral rolls are not OK; look for open datasets, Wikipedia/Wikidata labels in several scripts, census place names).
|
|
36
|
+
- [ ] **2.3 Non-match test.** Names that are *not* in the reference list should get no close match. Report the distribution of best distances for them (Vaani's eval did this; port it).
|
|
37
|
+
- [ ] **2.4 Report precision at a threshold**, not just top-1. A matcher that always returns something looks good on top-1.
|
|
38
|
+
- [ ] **2.5 Compare against stronger baselines:** Jaro-Winkler, libindic soundex, and transliterate-then-Levenshtein using IndicXlit.
|
|
39
|
+
|
|
40
|
+
## Phase 3: Speed (0.3.0)
|
|
41
|
+
|
|
42
|
+
Was 25 ms per lookup against 780 places; 3.1, 3.2 and 4.1 brought it to about 5 ms (Levenshtein: 0.1 ms).
|
|
43
|
+
|
|
44
|
+
- [x] **3.1 Profile.** The biggest cost was `best_match` re-sorting all results after every reference (quadratic), then the DP and `sub_cost` lookups. Now: top-k kept incrementally, a token → int index with a dense cost matrix, and the DP stops early once it can't beat the current top-k.
|
|
45
|
+
- [x] **3.2 Cache tokenized references** inside `best_match` (or a `Matcher` object built once from the references).
|
|
46
|
+
- [ ] **3.3 Blocking for large lists** (10⁵+ names): index the first consonant classes or a coarse phonetic key, and only score that bucket.
|
|
47
|
+
- [ ] **3.4 Rust kernel (PyO3 + maturin).** Compile only the hot loop: weighted edit distance over int token ids, the word-swap logic, and batch scoring with pruning over a `Matcher`'s references. Cost tables are passed in as arrays, so tuning weights or learning them (phase 5) never touches Rust. Transliteration, folding and tokenizing stay in Python: they change often, contributors can fix them without Rust, and with 3.2 they run once per reference. Ship `abi3` wheels (Linux/macOS/Windows, x86 and ARM) from CI. The pure-Python kernel stays as the fallback and as the reference: a property test checks both give identical distances on random inputs. Not C/C++: Rust's packaging via maturin is simpler and the parser input is untrusted.
|
|
48
|
+
|
|
49
|
+
Order: 3.1 and 3.2 first (they define the kernel boundary), then 3.4, then 3.3. Blocking removes most comparisons, the kernel makes the rest about 100× cheaper; large lists need both.
|
|
50
|
+
|
|
51
|
+
## Phase 4: API and configurability (0.4.0)
|
|
52
|
+
|
|
53
|
+
- [x] **4.1 `Matcher` class** holding prepared references: `Matcher(refs).best_match(heard)`. `best_match` remains as a shortcut. Costs and confusions as parameters come with 4.2 / 5.1.
|
|
54
|
+
- [ ] **4.2 Custom confusion tables**: merge with or replace the defaults.
|
|
55
|
+
- [x] **4.3 `explain(a, b)`** that returns the alignment and each step's cost. Useful for debugging and for the README, and required by 5.4: learning counts substitutions from these alignments.
|
|
56
|
+
- [ ] **4.4 Language hint** (`lang="mr"`) for Marathi ज्ञ, Bengali v→b, and similar.
|
|
57
|
+
- [ ] **4.5 Decide on confidence grading.** Either keep it out (current choice) or offer `grade(matches, high=..., margin=...)` as an optional helper.
|
|
58
|
+
- [ ] **4.6 Command-line tool.** `python -m indic_phonetic Barmer बाड़मेर` prints the distance (and the alignment, once 4.3 exists); `python -m indic_phonetic --refs places.txt Baadmer` prints the best matches. For trying the library without writing code.
|
|
59
|
+
|
|
60
|
+
## Phase 5: Context and learned costs (0.5.0)
|
|
61
|
+
|
|
62
|
+
The engine is already a small model: feature weights and confusion costs, set by hand. Accents reach this library as systematic speech-to-text errors (Bengali v→b, Tamil voicing, sh/s in Bihar and eastern UP), which are exactly what those parameters describe. So learn the parameters from data and select them with metadata, rather than replacing the engine with a neural model. It stays fast, explainable and deterministic.
|
|
63
|
+
|
|
64
|
+
Depends on 4.1/4.2 (costs as a parameter object), 4.3 (alignments) and phase 2 data.
|
|
65
|
+
|
|
66
|
+
Regional patterns to start the profiles from (already in `confusions.csv` with one all-India cost; a profile makes them cheaper where they're common and leaves them alone elsewhere):
|
|
67
|
+
|
|
68
|
+
| Pattern | Where | Examples |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| v → b | Bihar, Bengal, Odisha, Assam | Vinod/Binod, Vikas/Bikash, Vishnoi/Bishnoi |
|
|
71
|
+
| ड़ / ड / र | Bihar, eastern UP | ghoḍa/ghora, paḍhai/parhai, saḍak/sarak |
|
|
72
|
+
| sh → s | Bihar, eastern UP, Tamil | Shyam/Syam, Shiva/Siva |
|
|
73
|
+
| voiced/unvoiced stops | Tamil | Kannan/Gannan, Palani/Balani |
|
|
74
|
+
|
|
75
|
+
- [ ] **5.1 `Costs` object.** Feature weights, indel costs and confusions in one versioned, serialisable object. `Costs.default()` reproduces today's numbers exactly.
|
|
76
|
+
- [ ] **5.2 Profiles selected by metadata.** `Matcher(refs, profile=...)` or per call: conversation language (`hi`, `mr`, `ta`, `bn`, …), caller region, and STT engine. Each STT engine has its own error patterns, so an engine profile may matter as much as an accent profile. Profiles layer on the default; unknown or missing metadata falls back to it.
|
|
77
|
+
- [ ] **5.3 Priors over references.** `best_match(heard, prior={key: weight})` for session context (caller's state or district, a name said earlier). **Soft, not a filter:** the prior's effect is capped so wrong metadata (a migrant from Bihar giving a Bengaluru address) costs a little ranking, never the correct match.
|
|
78
|
+
- [ ] **5.4 `Costs.fit(pairs, base=...)`.** Learn weights and confusion costs from confirmed (transcript, true name) pairs, regularised towards `base` so a few hundred pairs are enough. Align each pair with `explain` and count substitutions per profile (record *Vinod*, heard *Binod*, confirmed → one v→b observation in that region). Also learn from **corrected** read-backs as negatives, otherwise costs only ever go down: in Bihar and Bengal *Binod* is a real name, and a list holding both a Vinod and a Binod needs v/b to stay distinguishable. Output is a plain table that can be read and diffed.
|
|
79
|
+
- [ ] **5.5 Benchmark with metadata.** Run each item with correct, wrong, and no metadata. Ship a profile only if it helps with correct metadata and hurts little with wrong metadata.
|
|
80
|
+
- [ ] **5.6 Data collection (in Vaani, not here).** Log confirmed and corrected read-backs ("Did you say Barmer?" → yes / no, Baadmer) as labelled pairs, with consent and minimal fields. The library never collects data or learns at runtime.
|
|
81
|
+
- [ ] **5.7 Per-person aliases (in the app, documented here).** If customer C17 is recorded as *Vinod* and confirms *Binod*, the app stores *Binod* as an alias under C17's key. `best_match` already groups `(key, text)` aliases, so the next call matches at distance 0 without touching any weights. Document this pattern in the README; it's the safe form of learning per session.
|
|
82
|
+
|
|
83
|
+
Not planned: learning live inside a session. One call has too few names to learn from, labels are noisy (an uncorrected wrong match looks like a confirmation), a few odd sessions could shift weights for everyone, and the inputs are personal data. Session context goes in through 5.2 and 5.3 instead. A neural model is worth trying only if learned tables plateau on the phase 2 benchmark.
|
|
84
|
+
|
|
85
|
+
## Phase 6: More scripts (0.6.0+)
|
|
86
|
+
|
|
87
|
+
Each script needs a transliterator, tests with real names, and benchmark rows.
|
|
88
|
+
|
|
89
|
+
- [ ] Telugu, Kannada (close to Tamil's structure, but voiced stops are written)
|
|
90
|
+
- [ ] Bengali (inherent vowel is *o*; v→b)
|
|
91
|
+
- [ ] Gujarati, Gurmukhi (close to Devanagari)
|
|
92
|
+
- [ ] Malayalam
|
|
93
|
+
|
|
94
|
+
## Phase 7: Publish (1.0.0)
|
|
95
|
+
|
|
96
|
+
- [ ] GitHub repo, CI (pytest on 3.10–3.13, benchmark on PR)
|
|
97
|
+
- [ ] Type-check with mypy/pyright, lint with ruff
|
|
98
|
+
- [ ] Docs page: how the costs work, with the confusions table rendered
|
|
99
|
+
- [ ] Publish to PyPI (name `indic-phonetic` was free as of 2026-11-13)
|
|
100
|
+
- [ ] Write-up: blog post or short paper on the method and benchmark
|
|
101
|
+
- [ ] Switch Vaani to depend on the library instead of its own copy
|
|
102
|
+
|
|
103
|
+
## Open questions
|
|
104
|
+
|
|
105
|
+
- **Name.** `indic-phonetic` is descriptive and easy to find in search. A more memorable name (*dhvani* is taken on PyPI) could come later.
|
|
106
|
+
- **Units.** Distances are unbounded floats. Normalising by length (0–1) would make thresholds portable across short and long names, but it changes every number.
|
|
107
|
+
- **Weights.** The feature weights were set by hand. Learning them is planned in phase 5 (`Costs.fit`), once phase 2 data exists.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# indic-phonetic
|
|
2
|
+
|
|
3
|
+
Match Indian names and places by how they **sound**, not how they are spelled, across Devanagari, Tamil and Roman script. Pure Python, no dependencies.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from indic_phonetic import distance, best_match, to_roman
|
|
7
|
+
|
|
8
|
+
to_roman("बाड़मेर") # 'baaṛmer'
|
|
9
|
+
to_roman("முருகன்") # 'murugan'
|
|
10
|
+
|
|
11
|
+
distance("Barmer", "बाड़मेर") # 0.4 same place
|
|
12
|
+
distance("Barmer", "Baadmer") # 0.6 same place, different spelling
|
|
13
|
+
distance("Mohan", "Rohan") # 1.7 different people
|
|
14
|
+
|
|
15
|
+
best_match("Rohun Varma", [("C1", "Rohan Verma"), ("C2", "Mohan Verma"), ("C3", "Sohan Verma")])
|
|
16
|
+
# [Match(key='C1', text='Rohan Verma', distance=1.7),
|
|
17
|
+
# Match(key='C3', text='Sohan Verma', distance=2.85),
|
|
18
|
+
# Match(key='C2', text='Mohan Verma', distance=3.4)]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install indic-phonetic
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> **Status: alpha (0.1).** The API may still change before 1.0. Known gaps and the roadmap are in [PLAN.md](https://github.com/trivedi-aman/indic-phonetic/blob/main/PLAN.md).
|
|
26
|
+
|
|
27
|
+
## Try it
|
|
28
|
+
|
|
29
|
+
From a clone of the repository:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e '.[demo]'
|
|
33
|
+
python demo/app.py # opens http://127.0.0.1:7860
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Type a name or place as it was heard, in any supported script, and see the closest matches from Indian districts or a sample list of people, why the top one matched, and what a voice agent might do with it (accept, read back, ask again). A second tab compares two names step by step.
|
|
37
|
+
|
|
38
|
+
## Why
|
|
39
|
+
|
|
40
|
+
Speech-to-text and hand-typed records spell Indian names in many ways: *Barmer / Baadmer / बाड़मेर*, *Lakshmi / Laxmi / லட்சுமி*, *Vikram / Wikram*. Edit distance counts every letter change as equally bad, so it is wrong in both directions:
|
|
41
|
+
|
|
42
|
+
- **Barmer** and **Baadmer** are the same place: a long vowel plus the retroflex flap ड़ spelled two ways. That should be cheap.
|
|
43
|
+
- **Mohan** and **Rohan** are different people: *m* and *r* sound nothing alike, even though only one letter differs. That should be expensive.
|
|
44
|
+
|
|
45
|
+
English phonetic codes (Soundex, Metaphone) follow English spelling rules and don't know about retroflex consonants, aspiration or the silent *a* in Hindi.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
1. **Transliterate** Devanagari and Tamil into one phonetic Roman form. Hindi drops the silent *a* (schwa deletion: मोहन → *mohan*). Tamil voices stops between vowels (முருகன் → *murugan*). Retroflex sounds stay marked (ṭ ḍ ṇ ḷ ṛ).
|
|
50
|
+
2. **Tokenize** into sounds, folding English-style spellings first (*x→ks*, *-ey→e*, doubled letters).
|
|
51
|
+
3. **Score substitutions by articulatory features**: place and manner of articulation, voicing and aspiration for consonants; height, backness and length for vowels. A swap between similar sounds is cheap, a swap between unrelated sounds is expensive.
|
|
52
|
+
4. **Override with known confusions** ([`data/confusions.csv`](https://github.com/trivedi-aman/indic-phonetic/blob/main/src/indic_phonetic/data/confusions.csv)): v/w, t/ṭ, ph/f, Tamil k/g, and so on. Each row says why it's there.
|
|
53
|
+
5. **Weighted edit distance** over the sounds. Dropping a silent *a* or an *h* costs little.
|
|
54
|
+
6. **Words as people say them.** Titles and honorifics are ignored (*Shri*, *Smt*, *Dr*, *ji*, *sahab*). A joined honorific (*Rameshbhai*) costs 0.1 to drop, and *Devi* or *Kumar* cost 0.5. An initial matches a word with the same first letter (*R. Kumar* / *Ramesh Kumar* = 0.3). Words may come in any order (*Kumar Ramesh Singh*) for 0.2.
|
|
55
|
+
|
|
56
|
+
`best_match` skips references whose length alone rules them out. It gives the same result as scoring every reference, with far fewer comparisons.
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
| | |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `distance(a, b) -> float` | Phonetic distance between two names, in any mix of scripts. 0 means the same sounds. |
|
|
63
|
+
| `Matcher(references)` | Prepare a reference list once. References are strings or `(key, text)` pairs; keys group aliases (*Bangalore*, *Bengaluru*) under one result. |
|
|
64
|
+
| `Matcher.best_match(heard, top=3) -> list[Match]` | Closest references, nearest first; equal distances keep list order. |
|
|
65
|
+
| `best_match(heard, references, top=3)` | Shortcut for a one-off list. |
|
|
66
|
+
| `explain(a, b) -> Explanation` | The steps behind a distance: each sound kept, swapped, added or dropped, with the reason from the confusion table, plus titles ignored, words left out, initials and reordering. `print()` it for a readable table. |
|
|
67
|
+
| `to_roman(text) -> str` | Devanagari / Tamil / Latin → phonetic Roman. |
|
|
68
|
+
| `plain(text) -> str` | Remove the retroflex marks: *baaṛmer* → *baarmer*. |
|
|
69
|
+
| `script_of(text) -> str` | `"devanagari"`, `"tamil"` or `"latin"`. |
|
|
70
|
+
| `tokenize(word) -> tuple[str, ...]` | The sounds the distance is computed over. |
|
|
71
|
+
|
|
72
|
+
The library does not decide whether a match is good enough, because that threshold depends on your application. A voice agent might accept a match below about 0.8 when it is at least 1.0 ahead of the next candidate, read back anything below 1.8, and ask again otherwise. Those values come from the project this library was extracted from; tune them on your own data.
|
|
73
|
+
|
|
74
|
+
## Benchmark
|
|
75
|
+
|
|
76
|
+
782 items: 300 synthetic people, and 745 Indian districts plus aliases. Run it with `python benchmarks/run.py`. Full results are in [benchmarks/results/latest.md](https://github.com/trivedi-aman/indic-phonetic/blob/main/benchmarks/results/latest.md).
|
|
77
|
+
|
|
78
|
+
| Source | Kind | Items | Levenshtein top-1 | Metaphone top-1 | indic-phonetic top-1 |
|
|
79
|
+
| --- | --- | --- | --- | --- | --- |
|
|
80
|
+
| handwritten | name | 62 | 96.8% | 96.8% | 100.0% |
|
|
81
|
+
| handwritten | place | 50 | 92.0% | 74.0% | 100.0% |
|
|
82
|
+
| native script | name | 254 | 97.6% | 97.6% | 100.0% |
|
|
83
|
+
| rule-generated | name | 246 | 99.6% | 96.3% | 100.0% |
|
|
84
|
+
| rule-generated | place | 170 | 97.6% | 90.0% | 100.0% |
|
|
85
|
+
|
|
86
|
+
**Read these numbers with care.** None of the items are real speech-to-text output yet. The rule-generated variants use the same confusions the engine models, and the handwritten ones were written by the author. Building a benchmark from data the engine wasn't designed around is the top item in [PLAN.md](https://github.com/trivedi-aman/indic-phonetic/blob/main/PLAN.md). Speed is still behind: about 5 ms per lookup against 780 places with a `Matcher`, compared with 0.1 ms for Levenshtein. A compiled core is planned.
|
|
87
|
+
|
|
88
|
+
## Scope
|
|
89
|
+
|
|
90
|
+
- **Scripts:** Devanagari (Hindi, Marathi, …), Tamil, Latin. Other scripts are not supported yet and raise `UnsupportedScriptError`.
|
|
91
|
+
- **Made for:** names of people and places, short spoken phrases. It is not a general transliterator: use [indic-transliteration](https://pypi.org/project/indic-transliteration/) or [IndicXlit](https://github.com/AI4Bharat/IndicXlit) for running text.
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python -m venv .venv && source .venv/bin/activate
|
|
97
|
+
pip install -e '.[dev,bench]'
|
|
98
|
+
pytest # tests marked xfail are known gaps from PLAN.md
|
|
99
|
+
python benchmarks/run.py
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT. The district list in `benchmarks/data` is derived from India Post's public pincode directory. All people in `benchmarks/data/people.csv` are synthetic.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Benchmark data
|
|
2
|
+
|
|
3
|
+
| File | What | Source |
|
|
4
|
+
| --- | --- | --- |
|
|
5
|
+
| `districts.csv` | 745 Indian districts with state | Derived from India Post's public all-India pincode directory |
|
|
6
|
+
| `place_aliases.csv` | Common city names that differ from the district name (Bangalore, Trichy) | Hand-written |
|
|
7
|
+
| `people.csv` | 300 full names, with Devanagari or Tamil spelling for most | Synthetic, built from common first names and surnames; not real people |
|
|
8
|
+
| `handwritten_variants.csv` | ~110 heard → expected pairs: STT-style misspellings, surname first, old city names, native script | Hand-written |
|