visqol-python 3.4.0__tar.gz → 3.6.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 (39) hide show
  1. visqol_python-3.6.0/CHANGELOG.md +227 -0
  2. {visqol_python-3.4.0 → visqol_python-3.6.0}/PKG-INFO +96 -22
  3. {visqol_python-3.4.0 → visqol_python-3.6.0}/README.md +83 -20
  4. {visqol_python-3.4.0 → visqol_python-3.6.0}/pyproject.toml +20 -2
  5. {visqol_python-3.4.0 → visqol_python-3.6.0}/tests/test_conformance.py +50 -7
  6. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/__init__.py +1 -1
  7. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/__main__.py +21 -2
  8. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/api.py +21 -3
  9. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/gammatone.py +2 -2
  10. visqol_python-3.6.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.6.0}/visqol/nsim.py +32 -1
  12. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/numba_accel.py +68 -57
  13. visqol_python-3.6.0/visqol/quality_mapper.py +228 -0
  14. visqol_python-3.6.0/visqol/signal_utils.py +167 -0
  15. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/visqol_manager.py +49 -4
  16. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol_python.egg-info/PKG-INFO +96 -22
  17. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol_python.egg-info/SOURCES.txt +1 -0
  18. visqol_python-3.6.0/visqol_python.egg-info/requires.txt +35 -0
  19. visqol_python-3.4.0/CHANGELOG.md +0 -100
  20. visqol_python-3.4.0/visqol/quality_mapper.py +0 -124
  21. visqol_python-3.4.0/visqol/signal_utils.py +0 -91
  22. visqol_python-3.4.0/visqol_python.egg-info/requires.txt +0 -16
  23. {visqol_python-3.4.0 → visqol_python-3.6.0}/CONTRIBUTING.md +0 -0
  24. {visqol_python-3.4.0 → visqol_python-3.6.0}/LICENSE +0 -0
  25. {visqol_python-3.4.0 → visqol_python-3.6.0}/MANIFEST.in +0 -0
  26. {visqol_python-3.4.0 → visqol_python-3.6.0}/setup.cfg +0 -0
  27. {visqol_python-3.4.0 → visqol_python-3.6.0}/tests/test_parallel_correctness.py +0 -0
  28. {visqol_python-3.4.0 → visqol_python-3.6.0}/tests/test_quick.py +0 -0
  29. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/alignment.py +0 -0
  30. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/analysis_window.py +0 -0
  31. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/audio_utils.py +0 -0
  32. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  33. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/patch_creator.py +0 -0
  34. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/patch_selector.py +0 -0
  35. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/py.typed +0 -0
  36. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol/visqol_core.py +0 -0
  37. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol_python.egg-info/dependency_links.txt +0 -0
  38. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol_python.egg-info/entry_points.txt +0 -0
  39. {visqol_python-3.4.0 → visqol_python-3.6.0}/visqol_python.egg-info/top_level.txt +0 -0
