visqol-python 3.4.0__tar.gz → 3.5.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 (37) hide show
  1. {visqol_python-3.4.0 → visqol_python-3.5.0}/CHANGELOG.md +63 -0
  2. {visqol_python-3.4.0 → visqol_python-3.5.0}/PKG-INFO +78 -18
  3. {visqol_python-3.4.0 → visqol_python-3.5.0}/README.md +69 -16
  4. {visqol_python-3.4.0 → visqol_python-3.5.0}/pyproject.toml +16 -2
  5. {visqol_python-3.4.0 → visqol_python-3.5.0}/tests/test_conformance.py +50 -7
  6. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/__init__.py +1 -1
  7. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/__main__.py +21 -2
  8. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/api.py +21 -3
  9. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/gammatone.py +2 -2
  10. visqol_python-3.5.0/visqol/model/lattice_tcditugenmeetpackhref_ls2_nl60_lr12_bs2048_learn.005_ep2400_train1_7_raw.tflite +0 -0
  11. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/nsim.py +4 -1
  12. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/numba_accel.py +9 -3
  13. visqol_python-3.5.0/visqol/quality_mapper.py +228 -0
  14. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/signal_utils.py +8 -5
  15. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/visqol_manager.py +49 -4
  16. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/PKG-INFO +78 -18
  17. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/SOURCES.txt +1 -0
  18. visqol_python-3.5.0/visqol_python.egg-info/requires.txt +30 -0
  19. visqol_python-3.4.0/visqol/quality_mapper.py +0 -124
  20. visqol_python-3.4.0/visqol_python.egg-info/requires.txt +0 -16
  21. {visqol_python-3.4.0 → visqol_python-3.5.0}/CONTRIBUTING.md +0 -0
  22. {visqol_python-3.4.0 → visqol_python-3.5.0}/LICENSE +0 -0
  23. {visqol_python-3.4.0 → visqol_python-3.5.0}/MANIFEST.in +0 -0
  24. {visqol_python-3.4.0 → visqol_python-3.5.0}/setup.cfg +0 -0
  25. {visqol_python-3.4.0 → visqol_python-3.5.0}/tests/test_parallel_correctness.py +0 -0
  26. {visqol_python-3.4.0 → visqol_python-3.5.0}/tests/test_quick.py +0 -0
  27. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/alignment.py +0 -0
  28. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/analysis_window.py +0 -0
  29. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/audio_utils.py +0 -0
  30. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  31. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/patch_creator.py +0 -0
  32. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/patch_selector.py +0 -0
  33. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/py.typed +0 -0
  34. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/visqol_core.py +0 -0
  35. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/dependency_links.txt +0 -0
  36. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/entry_points.txt +0 -0
  37. {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/top_level.txt +0 -0
@@ -4,6 +4,68 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [3.5.0] - 2026-05-26
8
+
9
+ ### Added
10
+ - **Deep-lattice TFLite speech quality mapper** (`pip install visqol-python[lattice]`):
11
+ - `TFLiteSpeechQualityMapper` loads the same `.tflite` lattice network used by
12
+ C++ ViSQOL's default `--use_lattice_model=true` and runs inference through
13
+ the upstream Google TFLite C++ runtime via `ai-edge-litert`
14
+ - New `use_lattice_model` parameter on `VisqolApi.create()` (default `None`
15
+ auto-enables lattice when the runtime is installed)
16
+ - New `lattice_model_path` parameter to override the bundled model
17
+ - New CLI flags `--no_lattice_model` and `--lattice_model PATH`
18
+ - New `[lattice]` and `[all]` extras in `pyproject.toml`
19
+ - Bundled `lattice_*.tflite` (2.1 MB) into the wheel as package data
20
+
21
+ ### Fixed
22
+ - **GH issue #1**: Speech-mode MOS scores were systematically 1–2 points higher
23
+ than C++ ViSQOL's default. Root cause: the Python port only implemented the
24
+ legacy polynomial mapper (`SpeechSimilarityToQualityMapper`, equivalent to
25
+ C++ `--use_lattice_model=false`), while the C++ default routes through the
26
+ TFLite lattice network. Installing `visqol-python[lattice]` now matches C++
27
+ default scoring (CA01 conformance: diff 0.027 vs 1–2 MOS before).
28
+ - **`signal_utils.normalize()` parity bug**: the previous implementation did
29
+ min–max scaling to ``[0, 1]`` (shifting the signal positive and adding a DC
30
+ offset), while C++ ``MiscMath::Normalize`` only divides by the peak. This
31
+ inflated the RMS values fed to the speech-mode VAD, causing Python to keep
32
+ every patch as voice-active and adding spurious patches the C++ binary
33
+ would have discarded. Fixing this brought polynomial speech parity from
34
+ diff 0.007 → 0.001 and was a prerequisite for lattice parity. Only the
35
+ speech-mode VAD path used this function; audio mode is unaffected.
36
+ - **`nsim` stddev estimator mismatch**: both ``nsim.measure_patch_similarity``
37
+ (``np.std(..., ddof=0)``) and the Numba ``_measure_patch_similarity_numba``
38
+ kernel (``sqrt(ss / cols)``) used the population estimator (divide by N).
39
+ C++ uses Armadillo's ``stddev(..., 0)`` which is the *unbiased* sample
40
+ estimator (divide by N-1, despite the misleading ``0`` flag). The
41
+ per-band ``freq_band_stddevs`` was therefore systematically smaller by
42
+ ``sqrt((N-1)/N) ≈ 0.974``, which fed into the pooled ``fstdnsim`` and
43
+ perturbed every lattice prediction.
44
+ - **`numba_accel.fastmath=True` on the Gammatone spectrogram kernel**: the
45
+ compounded LLVM-level FP reassociation across the 4-stage cascaded IIR ×
46
+ thousands of samples × hundreds of frames pushed lattice MOS off by
47
+ another ~0.02 vs strict IEEE-754. ``fastmath`` has been removed from the
48
+ spectrogram kernel; ``parallel=True`` is kept (each frame's IIR state is
49
+ independent so the reduction is safe).
50
+
51
+ ### Speech-mode parity numbers (CA01 conformance)
52
+
53
+ | Mode | Before all fixes | After all fixes | C++ baseline |
54
+ |------|------------------|-----------------|--------------|
55
+ | Polynomial | diff 0.0067 | **diff 0.0011** | 3.3745 |
56
+ | Lattice | diff 0.0856 (≈1–2 MOS on Nils's TCD-VOIP samples) | **diff 0.0023** | 3.3130 |
57
+
58
+ ### Changed
59
+ - Speech mode `create(mode="speech")` now auto-uses lattice when available; when
60
+ `ai-edge-litert` is missing, it logs a one-time warning and falls back to
61
+ polynomial (existing scores reproduce exactly).
62
+ - `tests/test_conformance.py` split the single speech case into
63
+ `test_speech_polynomial_conformance` (existing C++ polynomial baseline 3.3745)
64
+ and `test_speech_lattice_conformance` (regression baseline captured from this
65
+ implementation).
66
+ - README: documented the polynomial-vs-lattice distinction, new install matrix,
67
+ and parity caveats.
68
+
7
69
  ## [3.4.0] - 2026-03-23
8
70
 
9
71
  ### Added
@@ -93,6 +155,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
93
155
  - Bundled SVR model (`libsvm_nu_svr_model.txt`)
94
156
  - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
95
157
 
158
+ [3.5.0]: https://github.com/talker93/visqol-python/compare/v3.4.0...v3.5.0
96
159
  [3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
97
160
  [3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
98
161
  [3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.4.0
3
+ Version: 3.5.0
4
4
  Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
5
5
  Author: Shan Jiang
6
6
  License-Expression: Apache-2.0
@@ -9,7 +9,7 @@ Project-URL: Changelog, https://github.com/talker93/visqol-python/blob/main/CHAN
9
9
  Project-URL: Bug Reports, https://github.com/talker93/visqol-python/issues
10
10
  Project-URL: Source, https://github.com/talker93/visqol-python
11
11
  Project-URL: Original C++, https://github.com/google/visqol
12
- Keywords: audio-quality,speech-quality,MOS,PESQ,POLQA,visqol,objective-metric,perceptual-quality
12
+ Keywords: audio-quality,speech-quality,MOS,PESQ,POLQA,visqol,objective-metric,perceptual-quality,numba,batch-processing
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Science/Research
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
23
23
  Classifier: Topic :: Scientific/Engineering
24
+ Classifier: Typing :: Typed
24
25
  Requires-Python: >=3.9
25
26
  Description-Content-Type: text/markdown
26
27
  License-File: LICENSE
@@ -30,6 +31,11 @@ Requires-Dist: soundfile>=0.10
30
31
  Requires-Dist: libsvm-official>=3.25
31
32
  Provides-Extra: accel
32
33
  Requires-Dist: numba>=0.57; extra == "accel"
34
+ Provides-Extra: lattice
35
+ Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "lattice"
36
+ Provides-Extra: all
37
+ Requires-Dist: numba>=0.57; extra == "all"
38
+ Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "all"
33
39
  Provides-Extra: test
34
40
  Requires-Dist: pytest>=7.0; extra == "test"
35
41
  Provides-Extra: dev
@@ -37,6 +43,7 @@ Requires-Dist: pytest>=7.0; extra == "dev"
37
43
  Requires-Dist: ruff>=0.4; extra == "dev"
38
44
  Requires-Dist: mypy>=1.8; extra == "dev"
39
45
  Requires-Dist: numba>=0.57; extra == "dev"
46
+ Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "dev"
40
47
  Dynamic: license-file
41
48
 
42
49
  # ViSQOL (Python)
@@ -46,19 +53,25 @@ Dynamic: license-file
46
53
  [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
47
54
  [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
48
55
 
49
- A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) v3.3.3 for objective audio/speech quality assessment.
56
+ A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) for objective audio/speech quality assessment.
50
57
 
51
58
  ViSQOL compares a reference audio signal with a degraded version and outputs a **MOS-LQO** (Mean Opinion Score - Listening Quality Objective) score on a scale of **1.0 – 5.0**.
52
59
 
53
60
  ## Features
54
61
 
55
62
  - **Two modes**: Audio mode (music/general audio at 48 kHz) and Speech mode (speech at 16 kHz)
56
- - **High accuracy**: 11/11 conformance tests pass against the official C++ implementation
63
+ - **High accuracy**: 12/12 conformance tests pass against the official C++ implementation
57
64
  - Audio mode: 9/10 tests produce **identical** MOS scores (diff = 0.000000), 1 test diff = 0.000117
58
- - Speech mode: diff = 0.006715
59
- - **Pure Python**: no C/C++ compilation required
60
- - **Minimal dependencies**: only 4 pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
61
- - **Faster than real-time**: Audio RTF 0.71x, Speech RTF 0.38x
65
+ - Speech mode (polynomial): diff = 0.001057
66
+ - Speech mode (lattice TFLite): diff = 0.002341
67
+ - **Two speech quality mappers** matching C++ ViSQOL:
68
+ - **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
69
+ - **Polynomial (fallback)** — legacy exponential fit (`--use_lattice_model=false` in C++)
70
+ - **Pure Python**: no C/C++ compilation required (the optional `[lattice]` extra adds the Google `ai-edge-litert` TFLite runtime as a binary wheel)
71
+ - **Minimal dependencies**: 4 core pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
72
+ - **Optional Numba acceleration**: `pip install visqol-python[accel]` for JIT-compiled Gammatone filterbank (parallel + fastmath) and DP patch matching — **9× overall speedup**, RTF 0.064 (surpasses C++ estimates)
73
+ - **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
74
+ - **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
62
75
 
63
76
  ## Installation
64
77
 
@@ -66,14 +79,34 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
66
79
  pip install visqol-python
67
80
  ```
68
81
 
82
+ For **C++-default-equivalent speech mode** (deep-lattice TFLite mapper):
83
+
84
+ ```bash
85
+ pip install visqol-python[lattice] # requires Python ≥ 3.10
86
+ ```
87
+
88
+ For **Numba-accelerated** Gammatone filtering and DP matching (~9× faster):
89
+
90
+ ```bash
91
+ pip install visqol-python[accel]
92
+ ```
93
+
94
+ Install everything (lattice + numba):
95
+
96
+ ```bash
97
+ pip install visqol-python[all]
98
+ ```
99
+
69
100
  Or install from source:
70
101
 
71
102
  ```bash
72
103
  git clone https://github.com/talker93/visqol-python.git
73
104
  cd visqol-python
74
- pip install -e .
105
+ pip install -e ".[dev]"
75
106
  ```
76
107
 
108
+ > **Note on speech mode parity**: Without the `[lattice]` extra, speech mode falls back to the polynomial mapping (equivalent to running C++ ViSQOL with `--use_lattice_model=false`). The polynomial can over-predict MOS by 1–2 points on degraded speech vs the C++ default. Install `[lattice]` whenever you need numbers that line up with the C++ default behaviour (see [issue #1](https://github.com/talker93/visqol-python/issues/1)).
109
+
77
110
  ## Quick Start
78
111
 
79
112
  ### Python API
@@ -124,12 +157,15 @@ file_pairs = [
124
157
  ("ref3.wav", "deg3.wav"),
125
158
  ]
126
159
 
127
- # Optional progress callback
160
+ # Sequential with progress callback
128
161
  results = api.measure_batch(
129
162
  file_pairs,
130
163
  progress_callback=lambda done, total: print(f"{done}/{total}"),
131
164
  )
132
165
 
166
+ # Multi-process parallel (uses all CPU cores)
167
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
168
+
133
169
  for pair, result in zip(file_pairs, results):
134
170
  if isinstance(result, Exception):
135
171
  print(f"{pair}: FAILED — {result}")
@@ -156,7 +192,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
156
192
  |------|-------------|
157
193
  | `-r`, `--reference` | Path to reference WAV file (required) |
158
194
  | `-d`, `--degraded` | Path to degraded WAV file (required) |
159
- | `--speech_mode` | Use speech mode (16 kHz, polynomial mapping) |
195
+ | `--speech_mode` | Use speech mode (16 kHz) |
196
+ | `--no_lattice_model` | Speech mode: disable lattice TFLite mapper, use polynomial fallback |
197
+ | `--lattice_model` | Custom path to lattice `.tflite` model (speech mode) |
198
+ | `--unscaled_speech` | Don't scale polynomial speech MOS to 5.0 (polynomial only) |
160
199
  | `--model` | Custom SVR model file path (audio mode only) |
161
200
  | `--search_window` | Search window radius (default: 60) |
162
201
  | `--verbose`, `-v` | Show detailed per-patch results |
@@ -184,21 +223,34 @@ The `measure()` method returns a `SimilarityResult` object with:
184
223
 
185
224
  ### Speech Mode
186
225
  - Target sample rate: **16 kHz**
187
- - 32 Gammatone frequency bands (50 Hz – 8 000 Hz)
188
- - Quality mapping: exponential polynomial fit
226
+ - 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
189
227
  - VAD (Voice Activity Detection) based patch selection
228
+ - Quality mapping (choose one):
229
+ - **Deep-lattice TFLite (default)** — same mapper as C++ ViSQOL's default `--use_lattice_model=true`; requires `pip install visqol-python[lattice]`
230
+ - **Exponential polynomial (fallback)** — same as C++ `--use_lattice_model=false`; used automatically when the lattice runtime is not installed
231
+ - Toggle from Python: `api.create(mode="speech", use_lattice_model=False)`
232
+ - Toggle from CLI: `--no_lattice_model`
190
233
  - Best for: speech, VoIP, telephony
191
234
 
192
235
  ## Performance
193
236
 
194
237
  Measured on Apple M-series, Python 3.13:
195
238
 
239
+ ### Without Numba (pure Python + NumPy/SciPy)
240
+
196
241
  | Mode | Avg RTF | Typical Time |
197
242
  |------|---------|-------------|
198
- | Audio (48 kHz) | **0.71x** | 7 – 12 s per file pair |
243
+ | Audio (48 kHz) | **0.18x** | ~2.2 s per file pair |
199
244
  | Speech (16 kHz) | **0.38x** | ~1 s per file pair |
200
245
 
246
+ ### With Numba (`pip install visqol-python[accel]`)
247
+
248
+ | Mode | Avg RTF | Typical Time | Speedup |
249
+ |------|---------|-------------|---------|
250
+ | Audio (48 kHz) | **0.064x** | ~0.8 s per file pair | **9×** |
251
+
201
252
  > RTF (Real-Time Factor) < 1.0 means faster than real-time.
253
+ > With Numba acceleration, the Python implementation **surpasses C++ estimated performance** (RTF ≈ 0.093).
202
254
 
203
255
  ## Project Structure
204
256
 
@@ -218,18 +270,23 @@ visqol-python/
218
270
  │ ├── alignment.py # Global alignment via cross-correlation
219
271
  │ ├── nsim.py # NSIM similarity metric
220
272
  │ ├── quality_mapper.py # SVR & exponential quality mapping
273
+ │ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
221
274
  │ ├── __main__.py # CLI entry point
275
+ │ ├── py.typed # PEP 561 type marker
222
276
  │ └── model/ # Bundled SVR model
223
277
  │ └── libsvm_nu_svr_model.txt
224
- ├── tests/ # Tests (pytest)
278
+ ├── tests/ # Tests & benchmarks (pytest)
225
279
  │ ├── conftest.py # Shared fixtures & CLI options
226
280
  │ ├── test_quick.py # Smoke tests (no external data needed)
227
- └── test_conformance.py # Full conformance tests (needs testdata)
281
+ ├── test_conformance.py # Full conformance tests (needs testdata)
282
+ │ ├── test_parallel_correctness.py # Numba parallel correctness tests
283
+ │ └── bench_*.py # Performance benchmarks
228
284
  ├── .github/workflows/
229
- │ ├── ci.yml # CI: test on Python 3.9–3.13
285
+ │ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
230
286
  │ └── publish.yml # Auto-publish to PyPI on tag push
231
287
  ├── pyproject.toml # Package metadata & build config
232
288
  ├── CHANGELOG.md
289
+ ├── CONTRIBUTING.md
233
290
  ├── LICENSE
234
291
  └── README.md
235
292
  ```
@@ -250,7 +307,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
250
307
  | glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
251
308
  | contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
252
309
  | castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
253
- | speech_CA01 | Speech | 3.3745 | 3.3678 | 0.006715 |
310
+ | speech_CA01 (polynomial) | Speech | 3.3745 | 3.3756 | 0.001057 |
311
+ | speech_CA01 (lattice) | Speech | 3.3130 | 3.3153 | 0.002341 |
312
+
313
+ Both speech values come from running the C++ ViSQOL binary directly with the corresponding `--use_lattice_model` flag, so they represent ground-truth parity targets.
254
314
 
255
315
  ## References
256
316
 
@@ -5,19 +5,25 @@
5
5
  [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
6
6
  [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
7
7
 
8
- A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) v3.3.3 for objective audio/speech quality assessment.
8
+ A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) for objective audio/speech quality assessment.
9
9
 
10
10
  ViSQOL compares a reference audio signal with a degraded version and outputs a **MOS-LQO** (Mean Opinion Score - Listening Quality Objective) score on a scale of **1.0 – 5.0**.
11
11
 
12
12
  ## Features
13
13
 
14
14
  - **Two modes**: Audio mode (music/general audio at 48 kHz) and Speech mode (speech at 16 kHz)
15
- - **High accuracy**: 11/11 conformance tests pass against the official C++ implementation
15
+ - **High accuracy**: 12/12 conformance tests pass against the official C++ implementation
16
16
  - Audio mode: 9/10 tests produce **identical** MOS scores (diff = 0.000000), 1 test diff = 0.000117
17
- - Speech mode: diff = 0.006715
18
- - **Pure Python**: no C/C++ compilation required
19
- - **Minimal dependencies**: only 4 pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
20
- - **Faster than real-time**: Audio RTF 0.71x, Speech RTF 0.38x
17
+ - Speech mode (polynomial): diff = 0.001057
18
+ - Speech mode (lattice TFLite): diff = 0.002341
19
+ - **Two speech quality mappers** matching C++ ViSQOL:
20
+ - **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
21
+ - **Polynomial (fallback)** — legacy exponential fit (`--use_lattice_model=false` in C++)
22
+ - **Pure Python**: no C/C++ compilation required (the optional `[lattice]` extra adds the Google `ai-edge-litert` TFLite runtime as a binary wheel)
23
+ - **Minimal dependencies**: 4 core pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
24
+ - **Optional Numba acceleration**: `pip install visqol-python[accel]` for JIT-compiled Gammatone filterbank (parallel + fastmath) and DP patch matching — **9× overall speedup**, RTF 0.064 (surpasses C++ estimates)
25
+ - **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
26
+ - **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
21
27
 
22
28
  ## Installation
23
29
 
@@ -25,14 +31,34 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
25
31
  pip install visqol-python
26
32
  ```
27
33
 
34
+ For **C++-default-equivalent speech mode** (deep-lattice TFLite mapper):
35
+
36
+ ```bash
37
+ pip install visqol-python[lattice] # requires Python ≥ 3.10
38
+ ```
39
+
40
+ For **Numba-accelerated** Gammatone filtering and DP matching (~9× faster):
41
+
42
+ ```bash
43
+ pip install visqol-python[accel]
44
+ ```
45
+
46
+ Install everything (lattice + numba):
47
+
48
+ ```bash
49
+ pip install visqol-python[all]
50
+ ```
51
+
28
52
  Or install from source:
29
53
 
30
54
  ```bash
31
55
  git clone https://github.com/talker93/visqol-python.git
32
56
  cd visqol-python
33
- pip install -e .
57
+ pip install -e ".[dev]"
34
58
  ```
35
59
 
60
+ > **Note on speech mode parity**: Without the `[lattice]` extra, speech mode falls back to the polynomial mapping (equivalent to running C++ ViSQOL with `--use_lattice_model=false`). The polynomial can over-predict MOS by 1–2 points on degraded speech vs the C++ default. Install `[lattice]` whenever you need numbers that line up with the C++ default behaviour (see [issue #1](https://github.com/talker93/visqol-python/issues/1)).
61
+
36
62
  ## Quick Start
37
63
 
38
64
  ### Python API
@@ -83,12 +109,15 @@ file_pairs = [
83
109
  ("ref3.wav", "deg3.wav"),
84
110
  ]
85
111
 
86
- # Optional progress callback
112
+ # Sequential with progress callback
87
113
  results = api.measure_batch(
88
114
  file_pairs,
89
115
  progress_callback=lambda done, total: print(f"{done}/{total}"),
90
116
  )
91
117
 
118
+ # Multi-process parallel (uses all CPU cores)
119
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
120
+
92
121
  for pair, result in zip(file_pairs, results):
93
122
  if isinstance(result, Exception):
94
123
  print(f"{pair}: FAILED — {result}")
@@ -115,7 +144,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
115
144
  |------|-------------|
116
145
  | `-r`, `--reference` | Path to reference WAV file (required) |
117
146
  | `-d`, `--degraded` | Path to degraded WAV file (required) |
118
- | `--speech_mode` | Use speech mode (16 kHz, polynomial mapping) |
147
+ | `--speech_mode` | Use speech mode (16 kHz) |
148
+ | `--no_lattice_model` | Speech mode: disable lattice TFLite mapper, use polynomial fallback |
149
+ | `--lattice_model` | Custom path to lattice `.tflite` model (speech mode) |
150
+ | `--unscaled_speech` | Don't scale polynomial speech MOS to 5.0 (polynomial only) |
119
151
  | `--model` | Custom SVR model file path (audio mode only) |
120
152
  | `--search_window` | Search window radius (default: 60) |
121
153
  | `--verbose`, `-v` | Show detailed per-patch results |
@@ -143,21 +175,34 @@ The `measure()` method returns a `SimilarityResult` object with:
143
175
 
144
176
  ### Speech Mode
145
177
  - Target sample rate: **16 kHz**
146
- - 32 Gammatone frequency bands (50 Hz – 8 000 Hz)
147
- - Quality mapping: exponential polynomial fit
178
+ - 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
148
179
  - VAD (Voice Activity Detection) based patch selection
180
+ - Quality mapping (choose one):
181
+ - **Deep-lattice TFLite (default)** — same mapper as C++ ViSQOL's default `--use_lattice_model=true`; requires `pip install visqol-python[lattice]`
182
+ - **Exponential polynomial (fallback)** — same as C++ `--use_lattice_model=false`; used automatically when the lattice runtime is not installed
183
+ - Toggle from Python: `api.create(mode="speech", use_lattice_model=False)`
184
+ - Toggle from CLI: `--no_lattice_model`
149
185
  - Best for: speech, VoIP, telephony
150
186
 
151
187
  ## Performance
152
188
 
153
189
  Measured on Apple M-series, Python 3.13:
154
190
 
191
+ ### Without Numba (pure Python + NumPy/SciPy)
192
+
155
193
  | Mode | Avg RTF | Typical Time |
156
194
  |------|---------|-------------|
157
- | Audio (48 kHz) | **0.71x** | 7 – 12 s per file pair |
195
+ | Audio (48 kHz) | **0.18x** | ~2.2 s per file pair |
158
196
  | Speech (16 kHz) | **0.38x** | ~1 s per file pair |
159
197
 
198
+ ### With Numba (`pip install visqol-python[accel]`)
199
+
200
+ | Mode | Avg RTF | Typical Time | Speedup |
201
+ |------|---------|-------------|---------|
202
+ | Audio (48 kHz) | **0.064x** | ~0.8 s per file pair | **9×** |
203
+
160
204
  > RTF (Real-Time Factor) < 1.0 means faster than real-time.
205
+ > With Numba acceleration, the Python implementation **surpasses C++ estimated performance** (RTF ≈ 0.093).
161
206
 
162
207
  ## Project Structure
163
208
 
@@ -177,18 +222,23 @@ visqol-python/
177
222
  │ ├── alignment.py # Global alignment via cross-correlation
178
223
  │ ├── nsim.py # NSIM similarity metric
179
224
  │ ├── quality_mapper.py # SVR & exponential quality mapping
225
+ │ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
180
226
  │ ├── __main__.py # CLI entry point
227
+ │ ├── py.typed # PEP 561 type marker
181
228
  │ └── model/ # Bundled SVR model
182
229
  │ └── libsvm_nu_svr_model.txt
183
- ├── tests/ # Tests (pytest)
230
+ ├── tests/ # Tests & benchmarks (pytest)
184
231
  │ ├── conftest.py # Shared fixtures & CLI options
185
232
  │ ├── test_quick.py # Smoke tests (no external data needed)
186
- └── test_conformance.py # Full conformance tests (needs testdata)
233
+ ├── test_conformance.py # Full conformance tests (needs testdata)
234
+ │ ├── test_parallel_correctness.py # Numba parallel correctness tests
235
+ │ └── bench_*.py # Performance benchmarks
187
236
  ├── .github/workflows/
188
- │ ├── ci.yml # CI: test on Python 3.9–3.13
237
+ │ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
189
238
  │ └── publish.yml # Auto-publish to PyPI on tag push
190
239
  ├── pyproject.toml # Package metadata & build config
191
240
  ├── CHANGELOG.md
241
+ ├── CONTRIBUTING.md
192
242
  ├── LICENSE
193
243
  └── README.md
194
244
  ```
@@ -209,7 +259,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
209
259
  | glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
210
260
  | contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
211
261
  | castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
212
- | speech_CA01 | Speech | 3.3745 | 3.3678 | 0.006715 |
262
+ | speech_CA01 (polynomial) | Speech | 3.3745 | 3.3756 | 0.001057 |
263
+ | speech_CA01 (lattice) | Speech | 3.3130 | 3.3153 | 0.002341 |
264
+
265
+ Both speech values come from running the C++ ViSQOL binary directly with the corresponding `--use_lattice_model` flag, so they represent ground-truth parity targets.
213
266
 
214
267
  ## References
215
268
 
@@ -15,6 +15,7 @@ authors = [
15
15
  keywords = [
16
16
  "audio-quality", "speech-quality", "MOS", "PESQ", "POLQA",
17
17
  "visqol", "objective-metric", "perceptual-quality",
18
+ "numba", "batch-processing",
18
19
  ]
19
20
  classifiers = [
20
21
  "Development Status :: 4 - Beta",
@@ -28,6 +29,7 @@ classifiers = [
28
29
  "Programming Language :: Python :: 3.13",
29
30
  "Topic :: Multimedia :: Sound/Audio :: Analysis",
30
31
  "Topic :: Scientific/Engineering",
32
+ "Typing :: Typed",
31
33
  ]
32
34
  dependencies = [
33
35
  "numpy>=1.20",
@@ -38,12 +40,18 @@ dependencies = [
38
40
 
39
41
  [project.optional-dependencies]
40
42
  accel = ["numba>=0.57"]
43
+ lattice = ["ai-edge-litert>=2.1.5; python_version >= '3.10'"]
44
+ all = [
45
+ "numba>=0.57",
46
+ "ai-edge-litert>=2.1.5; python_version >= '3.10'",
47
+ ]
41
48
  test = ["pytest>=7.0"]
42
49
  dev = [
43
50
  "pytest>=7.0",
44
51
  "ruff>=0.4",
45
52
  "mypy>=1.8",
46
53
  "numba>=0.57",
54
+ "ai-edge-litert>=2.1.5; python_version >= '3.10'",
47
55
  ]
48
56
 
49
57
  [project.urls]
@@ -63,7 +71,7 @@ version = {attr = "visqol.__version__"}
63
71
  exclude = ["tests*"]
64
72
 
65
73
  [tool.setuptools.package-data]
66
- visqol = ["model/*.txt", "py.typed"]
74
+ visqol = ["model/*.txt", "model/*.tflite", "py.typed"]
67
75
 
68
76
  [tool.pytest.ini_options]
69
77
  testpaths = ["tests"]
@@ -103,7 +111,13 @@ warn_return_any = true
103
111
  warn_unused_configs = true
104
112
 
105
113
  [[tool.mypy.overrides]]
106
- module = ["svmutil.*", "libsvm.*", "soundfile.*", "numba.*"]
114
+ module = [
115
+ "svmutil.*",
116
+ "libsvm.*",
117
+ "soundfile.*",
118
+ "numba.*",
119
+ "ai_edge_litert.*",
120
+ ]
107
121
  ignore_missing_imports = true
108
122
 
109
123
  [[tool.mypy.overrides]]
@@ -52,9 +52,22 @@ def audio_api():
52
52
 
53
53
 
54
54
  @pytest.fixture(scope="session")
55
- def speech_api():
55
+ def speech_polynomial_api():
56
+ """Speech mode with the legacy polynomial mapper (C++ --use_lattice_model=false)."""
56
57
  api = VisqolApi()
57
- api.create(mode="speech")
58
+ api.create(mode="speech", use_lattice_model=False)
59
+ return api
60
+
61
+
62
+ @pytest.fixture(scope="session")
63
+ def speech_lattice_api():
64
+ """Speech mode with the deep-lattice TFLite mapper (C++ default)."""
65
+ pytest.importorskip(
66
+ "ai_edge_litert",
67
+ reason="ai-edge-litert not installed (pip install visqol-python[lattice]).",
68
+ )
69
+ api = VisqolApi()
70
+ api.create(mode="speech", use_lattice_model=True)
58
71
  return api
59
72
 
60
73
 
@@ -105,10 +118,21 @@ AUDIO_CASES = [
105
118
  ),
106
119
  ]
107
120
 
108
- SPEECH_CASES = [
121
+ # Speech conformance with the polynomial mapper (== C++ --use_lattice_model=false).
122
+ # Expected values come from running C++ ViSQOL v3.3.3 with that flag.
123
+ SPEECH_POLYNOMIAL_CASES = [
109
124
  ("CA01_01.wav", "transcoded_CA01_01.wav", 3.374505555111911, "CA01_transcoded"),
110
125
  ]
111
126
 
127
+ # Speech conformance with the deep-lattice TFLite mapper (C++ default).
128
+ # Expected values come from running the C++ ViSQOL binary with default flags
129
+ # (--use_lattice_model=true). Residual divergence from this Python port is
130
+ # tracked by the TOLERANCE constant above; lattice tends to amplify upstream
131
+ # numerical noise more than the polynomial mapper.
132
+ SPEECH_LATTICE_CASES = [
133
+ ("CA01_01.wav", "transcoded_CA01_01.wav", 3.3129589557647705, "CA01_transcoded_lattice"),
134
+ ]
135
+
112
136
 
113
137
  # ── Audio mode tests ──
114
138
 
@@ -133,13 +157,32 @@ def test_audio_conformance(audio_api, conf_dir, ref_name, deg_name, expected_mos
133
157
 
134
158
  @pytest.mark.parametrize(
135
159
  "ref_name, deg_name, expected_mos, test_id",
136
- SPEECH_CASES,
137
- ids=[c[3] for c in SPEECH_CASES],
160
+ SPEECH_POLYNOMIAL_CASES,
161
+ ids=[c[3] for c in SPEECH_POLYNOMIAL_CASES],
162
+ )
163
+ def test_speech_polynomial_conformance(
164
+ speech_polynomial_api, speech_dir, ref_name, deg_name, expected_mos, test_id
165
+ ):
166
+ ref_path = os.path.join(speech_dir, ref_name)
167
+ deg_path = os.path.join(speech_dir, deg_name)
168
+ result = speech_polynomial_api.measure(ref_path, deg_path)
169
+ diff = abs(result.moslqo - expected_mos)
170
+ assert diff < TOLERANCE, (
171
+ f"[{test_id}] MOS={result.moslqo:.6f}, expected={expected_mos:.6f}, diff={diff:.6f}"
172
+ )
173
+
174
+
175
+ @pytest.mark.parametrize(
176
+ "ref_name, deg_name, expected_mos, test_id",
177
+ SPEECH_LATTICE_CASES,
178
+ ids=[c[3] for c in SPEECH_LATTICE_CASES],
138
179
  )
139
- def test_speech_conformance(speech_api, speech_dir, ref_name, deg_name, expected_mos, test_id):
180
+ def test_speech_lattice_conformance(
181
+ speech_lattice_api, speech_dir, ref_name, deg_name, expected_mos, test_id
182
+ ):
140
183
  ref_path = os.path.join(speech_dir, ref_name)
141
184
  deg_path = os.path.join(speech_dir, deg_name)
142
- result = speech_api.measure(ref_path, deg_path)
185
+ result = speech_lattice_api.measure(ref_path, deg_path)
143
186
  diff = abs(result.moslqo - expected_mos)
144
187
  assert diff < TOLERANCE, (
145
188
  f"[{test_id}] MOS={result.moslqo:.6f}, expected={expected_mos:.6f}, diff={diff:.6f}"
@@ -13,7 +13,7 @@ Usage:
13
13
  print(f"MOS-LQO: {result.moslqo}")
14
14
  """
15
15
 
16
- __version__: str = "3.4.0"
16
+ __version__: str = "3.5.0"
17
17
 
18
18
  from visqol.api import ProgressCallback, VisqolApi
19
19
  from visqol.audio_utils import AudioSignal
@@ -53,7 +53,21 @@ def main() -> None:
53
53
  parser.add_argument(
54
54
  "--unscaled_speech",
55
55
  action="store_true",
56
- help="Don't scale speech MOS to max 5.0",
56
+ help="Don't scale speech MOS to max 5.0 (polynomial mapping only)",
57
+ )
58
+ parser.add_argument(
59
+ "--no_lattice_model",
60
+ action="store_true",
61
+ help=(
62
+ "Speech mode: disable the deep-lattice TFLite mapper and use the "
63
+ "polynomial mapping instead. By default lattice is auto-enabled "
64
+ "when `ai-edge-litert` is installed (matches C++ default)."
65
+ ),
66
+ )
67
+ parser.add_argument(
68
+ "--lattice_model",
69
+ default=None,
70
+ help="Path to lattice .tflite model (speech mode, optional override).",
57
71
  )
58
72
  parser.add_argument(
59
73
  "--no_alignment",
@@ -86,19 +100,24 @@ def main() -> None:
86
100
  mode: str = "speech" if args.speech_mode else "audio"
87
101
 
88
102
  try:
103
+ # Lattice toggle: pass False if --no_lattice_model, else None (auto-detect).
104
+ use_lattice = False if args.no_lattice_model else None
105
+
89
106
  api = VisqolApi()
90
107
  api.create(
91
108
  mode=mode,
92
109
  model_path=args.model,
93
110
  search_window=args.search_window,
94
111
  use_unscaled_speech=args.unscaled_speech,
112
+ use_lattice_model=use_lattice,
113
+ lattice_model_path=args.lattice_model,
95
114
  disable_global_alignment=args.no_alignment,
96
115
  disable_realignment=args.no_realignment,
97
116
  )
98
117
 
99
118
  result = api.measure(args.reference, args.degraded)
100
119
 
101
- except (FileNotFoundError, ValueError, RuntimeError) as exc:
120
+ except (FileNotFoundError, ValueError, RuntimeError, ImportError) as exc:
102
121
  logger.error("%s", exc)
103
122
  sys.exit(1)
104
123
  except Exception as exc: