techiaith-speech-normalizer 0.2.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.
Files changed (23) hide show
  1. techiaith_speech_normalizer-0.2.0/.github/build-requirements.in +4 -0
  2. techiaith_speech_normalizer-0.2.0/.github/build-requirements.txt +40 -0
  3. techiaith_speech_normalizer-0.2.0/.github/dependabot.yml +23 -0
  4. techiaith_speech_normalizer-0.2.0/.github/workflows/ci.yml +61 -0
  5. techiaith_speech_normalizer-0.2.0/.github/workflows/publish.yml +120 -0
  6. techiaith_speech_normalizer-0.2.0/.gitignore +14 -0
  7. techiaith_speech_normalizer-0.2.0/CHANGELOG.md +47 -0
  8. techiaith_speech_normalizer-0.2.0/CITATION.cff +22 -0
  9. techiaith_speech_normalizer-0.2.0/COMPARISON.md +162 -0
  10. techiaith_speech_normalizer-0.2.0/CONTRIBUTING.md +140 -0
  11. techiaith_speech_normalizer-0.2.0/LICENSE +21 -0
  12. techiaith_speech_normalizer-0.2.0/PKG-INFO +372 -0
  13. techiaith_speech_normalizer-0.2.0/README.md +345 -0
  14. techiaith_speech_normalizer-0.2.0/SECURITY.md +48 -0
  15. techiaith_speech_normalizer-0.2.0/pyproject.toml +44 -0
  16. techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/__init__.py +9 -0
  17. techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/tokenization.py +33 -0
  18. techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/welsh_data.py +769 -0
  19. techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/welsh_normalizer.py +673 -0
  20. techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/welsh_number_normalizer.py +587 -0
  21. techiaith_speech_normalizer-0.2.0/tests/__init__.py +0 -0
  22. techiaith_speech_normalizer-0.2.0/tests/test_normalizer.py +203 -0
  23. techiaith_speech_normalizer-0.2.0/tests/test_number_normalizer.py +204 -0
