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.
- techiaith_speech_normalizer-0.2.0/.github/build-requirements.in +4 -0
- techiaith_speech_normalizer-0.2.0/.github/build-requirements.txt +40 -0
- techiaith_speech_normalizer-0.2.0/.github/dependabot.yml +23 -0
- techiaith_speech_normalizer-0.2.0/.github/workflows/ci.yml +61 -0
- techiaith_speech_normalizer-0.2.0/.github/workflows/publish.yml +120 -0
- techiaith_speech_normalizer-0.2.0/.gitignore +14 -0
- techiaith_speech_normalizer-0.2.0/CHANGELOG.md +47 -0
- techiaith_speech_normalizer-0.2.0/CITATION.cff +22 -0
- techiaith_speech_normalizer-0.2.0/COMPARISON.md +162 -0
- techiaith_speech_normalizer-0.2.0/CONTRIBUTING.md +140 -0
- techiaith_speech_normalizer-0.2.0/LICENSE +21 -0
- techiaith_speech_normalizer-0.2.0/PKG-INFO +372 -0
- techiaith_speech_normalizer-0.2.0/README.md +345 -0
- techiaith_speech_normalizer-0.2.0/SECURITY.md +48 -0
- techiaith_speech_normalizer-0.2.0/pyproject.toml +44 -0
- techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/__init__.py +9 -0
- techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/tokenization.py +33 -0
- techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/welsh_data.py +769 -0
- techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/welsh_normalizer.py +673 -0
- techiaith_speech_normalizer-0.2.0/src/techiaith/speech_normalizer/welsh_number_normalizer.py +587 -0
- techiaith_speech_normalizer-0.2.0/tests/__init__.py +0 -0
- techiaith_speech_normalizer-0.2.0/tests/test_normalizer.py +203 -0
- techiaith_speech_normalizer-0.2.0/tests/test_number_normalizer.py +204 -0
|
@@ -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,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.
|