@@ -0,0 +1,227 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [3.6.0] - 2026-05-27
8
+
9
+ ### Added
10
+ - **Optional pyFFTW backend** (`pip install visqol-python[fftw]`):
11
+ - Routes `scipy.fft.fft / ifft / rfft / irfft` through FFTW3 via
12
+ `pyfftw.interfaces.scipy_fft` for the alignment and cross-correlation
13
+ FFTs. Detected at module load time in `signal_utils` and applied
14
+ transparently via a thin `_fft_backend()` context manager.
15
+ - Plan cache enabled with 60 s keep-alive so consecutive measurements
16
+ on equal-length signals reuse the FFTW plan.
17
+
18
+ ### Improved
19
+ - **Fused NSIM kernel**: `_measure_patch_similarity_numba` merged its 5
20
+ separate 2-D convolutions (μ_r, μ_d, ref², deg², ref·deg) and the
21
+ intensity/structure recombination into one `(r, c)` double loop. Each
22
+ patch element is read from L1 once per visit instead of five times,
23
+ and the four intermediate `(rows × cols)` matrices are no longer
24
+ materialised between convs. Bit-exact with the previous split-conv
25
+ path (ULP-level FP rounding only).
26
+ - **`nsim.measure_patch_similarity`** now dispatches to the fused JIT
27
+ kernel when Numba is available — the same code path the DP patch
28
+ matcher uses, so `finely_align_and_recreate_patches` shares the
29
+ speedup. The pure-NumPy implementation is preserved as a fallback.
30
+ - **`signal_utils._hilbert`** is a drop-in `scipy.signal.hilbert`
31
+ replacement built on `rfft` for the real-valued input (~2× less
32
+ forward-transform work than the original full complex `fft`).
33
+ - **`signal_utils.find_best_lag`** now uses `rfft + irfft` for the
34
+ cross-correlation, again exploiting the real input. Net effect:
35
+ alignment FFTs see roughly 2× less work overall.
36
+ - Removed dead helper `_conv2d_boundary_valid` from `numba_accel.py`
37
+ (subsumed by the fused kernel).
38
+
39
+ ### Fixed
40
+ - **`find_best_lag` interpreter hot loop**: the previous implementation
41
+ ran `xcorr_full[-max_lag:].tolist() + xcorr_full[:max_lag+1].tolist()`
42
+ and then `builtin argmax(list)` over a ~1.2 M-element Python list,
43
+ costing ~33 ms per call in pure interpreter overhead. Now uses
44
+ `np.concatenate + np.argmax` entirely in C.
45
+
46
+ ### Performance
47
+
48
+ Apple M-series, Python 3.13, audio mode, the `guitar48_stereo` 12.5 s
49
+ conformance case, average of 3 runs with Numba + pyFFTW both installed:
50
+
51
+ | Stage | v3.5.0 | v3.6.0 | Speedup |
52
+ |--------------------|----------|----------|---------|
53
+ | DP Patch matching | 0.397 s | 0.131 s | **3.0×** |
54
+ | Global align / FFT | 0.173 s | 0.091 s | 1.9× |
55
+ | Fine align + NSIM | 0.093 s | 0.043 s | 2.2× |
56
+ | Gammatone | 0.173 s | 0.179 s | ~ |
57
+ | **Total** | **0.839 s** | **0.447 s** | **1.9×** |
58
+ | **RTF** | 0.067 | **0.036** | (C++ est. 0.093) |
59
+
60
+ ### Numerical parity
61
+
62
+ All v3.5.0 conformance baselines preserved within ULP precision:
63
+
64
+ | Test | Max MOS diff vs v3.5.0 |
65
+ |-------------------------------|------------------------|
66
+ | Audio (10 conformance cases) | < 5 × 10⁻¹⁴ |
67
+ | Speech polynomial CA01 | 0.0 (bit-exact) |
68
+ | Speech lattice CA01 | 0.0 (bit-exact) |
69
+
70
+ ## [3.5.0] - 2026-05-26
71
+
72
+ ### Added
73
+ - **Deep-lattice TFLite speech quality mapper** (`pip install visqol-python[lattice]`):
74
+ - `TFLiteSpeechQualityMapper` loads the same `.tflite` lattice network used by
75
+ C++ ViSQOL's default `--use_lattice_model=true` and runs inference through
76
+ the upstream Google TFLite C++ runtime via `ai-edge-litert`
77
+ - New `use_lattice_model` parameter on `VisqolApi.create()` (default `None`
78
+ auto-enables lattice when the runtime is installed)
79
+ - New `lattice_model_path` parameter to override the bundled model
80
+ - New CLI flags `--no_lattice_model` and `--lattice_model PATH`
81
+ - New `[lattice]` and `[all]` extras in `pyproject.toml`
82
+ - Bundled `lattice_*.tflite` (2.1 MB) into the wheel as package data
83
+
84
+ ### Fixed
85
+ - **GH issue #1**: Speech-mode MOS scores were systematically 1–2 points higher
86
+ than C++ ViSQOL's default. Root cause: the Python port only implemented the
87
+ legacy polynomial mapper (`SpeechSimilarityToQualityMapper`, equivalent to
88
+ C++ `--use_lattice_model=false`), while the C++ default routes through the
89
+ TFLite lattice network. Installing `visqol-python[lattice]` now matches C++
90
+ default scoring (CA01 conformance: diff 0.027 vs 1–2 MOS before).
91
+ - **`signal_utils.normalize()` parity bug**: the previous implementation did
92
+ min–max scaling to ``[0, 1]`` (shifting the signal positive and adding a DC
93
+ offset), while C++ ``MiscMath::Normalize`` only divides by the peak. This
94
+ inflated the RMS values fed to the speech-mode VAD, causing Python to keep
95
+ every patch as voice-active and adding spurious patches the C++ binary
96
+ would have discarded. Fixing this brought polynomial speech parity from
97
+ diff 0.007 → 0.001 and was a prerequisite for lattice parity. Only the
98
+ speech-mode VAD path used this function; audio mode is unaffected.
99
+ - **`nsim` stddev estimator mismatch**: both ``nsim.measure_patch_similarity``
100
+ (``np.std(..., ddof=0)``) and the Numba ``_measure_patch_similarity_numba``
101
+ kernel (``sqrt(ss / cols)``) used the population estimator (divide by N).
102
+ C++ uses Armadillo's ``stddev(..., 0)`` which is the *unbiased* sample
103
+ estimator (divide by N-1, despite the misleading ``0`` flag). The
104
+ per-band ``freq_band_stddevs`` was therefore systematically smaller by
105
+ ``sqrt((N-1)/N) ≈ 0.974``, which fed into the pooled ``fstdnsim`` and
106
+ perturbed every lattice prediction.
107
+ - **`numba_accel.fastmath=True` on the Gammatone spectrogram kernel**: the
108
+ compounded LLVM-level FP reassociation across the 4-stage cascaded IIR ×
109
+ thousands of samples × hundreds of frames pushed lattice MOS off by
110
+ another ~0.02 vs strict IEEE-754. ``fastmath`` has been removed from the
111
+ spectrogram kernel; ``parallel=True`` is kept (each frame's IIR state is
112
+ independent so the reduction is safe).
113
+
114
+ ### Speech-mode parity numbers (CA01 conformance)
115
+
116
+ | Mode | Before all fixes | After all fixes | C++ baseline |
117
+ |------|------------------|-----------------|--------------|
118
+ | Polynomial | diff 0.0067 | **diff 0.0011** | 3.3745 |
119
+ | Lattice | diff 0.0856 (≈1–2 MOS on Nils's TCD-VOIP samples) | **diff 0.0023** | 3.3130 |
120
+
121
+ ### Changed
122
+ - Speech mode `create(mode="speech")` now auto-uses lattice when available; when
123
+ `ai-edge-litert` is missing, it logs a one-time warning and falls back to
124
+ polynomial (existing scores reproduce exactly).
125
+ - `tests/test_conformance.py` split the single speech case into
126
+ `test_speech_polynomial_conformance` (existing C++ polynomial baseline 3.3745)
127
+ and `test_speech_lattice_conformance` (regression baseline captured from this
128
+ implementation).
129
+ - README: documented the polynomial-vs-lattice distinction, new install matrix,
130
+ and parity caveats.
131
+
132
+ ## [3.4.0] - 2026-03-23
133
+
134
+ ### Added
135
+ - **Numba JIT acceleration** (`pip install visqol-python[accel]`):
136
+ - DP patch matching inner loops compiled to machine code via `@njit`
137
+ - Gammatone IIR filterbank compiled with `parallel=True` + `fastmath=True` — frames processed in parallel across all CPU cores
138
+ - NSIM similarity kernel JIT-compiled
139
+ - Automatic `NUMBA_THREADING_LAYER=workqueue` setup for macOS compatibility
140
+ - Zero-loss parallel accuracy (each frame's IIR state is independent)
141
+ - **Batch evaluation API**: `VisqolApi.measure_batch()` with optional `parallel=True` and `max_workers` for multi-process execution
142
+ - Exported `PatchSimilarityResult` and `ProgressCallback` from top-level package
143
+
144
+ ### Performance
145
+ - **12x Gammatone speedup** via parallel + fastmath (1.53s → 0.13s per signal pair)
146
+ - **8.7x DP patch matching speedup** via Numba JIT (3.5s → 0.40s)
147
+ - **Overall 9x speedup**: RTF 0.58 → 0.064 (surpasses C++ estimate of 0.093)
148
+ - Fine alignment skip optimization: 29x speedup when lag == 0
149
+
150
+ ### Improved
151
+ - `__repr__` / `__str__` for `SimilarityResult`, `AudioSignal`, `PatchSimilarityResult`, `Spectrogram`
152
+ - Logging replaces print statements in CLI verbose output
153
+ - Development tooling: ruff lint/format + mypy strict type checking in CI
154
+
155
+ ### Fixed
156
+ - **CI failures**: resolved all ruff lint (308 errors), ruff format (24 files), and mypy (24 errors) issues
157
+ - Added `per-file-ignores` for benchmark test scripts (E402, E702)
158
+ - Added mypy override for `numba_accel.py` (untyped `@njit` decorators)
159
+ - Fixed `no-any-return` errors across `audio_utils.py`, `gammatone.py`, `visqol_core.py`, `api.py`
160
+ - Added `TYPE_CHECKING` imports for `ImagePatchCreator` / `VadPatchCreator` in `visqol_core.py`
161
+
162
+ ## [3.3.6] - 2026-03-23
163
+
164
+ ### Added
165
+ - **Batch evaluation API**: `VisqolApi.measure_batch()` with `progress_callback` support
166
+ - **Numba optional acceleration**: `visqol/numba_accel.py` with JIT-compiled DP forward pass and NSIM kernel
167
+ - `[accel]` optional dependency group: `pip install visqol-python[accel]`
168
+
169
+ ### Improved
170
+ - `GammatoneFilterBank.apply_filter()` pre-builds coefficient arrays (avoids per-channel allocation)
171
+ - `prepare_spectrograms_for_comparison()` vectorized per-frame noise floor
172
+ - Ruff lint/format configuration added to `pyproject.toml`
173
+ - CI enhanced with lint and type-check jobs
174
+ - Development dependencies: `[project.optional-dependencies] dev`
175
+
176
+ ## [3.3.5] - 2026-03-23
177
+
178
+ ### Added
179
+ - **Type hints** on all public and internal APIs (`from __future__ import annotations`)
180
+ - **`py.typed`** marker (PEP 561) — mypy / pyright can now type-check dependents
181
+ - **CONTRIBUTING.md** with development setup, code style, and PR guidelines
182
+ - Exported `SimilarityResult` and `AudioSignal` from top-level `visqol` package
183
+ - `mypy` configuration in `pyproject.toml`
184
+
185
+ ### Improved
186
+ - **Error handling**: friendly `ValueError` / `FileNotFoundError` / `TypeError` throughout:
187
+ - `VisqolApi.create()` now validates mode, search_window, and model_path
188
+ - `VisqolApi.measure()` checks file existence before processing
189
+ - `VisqolApi.measure_from_arrays()` validates array types, emptiness, and sample rate
190
+ - `AudioSignal` validates sample rate on construction
191
+ - `AnalysisWindow` validates sample_rate and overlap range
192
+ - CLI now catches exceptions and prints user-friendly error messages
193
+ - `AnalysisWindow.apply_hann_window()` uses `ValueError` instead of bare `assert`
194
+
195
+ ## [3.3.4] - 2026-03-23
196
+
197
+ ### Improved
198
+ - Tests rewritten in **pytest** format with `parametrize` and fixtures
199
+ - Added **CI workflow** (GitHub Actions): auto-test on Python 3.9–3.13 for every push/PR
200
+ - Added **smoke tests** (`test_quick.py`) that run without external testdata
201
+ - Version number now managed in a single place (`visqol/__init__.py`)
202
+ - Removed redundant `setup.py` — `pyproject.toml` is the single source of truth
203
+ - Added this CHANGELOG
204
+ - README: added PyPI / CI / License badges
205
+
206
+ ### Fixed
207
+ - `requires-python` updated from `>=3.8` to `>=3.9` (numpy/scipy dropped 3.8 support)
208
+
209
+ ## [3.3.3] - 2026-03-23
210
+
211
+ ### Added
212
+ - Initial PyPI release as `visqol-python`
213
+ - Pure Python port of [Google's ViSQOL v3.3.3](https://github.com/google/visqol)
214
+ - **Audio mode** (48 kHz, SVR quality mapping) — 10/10 conformance tests pass
215
+ - **Speech mode** (16 kHz, exponential polynomial mapping) — 1/1 conformance test passes
216
+ - Python API: `VisqolApi.measure()` and `VisqolApi.measure_from_arrays()`
217
+ - CLI: `python -m visqol` / `visqol` command
218
+ - Bundled SVR model (`libsvm_nu_svr_model.txt`)
219
+ - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
220
+
221
+ [3.6.0]: https://github.com/talker93/visqol-python/compare/v3.5.0...v3.6.0
222
+ [3.5.0]: https://github.com/talker93/visqol-python/compare/v3.4.0...v3.5.0
223
+ [3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
224
+ [3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
225
+ [3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
226
+ [3.3.4]: https://github.com/talker93/visqol-python/compare/v3.3.3...v3.3.4
227
+ [3.3.3]: https://github.com/talker93/visqol-python/releases/tag/v3.3.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.4.0
3
+ Version: 3.6.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,14 @@ 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: fftw
37
+ Requires-Dist: pyfftw>=0.13; extra == "fftw"
38
+ Provides-Extra: all
39
+ Requires-Dist: numba>=0.57; extra == "all"
40
+ Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "all"
41
+ Requires-Dist: pyfftw>=0.13; extra == "all"
33
42
  Provides-Extra: test
34
43
  Requires-Dist: pytest>=7.0; extra == "test"
35
44
  Provides-Extra: dev
@@ -37,6 +46,8 @@ Requires-Dist: pytest>=7.0; extra == "dev"
37
46
  Requires-Dist: ruff>=0.4; extra == "dev"
38
47
  Requires-Dist: mypy>=1.8; extra == "dev"
39
48
  Requires-Dist: numba>=0.57; extra == "dev"
49
+ Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "dev"
50
+ Requires-Dist: pyfftw>=0.13; extra == "dev"
40
51
  Dynamic: license-file
41
52
 
42
53
  # ViSQOL (Python)
@@ -46,19 +57,26 @@ Dynamic: license-file
46
57
  [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
47
58
  [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
48
59
 
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.
60
+ 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
61
 
51
62
  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
63
 
53
64
  ## Features
54
65
 
55
66
  - **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
67
+ - **High accuracy**: 12/12 conformance tests pass against the official C++ implementation
57
68
  - 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
69
+ - Speech mode (polynomial): diff = 0.001057
70
+ - Speech mode (lattice TFLite): diff = 0.002341
71
+ - **Two speech quality mappers** matching C++ ViSQOL:
72
+ - **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
73
+ - **Polynomial (fallback)** — legacy exponential fit (`--use_lattice_model=false` in C++)
74
+ - **Pure Python**: no C/C++ compilation required (the optional `[lattice]` extra adds the Google `ai-edge-litert` TFLite runtime as a binary wheel)
75
+ - **Minimal dependencies**: 4 core pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
76
+ - **Optional Numba acceleration**: `pip install visqol-python[accel]` for JIT-compiled Gammatone filterbank (parallel) and a fused NSIM + DP patch matching kernel
77
+ - **Optional pyFFTW backend**: `pip install visqol-python[fftw]` routes alignment / xcorr FFTs through FFTW3 — **~16× overall speedup**, RTF 0.036 (vs C++ estimate 0.093)
78
+ - **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
79
+ - **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
62
80
 
63
81
  ## Installation
64
82
 
@@ -66,14 +84,40 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
66
84
  pip install visqol-python
67
85
  ```
68
86
 
87
+ For **C++-default-equivalent speech mode** (deep-lattice TFLite mapper):
88
+
89
+ ```bash
90
+ pip install visqol-python[lattice] # requires Python ≥ 3.10
91
+ ```
92
+
93
+ For **Numba-accelerated** Gammatone filtering and the fused NSIM + DP kernel:
94
+
95
+ ```bash
96
+ pip install visqol-python[accel]
97
+ ```
98
+
99
+ For **FFTW3-backed alignment FFTs** via pyFFTW:
100
+
101
+ ```bash
102
+ pip install visqol-python[fftw]
103
+ ```
104
+
105
+ Install everything (lattice + numba + fftw):
106
+
107
+ ```bash
108
+ pip install visqol-python[all]
109
+ ```
110
+
69
111
  Or install from source:
70
112
 
71
113
  ```bash
72
114
  git clone https://github.com/talker93/visqol-python.git
73
115
  cd visqol-python
74
- pip install -e .
116
+ pip install -e ".[dev]"
75
117
  ```
76
118
 
119
+ > **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)).
120
+
77
121
  ## Quick Start
78
122
 
79
123
  ### Python API
@@ -124,12 +168,15 @@ file_pairs = [
124
168
  ("ref3.wav", "deg3.wav"),
125
169
  ]
126
170
 
127
- # Optional progress callback
171
+ # Sequential with progress callback
128
172
  results = api.measure_batch(
129
173
  file_pairs,
130
174
  progress_callback=lambda done, total: print(f"{done}/{total}"),
131
175
  )
132
176
 
177
+ # Multi-process parallel (uses all CPU cores)
178
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
179
+
133
180
  for pair, result in zip(file_pairs, results):
134
181
  if isinstance(result, Exception):
135
182
  print(f"{pair}: FAILED — {result}")
@@ -156,7 +203,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
156
203
  |------|-------------|
157
204
  | `-r`, `--reference` | Path to reference WAV file (required) |
158
205
  | `-d`, `--degraded` | Path to degraded WAV file (required) |
159
- | `--speech_mode` | Use speech mode (16 kHz, polynomial mapping) |
206
+ | `--speech_mode` | Use speech mode (16 kHz) |
207
+ | `--no_lattice_model` | Speech mode: disable lattice TFLite mapper, use polynomial fallback |
208
+ | `--lattice_model` | Custom path to lattice `.tflite` model (speech mode) |
209
+ | `--unscaled_speech` | Don't scale polynomial speech MOS to 5.0 (polynomial only) |
160
210
  | `--model` | Custom SVR model file path (audio mode only) |
161
211
  | `--search_window` | Search window radius (default: 60) |
162
212
  | `--verbose`, `-v` | Show detailed per-patch results |
@@ -184,21 +234,37 @@ The `measure()` method returns a `SimilarityResult` object with:
184
234
 
185
235
  ### Speech Mode
186
236
  - Target sample rate: **16 kHz**
187
- - 32 Gammatone frequency bands (50 Hz – 8 000 Hz)
188
- - Quality mapping: exponential polynomial fit
237
+ - 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
189
238
  - VAD (Voice Activity Detection) based patch selection
239
+ - Quality mapping (choose one):
240
+ - **Deep-lattice TFLite (default)** — same mapper as C++ ViSQOL's default `--use_lattice_model=true`; requires `pip install visqol-python[lattice]`
241
+ - **Exponential polynomial (fallback)** — same as C++ `--use_lattice_model=false`; used automatically when the lattice runtime is not installed
242
+ - Toggle from Python: `api.create(mode="speech", use_lattice_model=False)`
243
+ - Toggle from CLI: `--no_lattice_model`
190
244
  - Best for: speech, VoIP, telephony
191
245
 
192
246
  ## Performance
193
247
 
194
- Measured on Apple M-series, Python 3.13:
248
+ Measured on Apple M-series, Python 3.13, audio mode on the `guitar48_stereo` 12.5 s conformance case (3-run average):
195
249
 
196
- | Mode | Avg RTF | Typical Time |
197
- |------|---------|-------------|
198
- | Audio (48 kHz) | **0.71x** | 7 – 12 s per file pair |
199
- | Speech (16 kHz) | **0.38x** | ~1 s per file pair |
250
+ | Configuration | RTF | Typical Time | Speedup vs pure Python |
251
+ |---|---|---|---|
252
+ | Pure Python + NumPy/SciPy | 0.58 | ~7 s | 1.0× |
253
+ | + `[accel]` (Numba JIT) | 0.067 | ~0.84 s | 8.7× |
254
+ | + `[accel] [fftw]` (Numba + FFTW3) | **0.036** | **~0.45 s** | **16×** |
200
255
 
201
256
  > RTF (Real-Time Factor) < 1.0 means faster than real-time.
257
+ > With Numba + pyFFTW the Python implementation runs at **2.6× the C++ estimated speed** (C++ RTF ≈ 0.093).
258
+
259
+ Stage-level breakdown of the v3.6.0 fully-accelerated path:
260
+
261
+ | Stage | Time | % |
262
+ |---|---|---|
263
+ | Gammatone filterbank | 0.179 s | 40% |
264
+ | DP Patch matching (fused NSIM kernel) | 0.131 s | 29% |
265
+ | Global alignment (pyFFTW rfft/irfft) | 0.091 s | 20% |
266
+ | Fine alignment + NSIM | 0.043 s | 10% |
267
+ | Other (SPL, postproc, SVR, …) | 0.003 s | < 1% |
202
268
 
203
269
  ## Project Structure
204
270
 
@@ -218,18 +284,23 @@ visqol-python/
218
284
  │ ├── alignment.py # Global alignment via cross-correlation
219
285
  │ ├── nsim.py # NSIM similarity metric
220
286
  │ ├── quality_mapper.py # SVR & exponential quality mapping
287
+ │ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
221
288
  │ ├── __main__.py # CLI entry point
289
+ │ ├── py.typed # PEP 561 type marker
222
290
  │ └── model/ # Bundled SVR model
223
291
  │ └── libsvm_nu_svr_model.txt
224
- ├── tests/ # Tests (pytest)
292
+ ├── tests/ # Tests & benchmarks (pytest)
225
293
  │ ├── conftest.py # Shared fixtures & CLI options
226
294
  │ ├── test_quick.py # Smoke tests (no external data needed)
227
- └── test_conformance.py # Full conformance tests (needs testdata)
295
+ ├── test_conformance.py # Full conformance tests (needs testdata)
296
+ │ ├── test_parallel_correctness.py # Numba parallel correctness tests
297
+ │ └── bench_*.py # Performance benchmarks
228
298
  ├── .github/workflows/
229
- │ ├── ci.yml # CI: test on Python 3.9–3.13
299
+ │ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
230
300
  │ └── publish.yml # Auto-publish to PyPI on tag push
231
301
  ├── pyproject.toml # Package metadata & build config
232
302
  ├── CHANGELOG.md
303
+ ├── CONTRIBUTING.md
233
304
  ├── LICENSE
234
305
  └── README.md
235
306
  ```
@@ -250,7 +321,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
250
321
  | glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
251
322
  | contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
252
323
  | castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
253
- | speech_CA01 | Speech | 3.3745 | 3.3678 | 0.006715 |
324
+ | speech_CA01 (polynomial) | Speech | 3.3745 | 3.3756 | 0.001057 |
325
+ | speech_CA01 (lattice) | Speech | 3.3130 | 3.3153 | 0.002341 |
326
+
327
+ 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
328
 
255
329
  ## References
256
330
 
@@ -5,19 +5,26 @@
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) and a fused NSIM + DP patch matching kernel
25
+ - **Optional pyFFTW backend**: `pip install visqol-python[fftw]` routes alignment / xcorr FFTs through FFTW3 — **~16× overall speedup**, RTF 0.036 (vs C++ estimate 0.093)
26
+ - **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
27
+ - **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
21
28
 
22
29
  ## Installation
23
30
 
@@ -25,14 +32,40 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
25
32
  pip install visqol-python
26
33
  ```
27
34
 
35
+ For **C++-default-equivalent speech mode** (deep-lattice TFLite mapper):
36
+
37
+ ```bash
38
+ pip install visqol-python[lattice] # requires Python ≥ 3.10
39
+ ```
40
+
41
+ For **Numba-accelerated** Gammatone filtering and the fused NSIM + DP kernel:
42
+
43
+ ```bash
44
+ pip install visqol-python[accel]
45
+ ```
46
+
47
+ For **FFTW3-backed alignment FFTs** via pyFFTW:
48
+
49
+ ```bash
50
+ pip install visqol-python[fftw]
51
+ ```
52
+
53
+ Install everything (lattice + numba + fftw):
54
+
55
+ ```bash
56
+ pip install visqol-python[all]
57
+ ```
58
+
28
59
  Or install from source:
29
60
 
30
61
  ```bash
31
62
  git clone https://github.com/talker93/visqol-python.git
32
63
  cd visqol-python
33
- pip install -e .
64
+ pip install -e ".[dev]"
34
65
  ```
35
66
 
67
+ > **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)).
68
+
36
69
  ## Quick Start
37
70
 
38
71
  ### Python API
@@ -83,12 +116,15 @@ file_pairs = [
83
116
  ("ref3.wav", "deg3.wav"),
84
117
  ]
85
118
 
86
- # Optional progress callback
119
+ # Sequential with progress callback
87
120
  results = api.measure_batch(
88
121
  file_pairs,
89
122
  progress_callback=lambda done, total: print(f"{done}/{total}"),
90
123
  )
91
124
 
125
+ # Multi-process parallel (uses all CPU cores)
126
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
127
+
92
128
  for pair, result in zip(file_pairs, results):
93
129
  if isinstance(result, Exception):
94
130
  print(f"{pair}: FAILED — {result}")
@@ -115,7 +151,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
115
151
  |------|-------------|
116
152
  | `-r`, `--reference` | Path to reference WAV file (required) |
117
153
  | `-d`, `--degraded` | Path to degraded WAV file (required) |
118
- | `--speech_mode` | Use speech mode (16 kHz, polynomial mapping) |
154
+ | `--speech_mode` | Use speech mode (16 kHz) |
155
+ | `--no_lattice_model` | Speech mode: disable lattice TFLite mapper, use polynomial fallback |
156
+ | `--lattice_model` | Custom path to lattice `.tflite` model (speech mode) |
157
+ | `--unscaled_speech` | Don't scale polynomial speech MOS to 5.0 (polynomial only) |
119
158
  | `--model` | Custom SVR model file path (audio mode only) |
120
159
  | `--search_window` | Search window radius (default: 60) |
121
160
  | `--verbose`, `-v` | Show detailed per-patch results |
@@ -143,21 +182,37 @@ The `measure()` method returns a `SimilarityResult` object with:
143
182
 
144
183
  ### Speech Mode
145
184
  - Target sample rate: **16 kHz**
146
- - 32 Gammatone frequency bands (50 Hz – 8 000 Hz)
147
- - Quality mapping: exponential polynomial fit
185
+ - 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
148
186
  - VAD (Voice Activity Detection) based patch selection
187
+ - Quality mapping (choose one):
188
+ - **Deep-lattice TFLite (default)** — same mapper as C++ ViSQOL's default `--use_lattice_model=true`; requires `pip install visqol-python[lattice]`
189
+ - **Exponential polynomial (fallback)** — same as C++ `--use_lattice_model=false`; used automatically when the lattice runtime is not installed
190
+ - Toggle from Python: `api.create(mode="speech", use_lattice_model=False)`
191
+ - Toggle from CLI: `--no_lattice_model`
149
192
  - Best for: speech, VoIP, telephony
150
193
 
151
194
  ## Performance
152
195
 
153
- Measured on Apple M-series, Python 3.13:
196
+ Measured on Apple M-series, Python 3.13, audio mode on the `guitar48_stereo` 12.5 s conformance case (3-run average):
154
197
 
155
- | Mode | Avg RTF | Typical Time |
156
- |------|---------|-------------|
157
- | Audio (48 kHz) | **0.71x** | 7 – 12 s per file pair |
158
- | Speech (16 kHz) | **0.38x** | ~1 s per file pair |
198
+ | Configuration | RTF | Typical Time | Speedup vs pure Python |
199
+ |---|---|---|---|
200
+ | Pure Python + NumPy/SciPy | 0.58 | ~7 s | 1.0× |
201
+ | + `[accel]` (Numba JIT) | 0.067 | ~0.84 s | 8.7× |
202
+ | + `[accel] [fftw]` (Numba + FFTW3) | **0.036** | **~0.45 s** | **16×** |
159
203
 
160
204
  > RTF (Real-Time Factor) < 1.0 means faster than real-time.
205
+ > With Numba + pyFFTW the Python implementation runs at **2.6× the C++ estimated speed** (C++ RTF ≈ 0.093).
206
+
207
+ Stage-level breakdown of the v3.6.0 fully-accelerated path:
208
+
209
+ | Stage | Time | % |
210
+ |---|---|---|
211
+ | Gammatone filterbank | 0.179 s | 40% |
212
+ | DP Patch matching (fused NSIM kernel) | 0.131 s | 29% |
213
+ | Global alignment (pyFFTW rfft/irfft) | 0.091 s | 20% |
214
+ | Fine alignment + NSIM | 0.043 s | 10% |
215
+ | Other (SPL, postproc, SVR, …) | 0.003 s | < 1% |
161
216
 
162
217
  ## Project Structure
163
218
 
@@ -177,18 +232,23 @@ visqol-python/
177
232
  │ ├── alignment.py # Global alignment via cross-correlation
178
233
  │ ├── nsim.py # NSIM similarity metric
179
234
  │ ├── quality_mapper.py # SVR & exponential quality mapping
235
+ │ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
180
236
  │ ├── __main__.py # CLI entry point
237
+ │ ├── py.typed # PEP 561 type marker
181
238
  │ └── model/ # Bundled SVR model
182
239
  │ └── libsvm_nu_svr_model.txt
183
- ├── tests/ # Tests (pytest)
240
+ ├── tests/ # Tests & benchmarks (pytest)
184
241
  │ ├── conftest.py # Shared fixtures & CLI options
185
242
  │ ├── test_quick.py # Smoke tests (no external data needed)
186
- └── test_conformance.py # Full conformance tests (needs testdata)
243
+ ├── test_conformance.py # Full conformance tests (needs testdata)
244
+ │ ├── test_parallel_correctness.py # Numba parallel correctness tests
245
+ │ └── bench_*.py # Performance benchmarks
187
246
  ├── .github/workflows/
188
- │ ├── ci.yml # CI: test on Python 3.9–3.13
247
+ │ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
189
248
  │ └── publish.yml # Auto-publish to PyPI on tag push
190
249
  ├── pyproject.toml # Package metadata & build config
191
250
  ├── CHANGELOG.md
251
+ ├── CONTRIBUTING.md
192
252
  ├── LICENSE
193
253
  └── README.md
194
254
  ```
@@ -209,7 +269,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
209
269
  | glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
210
270
  | contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
211
271
  | castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
212
- | speech_CA01 | Speech | 3.3745 | 3.3678 | 0.006715 |
272
+ | speech_CA01 (polynomial) | Speech | 3.3745 | 3.3756 | 0.001057 |
273
+ | speech_CA01 (lattice) | Speech | 3.3130 | 3.3153 | 0.002341 |
274
+
275
+ 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
276
 
214
277
  ## References
215
278