@@ -0,0 +1,4 @@
1
+ # Tools that .github/workflows/publish.yml uses to build the sdist and wheel.
2
+ # build-requirements.txt pins them with hashes; Dependabot keeps it up to date.
3
+ build
4
+ hatchling
@@ -0,0 +1,40 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.12
3
+ # by the following command:
4
+ #
5
+ # pip-compile --generate-hashes --output-file=.github/build-requirements.txt --strip-extras .github/build-requirements.in
6
+ #
7
+ build==1.6.1 \
8
+ --hash=sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53 \
9
+ --hash=sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7
10
+ # via -r .github/build-requirements.in
11
+ hatchling==1.33.0 \
12
+ --hash=sha256:415290b84f464d2884303fca8974d38e7b85579069fab02a15db0828fbb1446d \
13
+ --hash=sha256:4c422bd4d554fcff5de5b73e9596f85e41e9f97f7e553d8f7595ecb31a627bef
14
+ # via -r .github/build-requirements.in
15
+ packaging==26.3 \
16
+ --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
17
+ --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
18
+ # via
19
+ # build
20
+ # hatchling
21
+ pathspec==1.1.1 \
22
+ --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
23
+ --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
24
+ # via hatchling
25
+ pluggy==1.7.0 \
26
+ --hash=sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec \
27
+ --hash=sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8
28
+ # via hatchling
29
+ pyproject-hooks==1.3.3 \
30
+ --hash=sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b \
31
+ --hash=sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43
32
+ # via build
33
+ tomlkit==0.15.2 \
34
+ --hash=sha256:05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437 \
35
+ --hash=sha256:83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6
36
+ # via hatchling
37
+ trove-classifiers==2026.9.21.13 \
38
+ --hash=sha256:0a9ebc8d4e2f3e8a22848c5258033035bec17a3012ac3fea16dbaa764489eb71 \
39
+ --hash=sha256:8b1ff4f9c191b1040b71c37f1e445ab99732911e3cd91de52838453a854d7a17
40
+ # via hatchling
@@ -0,0 +1,23 @@
1
+ # The cooldown holds back new releases for a week, so a compromised release is
2
+ # likely to be found and pulled before it reaches this repository.
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: github-actions
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ cooldown:
10
+ default-days: 7
11
+ groups:
12
+ github-actions:
13
+ patterns: ["*"]
14
+
15
+ - package-ecosystem: pip
16
+ directory: /.github
17
+ schedule:
18
+ interval: weekly
19
+ cooldown:
20
+ default-days: 7
21
+ groups:
22
+ build-tools:
23
+ patterns: ["*"]
@@ -0,0 +1,61 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ name: Test (Python ${{ matrix.python-version }})
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ persist-credentials: false
24
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ - name: Install
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ python -m pip install -e . pytest
31
+ - name: Test
32
+ run: python -m pytest -q
33
+
34
+ build:
35
+ name: Build distribution
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39
+ with:
40
+ persist-credentials: false
41
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
42
+ with:
43
+ python-version: "3.12"
44
+ # Builds the same way as publish.yml, then checks the metadata.
45
+ - name: Build sdist and wheel
46
+ run: |
47
+ python -m pip install --require-hashes --only-binary :all: -r .github/build-requirements.txt
48
+ python -m build --no-isolation
49
+ python -m pip install twine
50
+ python -m twine check --strict dist/*
51
+
52
+ zizmor:
53
+ name: Check workflows (zizmor)
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
57
+ with:
58
+ persist-credentials: false
59
+ - uses: zizmorcore/zizmor-action@53290badfa369cb0bf7ad16342cb62c2fa47ac7b # v0.8.0
60
+ with:
61
+ advanced-security: false
@@ -0,0 +1,120 @@
1
+ # Publishes a release to PyPI and GitHub when a version tag (e.g. v0.3.0) is
2
+ # pushed. Each job gets only the permissions it needs:
3
+ #
4
+ # build checks the tag and builds the sdist and wheel with the
5
+ # hash-pinned tools in .github/build-requirements.txt;
6
+ # read-only token, no secrets
7
+ # publish-pypi uploads to PyPI with trusted publishing (OpenID Connect), so
8
+ # no API token is stored anywhere; runs in the "pypi"
9
+ # environment, which waits for a maintainer's approval
10
+ # github-release attests the files' build provenance and creates the GitHub
11
+ # release with the files and the notes from CHANGELOG.md
12
+ #
13
+ # PyPI trusted publisher: owner "techiaith", repository
14
+ # "techiaith-speech-normalizer", workflow "publish.yml", environment "pypi".
15
+ # See https://docs.pypi.org/trusted-publishers/
16
+
17
+ name: Publish
18
+
19
+ on:
20
+ push:
21
+ tags: ["v*"]
22
+
23
+ permissions: {}
24
+
25
+ jobs:
26
+ build:
27
+ name: Build distribution
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ contents: read
31
+ steps:
32
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
33
+ with:
34
+ fetch-depth: 0 # to check that the tagged commit is on main
35
+ persist-credentials: false
36
+ - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
37
+ with:
38
+ python-version: "3.12"
39
+ - name: Check the tag against main and the package version
40
+ run: |
41
+ if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
42
+ echo "$GITHUB_REF_NAME does not point to a commit on main" >&2
43
+ exit 1
44
+ fi
45
+ version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
46
+ if [ "$version" != "${GITHUB_REF_NAME#v}" ]; then
47
+ echo "pyproject.toml version ($version) does not match the tag ($GITHUB_REF_NAME)" >&2
48
+ exit 1
49
+ fi
50
+ citation=$(sed -n 's/^version: *//p' CITATION.cff | tr -d "\"'")
51
+ if [ "$citation" != "$version" ]; then
52
+ echo "CITATION.cff version ($citation) does not match pyproject.toml ($version)" >&2
53
+ exit 1
54
+ fi
55
+ if ! grep -q "^## \[$version\]" CHANGELOG.md; then
56
+ echo "CHANGELOG.md has no section for $version" >&2
57
+ exit 1
58
+ fi
59
+ - name: Build sdist and wheel
60
+ run: |
61
+ python -m pip install --require-hashes --only-binary :all: -r .github/build-requirements.txt
62
+ python -m build --no-isolation
63
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+ if-no-files-found: error
68
+
69
+ publish-pypi:
70
+ name: Publish to PyPI
71
+ needs: build
72
+ runs-on: ubuntu-latest
73
+ environment:
74
+ name: pypi
75
+ url: https://pypi.org/p/techiaith-speech-normalizer
76
+ permissions:
77
+ id-token: write # trusted publishing and PEP 740 attestations
78
+ steps:
79
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
80
+ with:
81
+ name: dist
82
+ path: dist/
83
+ - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
84
+
85
+ github-release:
86
+ name: Create GitHub release
87
+ needs: publish-pypi
88
+ runs-on: ubuntu-latest
89
+ permissions:
90
+ contents: write # create the release
91
+ id-token: write # sign the provenance attestation
92
+ attestations: write # store the provenance attestation
93
+ steps:
94
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
95
+ with:
96
+ persist-credentials: false
97
+ sparse-checkout: CHANGELOG.md
98
+ sparse-checkout-cone-mode: false
99
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
100
+ with:
101
+ name: dist
102
+ path: dist/
103
+ - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
104
+ with:
105
+ subject-path: dist/*
106
+ - name: Create the release
107
+ env:
108
+ GH_TOKEN: ${{ github.token }}
109
+ GH_REPO: ${{ github.repository }}
110
+ run: |
111
+ # The notes are this version's section of CHANGELOG.md.
112
+ awk -v heading="## [${GITHUB_REF_NAME#v}]" '
113
+ /^## \[/ { if (found) exit; found = (index($0, heading) == 1); next }
114
+ found
115
+ ' CHANGELOG.md > "$RUNNER_TEMP/notes.md"
116
+ # Attach the files while the release is a draft: once published, an
117
+ # immutable release cannot be changed.
118
+ gh release create "$GITHUB_REF_NAME" dist/* --draft --verify-tag \
119
+ --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md"
120
+ gh release edit "$GITHUB_REF_NAME" --draft=false
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .venv/
9
+ venv/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .DS_Store
14
+ uv.lock
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
5
+ and the project follows [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-10-29
10
+
11
+ First public release, on GitHub and PyPI. Changes since 0.1.0:
12
+
13
+ ### Added
14
+ - `WelshNumberNormalizer`: converts Welsh number words to digits (decimal and
15
+ vigesimal systems, ordinals, currency, mutated forms). Enabled in
16
+ `WelshNormalizer` with `normalize_numbers=True`.
17
+ - WER-only toggles, all off by default: `lowercase`, `strip_punctuation`,
18
+ `join_hyphenated_compounds`.
19
+ - Unicode NFKC normalisation as the first step of the pipeline, and a callable
20
+ `__call__` interface matching OpenAI Whisper's normalizers.
21
+ - Expansion of `{llafar|ysgrifenedig}` annotations, e.g. `{saith|7}` → `7`.
22
+ - `standardize_adjectives` and the `ADJECTIVE_REDUCTIONS` table, plus many new
23
+ vocabulary entries including possessive forms.
24
+ - Contractions `timod`/`t'mod`/`t'bo'`, `fan'na`, `'na beth yw`, `fel na` and
25
+ `ma' na`; fixes for `dwi'n`/`dwi'm`, `ma' 'na` → `mae yna`, and tokens with a
26
+ leading apostrophe (`'di`, `'na`).
27
+ - `COMPARISON.md`, comparing this normaliser with OpenAI Whisper's text
28
+ normalisers.
29
+ - GitHub Actions workflows for tests and for publishing releases to PyPI and
30
+ GitHub, plus `CONTRIBUTING.md`, `SECURITY.md` and `CITATION.cff`.
31
+
32
+ ### Changed
33
+ - Package renamed to the `techiaith.speech_normalizer` namespace.
34
+ - Python 3.9 or later is now required; support for Python 3.8 has been
35
+ dropped.
36
+ - The README now states that `remove_tags` and `remove_asterisks` are off by
37
+ default (as the code has always behaved), and its examples reflect the
38
+ current hesitation standardisation (`yy`/`yrm`/`ymm` → `yym`).
39
+ - The tests for tag and asterisk removal now enable those options explicitly,
40
+ and new tests check that both are kept by default.
41
+
42
+ ## [0.1.0] - 2026-05-19
43
+
44
+ ### Added
45
+ - Initial release: configurable `WelshNormalizer` for Welsh verbatim speech
46
+ transcriptions, SRT subtitle file support, uncertainty flagging and the
47
+ `techiaith-speech-normalize` command-line tool.
@@ -0,0 +1,22 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ title: "techiaith-speech-normalizer: Normaleiddiwr Lleferydd Cymraeg / Welsh Speech Normalizer"
4
+ type: software
5
+ authors:
6
+ - family-names: Jones
7
+ given-names: Dewi Bryn
8
+ affiliation: "Uned Technolegau Iaith, Prifysgol Bangor / Language Technologies Unit, Bangor University"
9
+ - family-names: Wanasky
10
+ given-names: Sasha
11
+ affiliation: "Prifysgol Bangor / Bangor University"
12
+ repository-code: "https://github.com/techiaith/techiaith-speech-normalizer"
13
+ license: MIT
14
+ version: 0.2.0
15
+ date-released: "2026-10-29"
16
+ keywords:
17
+ - welsh
18
+ - cymraeg
19
+ - speech recognition
20
+ - transcription
21
+ - text normalization
22
+ - nlp
@@ -0,0 +1,162 @@
1
+ # WelshNormalizer vs. Whisper Text Normalizers: Comparison & Future Work
2
+
3
+ ## Overview
4
+
5
+ This document compares `techiaith.speech_normalizer.WelshNormalizer` with OpenAI's Whisper text normalization classes: `BasicTextNormalizer` and `EnglishTextNormalizer`. The goal is to assess whether WelshNormalizer occupies the same role for Welsh that EnglishTextNormalizer does for English.
6
+
7
+ ## Whisper's Normalizers
8
+
9
+ ### BasicTextNormalizer (language-agnostic)
10
+
11
+ A minimal, language-agnostic normalizer that prepares text for WER (Word Error Rate) comparison. Its pipeline:
12
+
13
+ 1. **Lowercasing**
14
+ 2. **Annotation removal** — strips text within `<...>`, `[...]`, and `(...)`
15
+ 3. **Unicode normalization** — NFKD (with diacritic removal) or NFKC (preserving diacritics)
16
+ 4. **Symbol/punctuation stripping** — replaces all Unicode Mark, Symbol, and Punctuation characters with spaces
17
+ 5. **Optional grapheme splitting** — splits into individual grapheme clusters
18
+ 6. **Whitespace collapsing**
19
+
20
+ This normalizer performs no linguistic transformations — no contraction expansion, no spelling correction, no number handling. It is purely a text-cleaning pass.
21
+
22
+ ### EnglishTextNormalizer (English-specific)
23
+
24
+ A linguistically-aware normalizer for English speech transcription. Its pipeline:
25
+
26
+ 1. **Lowercasing**
27
+ 2. **Annotation removal** — same as BasicTextNormalizer
28
+ 3. **Filler word removal** — removes `hmm`, `mm`, `mhm`, `mmm`, `uh`, `um`
29
+ 4. **Contraction expansion** — 40+ patterns (`won't` → `will not`, `gonna` → `going to`, `'re` → `are`, etc.)
30
+ 5. **Abbreviation expansion** — titles and honorifics (`mr` → `mister`, `dr` → `doctor`, `prof` → `professor`, etc.)
31
+ 6. **Number-to-digit conversion** — a complex stateful sliding-window parser handling:
32
+ - Cardinal and ordinal numbers (`twenty million` → `20000000`, `third` → `3rd`)
33
+ - Currency (`twenty dollars` → `$20`, combining dollars and cents)
34
+ - Decimals (`three point five` → `3.5`)
35
+ - Nominal sequences (`double oh seven` → `007`)
36
+ - Percentage (`fifty percent` → `50%`)
37
+ 7. **British → American spelling** — dictionary lookup from a JSON file
38
+ 8. **Symbol cleanup and whitespace collapsing**
39
+
40
+ ## Feature-by-Feature Comparison
41
+
42
+ | Capability | BasicText | EnglishText | WelshNormalizer |
43
+ |---|:---:|:---:|:---:|
44
+ | Lowercasing | Yes | Yes | Optional toggle |
45
+ | Remove annotations/tags | `<>` `[]` `()` | `<>` `[]` `()` | `<>` tags |
46
+ | Unicode normalization (NFKD/NFKC) | Yes | Yes | Yes (NFKC) |
47
+ | Strip all punctuation/symbols | Yes | Selective | Optional toggle |
48
+ | Hesitation/filler removal | No | Yes | Yes |
49
+ | Hesitation standardization | No | No | Yes |
50
+ | Contraction expansion | No | ~40 patterns | ~130+ patterns |
51
+ | Pronoun normalization | No | No | Yes |
52
+ | Dialect → standard spelling | No | British → American | Dialect → Standard Welsh |
53
+ | Number-to-digit parsing | No | Yes (complex) | Yes (decimal, vigesimal, mutated forms) |
54
+ | Currency normalization | No | Yes | Yes (punt/ceiniog) |
55
+ | Abbreviation expansion (titles) | No | Yes | No |
56
+ | Multi-word phrase correction | No | No | Yes |
57
+ | Context-aware transforms | No | Partial | Yes |
58
+ | Uncertainty flagging | No | No | Yes |
59
+ | Per-category toggles | Minimal | No | Yes (22 toggles) |
60
+ | SRT subtitle file support | No | No | Yes |
61
+ | Custom user corrections | No | No | Yes |
62
+ | False start removal | No | No | Yes |
63
+ | Hyphenated compound joining | No | No | Optional toggle |
64
+
65
+ ## Where WelshNormalizer Matches or Exceeds
66
+
67
+ ### Contraction and dialect handling
68
+
69
+ EnglishTextNormalizer handles approximately 40 contraction patterns. WelshNormalizer handles 130+ across multiple categories: pronouns, verb contractions, `mae` contractions, other contractions, dialectal plurals (North `-a` and South `-e` → standard `-au`), dialectal verbs, internal vowel reductions, and spelling corrections. This reflects the greater distance between spoken and written Welsh compared to English.
70
+
71
+ ### Context-sensitive rules
72
+
73
+ Whisper's English normalizer uses ordered regex patterns to disambiguate contractions (e.g., matching `'d been` → `had been` before the generic `'d` → `would`). WelshNormalizer implements explicit contextual logic:
74
+
75
+ - **`'di` → `wedi`** only when preceded by a pronoun (`dwi 'di` → `dwi wedi`), left unchanged otherwise
76
+ - **`ma'` + follower** normalization using a lookup table that normalizes both `ma'` and its following word together
77
+
78
+ These are genuinely context-sensitive transformations that go beyond pattern ordering.
79
+
80
+ ### Uncertainty flagging
81
+
82
+ WelshNormalizer flags ambiguous cases for human review rather than guessing. For example, `'di` without a preceding pronoun is flagged as `CONTEXT_UNCERTAIN`. Whisper's normalizers have no equivalent — they always produce a deterministic output with no indication of confidence.
83
+
84
+ ### Configurability
85
+
86
+ Whisper's normalizers are essentially all-or-nothing (`BasicTextNormalizer` has two boolean options; `EnglishTextNormalizer` has none). WelshNormalizer offers 18 independent toggles, allowing researchers to measure the impact of each normalization category on downstream tasks like WER calculation.
87
+
88
+ ### Additional features
89
+
90
+ - **SRT file processing** — direct support for normalizing subtitle files
91
+ - **Custom corrections** — users can supply their own single-word and multi-word replacements
92
+ - **False start removal** — removes truncated words (`m-`, `o-`) common in spontaneous speech
93
+
94
+ ## Where WelshNormalizer Falls Short
95
+
96
+ ### ~~1. Unicode normalization~~ (DONE)
97
+
98
+ NFKC normalization is now applied as the first step in the pipeline, ensuring circumflexed vowels (`ŵ`, `â`, etc.) match lookup tables regardless of encoding.
99
+
100
+ ### ~~2. Number handling~~ (DONE)
101
+
102
+ A separate `WelshNumberNormalizer` class now handles:
103
+
104
+ - **Decimal system**: `tri deg pump` → `35`
105
+ - **Vigesimal system**: `pymtheg ar hugain` → `35`
106
+ - **Teens**: `un ar ddeg` → `11`, `deunaw` → `18`
107
+ - **Multipliers**: `mil naw cant pedwar deg tri` → `1943`
108
+ - **Ordinals**: `trydydd` → `3ydd`, `cyntaf` → `1af`
109
+ - **Currency**: `pum punt a deg ceiniog` → `£5.10`
110
+ - **Feminine forms**: `dwy`, `tair`, `pedair`, `trydedd`, etc.
111
+ - **Mutated forms**: soft mutation (`ddau`, `dri`, `fil`, `gyntaf`, etc.), aspirate mutation (`thri`, `phum`, `phedwar`, etc.)
112
+
113
+ Integrated into WelshNormalizer via an optional `normalize_numbers` toggle (off by default), following Whisper's compositional sub-normalizer pattern.
114
+
115
+ ### 3. Abbreviation and title expansion
116
+
117
+ **Gap:** No expansion of titles or common abbreviations.
118
+
119
+ **Impact:** Transcriptions may contain `Mr`, `Mrs`, `Dr`, `St` (sant/stryd), `Prif`, etc. These are not expanded.
120
+
121
+ **Recommendation:** Lower priority than number handling. Could be added as an additional lookup table in `welsh_data.py` if needed. Note that some Welsh abbreviations are more complex due to mutation (`Dr` → `Doctor`/`Ddoctor` depending on context).
122
+
123
+ ### ~~4. Lowercasing~~ (DONE)
124
+
125
+ Optional `lowercase` toggle added (off by default). When enabled, converts output to lowercase for WER calculation.
126
+
127
+ ### ~~5. Punctuation and symbol handling~~ (DONE)
128
+
129
+ Optional `strip_punctuation` toggle added (off by default). When enabled, strips all punctuation and symbols for WER calculation.
130
+
131
+ ## Architectural Comparison
132
+
133
+ | Aspect | Whisper normalizers | WelshNormalizer |
134
+ |---|---|---|
135
+ | Callable pattern | `__call__` (normalizer as function) | `__call__` + `.normalize()` |
136
+ | Composition | Composes sub-normalizers as classes | Composed `WelshNumberNormalizer` sub-normalizer |
137
+ | Data storage | Inline dictionaries + JSON file | Separate `welsh_data.py` module |
138
+ | Configuration | Constructor booleans (2-3) | Constructor booleans (18) |
139
+ | Return type | `str` | `str` or `Tuple[str, List[Flag]]` |
140
+ | Tokenization | Regex-based inline | Separate tokenizer module |
141
+
142
+ ### Architectural alignment (DONE)
143
+
144
+ 1. **`__call__`** — `WelshNormalizer` is now callable: `normalizer(text)` works alongside `normalizer.normalize(text)`.
145
+ 2. **Sub-normalizer composition** — `WelshNumberNormalizer` is a separate class composed into `WelshNormalizer`, following Whisper's pattern.
146
+
147
+ ## Summary
148
+
149
+ WelshNormalizer is a **WelshTextNormalizer** in the same sense that Whisper's `EnglishTextNormalizer` is an English-specific speech normalization layer. It occupies the same architectural role, handles the same category of problem (spoken-form → written-form normalization), and in several areas (contraction coverage, context sensitivity, configurability, uncertainty handling) exceeds what Whisper provides for English.
150
+
151
+ With NFKC Unicode normalization, number-to-digit conversion (including vigesimal and mutated forms), `__call__` API alignment, lowercasing, and punctuation stripping now implemented, **WelshNormalizer has reached effective parity with EnglishTextNormalizer**. The only remaining gap is title/abbreviation expansion, which is a minor completeness improvement.
152
+
153
+ ## Priority Roadmap
154
+
155
+ | Priority | Task | Effort | Impact | Status |
156
+ |---|---|---|---|---|
157
+ | 1 | Add NFKC Unicode normalization pass | Small | Robustness against encoding variation | Done |
158
+ | 2 | Add `__call__` method | Trivial | API consistency with Whisper | Done |
159
+ | 3 | Welsh number normalizer (decimal, vigesimal, mutations) | Large | Feature parity for number handling | Done |
160
+ | 4 | Optional lowercasing toggle | Small | WER calculation support | Done |
161
+ | 5 | Optional punctuation stripping | Small | WER calculation support | Done |
162
+ | 6 | Title/abbreviation expansion | Small | Minor completeness improvement | Pending |
@@ -0,0 +1,140 @@
1
+ # Cyfrannu / Contributing
2
+
3
+ ## Cymraeg
4
+
5
+ Croeso i gyfraniadau: adroddiadau am wallau, geiriau ac ymadroddion newydd ar
6
+ gyfer y tablau yn `welsh_data.py`, a gwelliannau i'r cod.
7
+
8
+ ### Gosod ar gyfer datblygu
9
+
10
+ ```bash
11
+ git clone https://github.com/techiaith/techiaith-speech-normalizer.git
12
+ cd techiaith-speech-normalizer
13
+ pip install -e . pytest
14
+ pytest
15
+ ```
16
+
17
+ Neu gydag [uv](https://docs.astral.sh/uv/): `uv run pytest`.
18
+
19
+ ### Ychwanegu geiriau neu reolau newydd
20
+
21
+ Mae'r holl dablau ieithyddol yn `src/techiaith/speech_normalizer/welsh_data.py`.
22
+ Ar ôl eu newid:
23
+
24
+ 1. Ychwanegwch brawf yn `tests/` ar gyfer y ffurf newydd.
25
+ 2. Rhedwch `pytest` a sicrhewch fod pob prawf yn pasio.
26
+
27
+ ### Ceisiadau tynnu (pull requests)
28
+
29
+ - Agorwch issue yn gyntaf ar gyfer newidiadau mawr.
30
+ - Cadwch bob cais yn fach ac yn canolbwyntio ar un peth.
31
+ - Ychwanegwch nodyn o dan "Unreleased" yn `CHANGELOG.md`.
32
+
33
+ ### Rhyddhau fersiwn newydd (cynhalwyr)
34
+
35
+ 1. Mewn cais tynnu, diweddarwch `version` yn `pyproject.toml`, diweddarwch
36
+ `version` a `date-released` yn `CITATION.cff`, a symudwch y nodiadau yn
37
+ `CHANGELOG.md` o "Unreleased" i bennawd y fersiwn newydd, gyda'r dyddiad.
38
+ 2. Ar ôl ei uno, tagiwch y commit newydd ar `main` a gwthiwch y tag:
39
+
40
+ ```bash
41
+ git switch main && git pull
42
+ git tag -a v0.3.0 -m v0.3.0
43
+ git push origin v0.3.0
44
+ ```
45
+
46
+ 3. Mae'r llif gwaith `.github/workflows/publish.yml` yn gwirio bod y tag ar
47
+ `main` a'i fod yn cyfateb i'r fersiwn yn `pyproject.toml`, `CITATION.cff` a
48
+ `CHANGELOG.md`, ac yna'n adeiladu'r pecyn. Mae cyhoeddi ar PyPI yn aros am
49
+ gymeradwyaeth yn yr amgylchedd `pypi` (y botwm "Review deployments" yn
50
+ rhediad y llif gwaith). Wedyn mae'r llif gwaith yn creu'r datganiad (release)
51
+ ar GitHub, gyda'r ffeiliau a'r nodiadau o `CHANGELOG.md`.
52
+
53
+ Dim ond gweinyddwyr yr ystorfa sy'n gallu gwthio tagiau `v*`. Ni ellir newid
54
+ datganiad ar ôl ei gyhoeddi, ar PyPI nac ar GitHub: rhaid rhyddhau fersiwn
55
+ newydd i gywiro camgymeriad.
56
+
57
+ Mae'r offer sy'n adeiladu'r pecyn wedi'u pinio gyda hashes yn
58
+ `.github/build-requirements.txt`, ac mae Dependabot yn cynnig diweddariadau
59
+ iddynt ac i'r GitHub Actions. I'w newid â llaw, golygwch
60
+ `.github/build-requirements.in` a rhedwch:
61
+
62
+ ```bash
63
+ uvx --python 3.12 --from pip-tools pip-compile --generate-hashes --strip-extras --output-file .github/build-requirements.txt .github/build-requirements.in
64
+ ```
65
+
66
+ Mae cyhoeddi ar PyPI yn defnyddio
67
+ [cyhoeddi dibynadwy (trusted publishing)](https://docs.pypi.org/trusted-publishers/),
68
+ felly nid oes tocyn API yn cael ei gadw yn unman. Mae'r cyhoeddwr dibynadwy ar
69
+ PyPI wedi'i osod i berchennog `techiaith`, ystorfa `techiaith-speech-normalizer`,
70
+ llif gwaith `publish.yml` ac amgylchedd `pypi`; os caiff ffeil y llif gwaith
71
+ neu'r amgylchedd eu hailenwi, bydd cyhoeddi'n methu nes eu diweddaru yno hefyd.
72
+
73
+ ## English
74
+
75
+ Contributions are welcome: bug reports, new words and phrases for the tables in
76
+ `welsh_data.py`, and improvements to the code.
77
+
78
+ ### Development setup
79
+
80
+ ```bash
81
+ git clone https://github.com/techiaith/techiaith-speech-normalizer.git
82
+ cd techiaith-speech-normalizer
83
+ pip install -e . pytest
84
+ pytest
85
+ ```
86
+
87
+ Or with [uv](https://docs.astral.sh/uv/): `uv run pytest`.
88
+
89
+ ### Adding words or rules
90
+
91
+ All the linguistic tables live in `src/techiaith/speech_normalizer/welsh_data.py`.
92
+ After changing them:
93
+
94
+ 1. Add a test in `tests/` for the new form.
95
+ 2. Run `pytest` and make sure everything passes.
96
+
97
+ ### Pull requests
98
+
99
+ - Open an issue first for larger changes.
100
+ - Keep each pull request small and focused on one thing.
101
+ - Add a note under "Unreleased" in `CHANGELOG.md`.
102
+
103
+ ### Releasing a new version (maintainers)
104
+
105
+ 1. In a pull request, update `version` in `pyproject.toml`, update `version`
106
+ and `date-released` in `CITATION.cff`, and move the notes in `CHANGELOG.md`
107
+ from "Unreleased" to a heading for the new version, with the date.
108
+ 2. Once it is merged, tag the new commit on `main` and push the tag:
109
+
110
+ ```bash
111
+ git switch main && git pull
112
+ git tag -a v0.3.0 -m v0.3.0
113
+ git push origin v0.3.0
114
+ ```
115
+
116
+ 3. The `.github/workflows/publish.yml` workflow checks that the tag is on
117
+ `main` and matches the versions in `pyproject.toml`, `CITATION.cff` and
118
+ `CHANGELOG.md`, then builds the package. Publishing to PyPI waits for
119
+ approval in the `pypi` environment (the "Review deployments" button on the
120
+ workflow run). The workflow then creates the GitHub release, with the files
121
+ and the notes from `CHANGELOG.md`.
122
+
123
+ Only repository admins can push `v*` tags. A release cannot be changed once it
124
+ is published, on PyPI or on GitHub: fix a mistake by releasing a new version.
125
+
126
+ The tools that build the package are pinned with hashes in
127
+ `.github/build-requirements.txt`, and Dependabot proposes updates to them and
128
+ to the GitHub Actions. To change them by hand, edit
129
+ `.github/build-requirements.in` and run:
130
+
131
+ ```bash
132
+ uvx --python 3.12 --from pip-tools pip-compile --generate-hashes --strip-extras --output-file .github/build-requirements.txt .github/build-requirements.in
133
+ ```
134
+
135
+ PyPI publishing uses
136
+ [trusted publishing](https://docs.pypi.org/trusted-publishers/), so no API
137
+ token is stored anywhere. The trusted publisher on PyPI is set to owner
138
+ `techiaith`, repository `techiaith-speech-normalizer`, workflow `publish.yml`
139
+ and environment `pypi`; renaming the workflow file or the environment breaks
140
+ publishing until it is updated there too.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Uned Technolegau Iaith, Prifysgol Bangor / Language Technologies Unit, Bangor University
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.