Metadata-Version: 2.5
Name: unsaid
Version: 0.1.0
Summary: Find the characters your TTS model will silently refuse to say
Project-URL: Source, https://github.com/DevGurav/unsaid
Project-URL: Issues, https://github.com/DevGurav/unsaid/issues
Author: Devendra Ramesh Gurav
License: MIT License
        
        Copyright (c) 2026 Devendra Ramesh Gurav
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: i18n,testing,text-to-speech,tokenizer,tts,vocabulary
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: hf
Requires-Dist: transformers>=4.40; extra == 'hf'
Provides-Extra: test-real
Requires-Dist: pytest>=8.0; extra == 'test-real'
Requires-Dist: ruff>=0.6; extra == 'test-real'
Requires-Dist: transformers>=4.40; extra == 'test-real'
Description-Content-Type: text/markdown

# unsaid

[![CI](https://github.com/DevGurav/unsaid/actions/workflows/ci.yml/badge.svg)](https://github.com/DevGurav/unsaid/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

**Find out which characters your TTS model will silently refuse to say — before your users do.**

📊 **[Which TTS models can say a number?](https://devgurav.github.io/unsaid/)** — digit
coverage measured across 30 front-ends. Only 4 of 28 can say all ten digits.

Text-to-speech tokenizers drop what they cannot encode. They do not raise, they do not
warn, and they do not mark the gap in the output. The text you logged is correct; the
audio your user heard is not. There is no exception to catch and no assertion that fails.

This is not a rare edge case. `facebook/mms-tts` was never trained on numerals or special
characters at all, and the model card carries an open discussion titled *"The model has a
serious bug when handling tts with numbers"*. SpeechT5 has no number tokens and maps
unknown characters to `<unk>`. Hugging Face's own TTS guide acknowledges the problem and
tells you to write a script to check your dataset by hand.

`unsaid` is that script, packaged, with per-character attribution and a test assertion.

```
$ unsaid check facebook/mms-tts-mar "ही 500 रुपयांची नोट आहे."

  ही 500 रुपयांची नोट आहे.

  2 characters will not be spoken: '5' '.'

    '5' at index 3  (DIGIT, dropped)
    '.' at index 23  (PUNCTUATION, dropped)

  spoken as:  ही 00 रुपयांची नोट आहे

  DIGIT LOSS -- the amount is not degraded, it is changed.

  2 findings, severity high  (facebook/mms-tts-mar, high confidence)
```

That is real output, and it shipped in a production app for twelve days: an OCR'd ₹500
note was announced as "00 rupees", and an MRP of ₹10.30 as ₹100, with correct text printed
beside the wrong audio each time. A 237-test suite did not catch it, because every test
asserted on the text.

Note which digit vanished. `facebook/mms-tts-mar` is missing exactly `3`, `5` and `8` —
the other seven digits work fine. **That partial coverage is what makes this dangerous.**
Both zeros survived, so ₹500 became a well-formed "00 rupees" rather than obvious garbage;
₹10.30 lost only its `3` and became ₹100, a perfectly plausible price for something else.
Had every digit been missing, numbers would have come out empty and someone would have
noticed the same afternoon.

`unsaid coverage` reports that shape directly:

```
$ unsaid coverage facebook/mms-tts-mar

  facebook/mms-tts-mar  (vits, 73 characters)
  scripts: DEVANAGARI, LATIN

    digit                missing 3 5 8
    digit_devanagari     ENTIRE CLASS ABSENT
    latin_lower          ENTIRE CLASS ABSENT
    latin_upper          ENTIRE CLASS ABSENT
    punctuation          missing . , ? ! " : ; ( )
    whitespace           all present

  PARTIAL DIGIT COVERAGE -- some digits work and some do not, so numbers
  come out wrong rather than empty. This is the dangerous shape.
```

The Devanagari digits are absent too, so for this model there is no way to write a number
in Marathi that it will say.

## What this turns out to look like across models

`unsaid matrix` was run over 30 TTS front-ends — most of the widely used MMS-TTS
languages, plus SpeechT5. The rendered table is at
**[devgurav.github.io/unsaid](https://devgurav.github.io/unsaid/)**; the raw data is
[`data/matrix.json`](data/matrix.json). 28 could be analysed.

**Only 4 of the 28 can say all ten digits.**

| | |
| --- | --- |
| **18** | support *some* digits and not others — numbers come out wrong, not empty |
| 6 | have no digits at all — the safer failure, because it is obvious |
| 4 | all ten digits present |

Some specifics, because the aggregate undersells it:

- **`facebook/mms-tts-eng`** — English — is missing `7`, `8` and `9`.
- **`facebook/mms-tts-deu`** is missing exactly one digit: `9`. Ninety percent of numbers
  come out perfectly, which is precisely why nobody notices that the rest are wrong.
  `facebook/mms-tts-swh` has the same shape.
- **`facebook/mms-tts-vie`** can say `2`. Nothing else.
- `tel`, `pan` and `hau` can say `6` and nothing else.

None of these fail loudly. Every one of them silently returns a fluent sentence with a
different number in it.

Two models (`amh`, `kor`) are listed as not analysable rather than guessed at: they
romanise text before the vocabulary is consulted, so a character report would describe
nothing ([ADR 0005](docs/decisions/0005-decline-membership-when-the-front-end-rewrites.md)).

Reproduce the whole thing:

```sh
unsaid matrix --models data/models.txt --out data/matrix.json
python tools/build_site.py
```

## Install

```sh
pip install unsaid          # once the first release lands; see Status below
pip install "unsaid[hf]"    # to resolve Hugging Face model ids
```

## Use

### As a test assertion

This is the point of the library. It turns an invisible runtime failure into a red test.

```python
from unsaid import assert_speakable

def test_currency_announcement_is_audible():
    assert_speakable("ही 500 रुपयांची नोट आहे.", model="facebook/mms-tts-mar")
```

```
UnspeakableText: 2 characters will not be spoken by facebook/mms-tts-mar
  '5' at index 3  (DIGIT, dropped)
  '.' at index 23  (PUNCTUATION, dropped)
```

### Programmatically

```python
from unsaid import check, coverage

report = check("ही 500 रुपयांची नोट आहे.", model="facebook/mms-tts-mar")
report.ok               # False
report.dropped          # ['5', '.']
report.spoken           # 'ही 00 रुपयांची नोट आहे'
report.has_digit_loss   # True
print(report.render())  # the report above

cov = coverage("facebook/mms-tts-mar")
cov.digits.missing                  # ['3', '5', '8']
cov.partial_digits                  # True — the dangerous shape
cov.classes["latin_lower"].absent   # True, every Latin letter
```

### In CI

`unsaid check` exits non-zero when it finds loss, so it drops into a pipeline unchanged.

```sh
unsaid check facebook/mms-tts-mar --file strings/mr.txt
```

## The 30-second mental model

A TTS front-end applies a **declared normalisation** (case folding, Unicode form,
whitespace collapse) and then maps what remains onto a **fixed vocabulary**. Loss during
declared normalisation is intended and harmless. Loss *after* it is a bug, and it is
invisible.

`unsaid` models both halves per backend: it applies the backend's declared normalisation
itself, then diffs what survives the vocabulary against what went in. Everything missing
at that point is a finding, attributed to a specific character at a specific index — see
[`docs/architecture.md`](docs/architecture.md).

Findings are graded, and digits are graded hardest. A dropped diacritic degrades a word
you can still recognise. A dropped digit silently rewrites a quantity into a different,
equally plausible quantity, and nothing downstream can tell. That asymmetry is the
reasoning in [ADR 0004](docs/decisions/0004-severity-classes.md).

## Status

Not on PyPI yet — `pip install unsaid` will not work until the first release. The library
and CLI work today from a clone: `pip install -e ".[hf]"`. See
[`docs/roadmap.md`](docs/roadmap.md) for phase state and
[`docs/build-log.md`](docs/build-log.md) for the running narrative.

Supported front-ends and the confidence level of each are tracked in
[`docs/architecture.md`](docs/architecture.md#backends). VITS/MMS is the reference
implementation because it is the family the original failure came from.

## Docs

| | |
|---|---|
| [`docs/architecture.md`](docs/architecture.md) | Structure, the detection mechanism, backends, invariants |
| [`docs/roadmap.md`](docs/roadmap.md) | Phases and their state |
| [`docs/build-log.md`](docs/build-log.md) | What was built each session and why |
| [`docs/decisions/`](docs/decisions/) | ADRs — the settled questions |
| [`docs/api.md`](docs/api.md) | Python and CLI contract |
| [`docs/testing.md`](docs/testing.md) | What is tested at which level, and what is not |
| [`docs/glossary.md`](docs/glossary.md) | Terms, used consistently across code and docs |

## Licence

MIT — see [LICENSE](LICENSE).
