visqol-python 3.3.6__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 (40) hide show
  1. visqol_python-3.5.0/CHANGELOG.md +163 -0
  2. {visqol_python-3.3.6 → visqol_python-3.5.0}/PKG-INFO +81 -18
  3. {visqol_python-3.3.6 → visqol_python-3.5.0}/README.md +69 -16
  4. {visqol_python-3.3.6 → visqol_python-3.5.0}/pyproject.toml +36 -3
  5. visqol_python-3.5.0/tests/test_conformance.py +189 -0
  6. visqol_python-3.5.0/tests/test_parallel_correctness.py +96 -0
  7. {visqol_python-3.3.6 → visqol_python-3.5.0}/tests/test_quick.py +9 -9
  8. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/__init__.py +3 -3
  9. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/__main__.py +49 -14
  10. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/alignment.py +7 -11
  11. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/analysis_window.py +3 -6
  12. visqol_python-3.5.0/visqol/api.py +364 -0
  13. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/audio_utils.py +6 -11
  14. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/gammatone.py +144 -44
  15. visqol_python-3.5.0/visqol/model/lattice_tcditugenmeetpackhref_ls2_nl60_lr12_bs2048_learn.005_ep2400_train1_7_raw.tflite +0 -0
  16. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/nsim.py +17 -18
  17. visqol_python-3.5.0/visqol/numba_accel.py +625 -0
  18. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/patch_creator.py +25 -28
  19. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/patch_selector.py +224 -61
  20. visqol_python-3.5.0/visqol/quality_mapper.py +228 -0
  21. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/signal_utils.py +11 -8
  22. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/visqol_core.py +38 -31
  23. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/visqol_manager.py +65 -18
  24. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol_python.egg-info/PKG-INFO +81 -18
  25. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol_python.egg-info/SOURCES.txt +3 -0
  26. visqol_python-3.5.0/visqol_python.egg-info/requires.txt +30 -0
  27. visqol_python-3.3.6/CHANGELOG.md +0 -55
  28. visqol_python-3.3.6/tests/test_conformance.py +0 -125
  29. visqol_python-3.3.6/visqol/api.py +0 -231
  30. visqol_python-3.3.6/visqol/quality_mapper.py +0 -125
  31. visqol_python-3.3.6/visqol_python.egg-info/requires.txt +0 -12
  32. {visqol_python-3.3.6 → visqol_python-3.5.0}/CONTRIBUTING.md +0 -0
  33. {visqol_python-3.3.6 → visqol_python-3.5.0}/LICENSE +0 -0
  34. {visqol_python-3.3.6 → visqol_python-3.5.0}/MANIFEST.in +0 -0
  35. {visqol_python-3.3.6 → visqol_python-3.5.0}/setup.cfg +0 -0
  36. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  37. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol/py.typed +0 -0
  38. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol_python.egg-info/dependency_links.txt +0 -0
  39. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol_python.egg-info/entry_points.txt +0 -0
  40. {visqol_python-3.3.6 → visqol_python-3.5.0}/visqol_python.egg-info/top_level.txt +0 -0
@@ -0,0 +1,163 @@
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.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
+
69
+ ## [3.4.0] - 2026-03-23
70
+
71
+ ### Added
72
+ - **Numba JIT acceleration** (`pip install visqol-python[accel]`):
73
+ - DP patch matching inner loops compiled to machine code via `@njit`
74
+ - Gammatone IIR filterbank compiled with `parallel=True` + `fastmath=True` — frames processed in parallel across all CPU cores
75
+ - NSIM similarity kernel JIT-compiled
76
+ - Automatic `NUMBA_THREADING_LAYER=workqueue` setup for macOS compatibility
77
+ - Zero-loss parallel accuracy (each frame's IIR state is independent)
78
+ - **Batch evaluation API**: `VisqolApi.measure_batch()` with optional `parallel=True` and `max_workers` for multi-process execution
79
+ - Exported `PatchSimilarityResult` and `ProgressCallback` from top-level package
80
+
81
+ ### Performance
82
+ - **12x Gammatone speedup** via parallel + fastmath (1.53s → 0.13s per signal pair)
83
+ - **8.7x DP patch matching speedup** via Numba JIT (3.5s → 0.40s)
84
+ - **Overall 9x speedup**: RTF 0.58 → 0.064 (surpasses C++ estimate of 0.093)
85
+ - Fine alignment skip optimization: 29x speedup when lag == 0
86
+
87
+ ### Improved
88
+ - `__repr__` / `__str__` for `SimilarityResult`, `AudioSignal`, `PatchSimilarityResult`, `Spectrogram`
89
+ - Logging replaces print statements in CLI verbose output
90
+ - Development tooling: ruff lint/format + mypy strict type checking in CI
91
+
92
+ ### Fixed
93
+ - **CI failures**: resolved all ruff lint (308 errors), ruff format (24 files), and mypy (24 errors) issues
94
+ - Added `per-file-ignores` for benchmark test scripts (E402, E702)
95
+ - Added mypy override for `numba_accel.py` (untyped `@njit` decorators)
96
+ - Fixed `no-any-return` errors across `audio_utils.py`, `gammatone.py`, `visqol_core.py`, `api.py`
97
+ - Added `TYPE_CHECKING` imports for `ImagePatchCreator` / `VadPatchCreator` in `visqol_core.py`
98
+
99
+ ## [3.3.6] - 2026-03-23
100
+
101
+ ### Added
102
+ - **Batch evaluation API**: `VisqolApi.measure_batch()` with `progress_callback` support
103
+ - **Numba optional acceleration**: `visqol/numba_accel.py` with JIT-compiled DP forward pass and NSIM kernel
104
+ - `[accel]` optional dependency group: `pip install visqol-python[accel]`
105
+
106
+ ### Improved
107
+ - `GammatoneFilterBank.apply_filter()` pre-builds coefficient arrays (avoids per-channel allocation)
108
+ - `prepare_spectrograms_for_comparison()` vectorized per-frame noise floor
109
+ - Ruff lint/format configuration added to `pyproject.toml`
110
+ - CI enhanced with lint and type-check jobs
111
+ - Development dependencies: `[project.optional-dependencies] dev`
112
+
113
+ ## [3.3.5] - 2026-03-23
114
+
115
+ ### Added
116
+ - **Type hints** on all public and internal APIs (`from __future__ import annotations`)
117
+ - **`py.typed`** marker (PEP 561) — mypy / pyright can now type-check dependents
118
+ - **CONTRIBUTING.md** with development setup, code style, and PR guidelines
119
+ - Exported `SimilarityResult` and `AudioSignal` from top-level `visqol` package
120
+ - `mypy` configuration in `pyproject.toml`
121
+
122
+ ### Improved
123
+ - **Error handling**: friendly `ValueError` / `FileNotFoundError` / `TypeError` throughout:
124
+ - `VisqolApi.create()` now validates mode, search_window, and model_path
125
+ - `VisqolApi.measure()` checks file existence before processing
126
+ - `VisqolApi.measure_from_arrays()` validates array types, emptiness, and sample rate
127
+ - `AudioSignal` validates sample rate on construction
128
+ - `AnalysisWindow` validates sample_rate and overlap range
129
+ - CLI now catches exceptions and prints user-friendly error messages
130
+ - `AnalysisWindow.apply_hann_window()` uses `ValueError` instead of bare `assert`
131
+
132
+ ## [3.3.4] - 2026-03-23
133
+
134
+ ### Improved
135
+ - Tests rewritten in **pytest** format with `parametrize` and fixtures
136
+ - Added **CI workflow** (GitHub Actions): auto-test on Python 3.9–3.13 for every push/PR
137
+ - Added **smoke tests** (`test_quick.py`) that run without external testdata
138
+ - Version number now managed in a single place (`visqol/__init__.py`)
139
+ - Removed redundant `setup.py` — `pyproject.toml` is the single source of truth
140
+ - Added this CHANGELOG
141
+ - README: added PyPI / CI / License badges
142
+
143
+ ### Fixed
144
+ - `requires-python` updated from `>=3.8` to `>=3.9` (numpy/scipy dropped 3.8 support)
145
+
146
+ ## [3.3.3] - 2026-03-23
147
+
148
+ ### Added
149
+ - Initial PyPI release as `visqol-python`
150
+ - Pure Python port of [Google's ViSQOL v3.3.3](https://github.com/google/visqol)
151
+ - **Audio mode** (48 kHz, SVR quality mapping) — 10/10 conformance tests pass
152
+ - **Speech mode** (16 kHz, exponential polynomial mapping) — 1/1 conformance test passes
153
+ - Python API: `VisqolApi.measure()` and `VisqolApi.measure_from_arrays()`
154
+ - CLI: `python -m visqol` / `visqol` command
155
+ - Bundled SVR model (`libsvm_nu_svr_model.txt`)
156
+ - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
157
+
158
+ [3.5.0]: https://github.com/talker93/visqol-python/compare/v3.4.0...v3.5.0
159
+ [3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
160
+ [3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
161
+ [3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
162
+ [3.3.4]: https://github.com/talker93/visqol-python/compare/v3.3.3...v3.3.4
163
+ [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.3.6
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
@@ -28,12 +29,21 @@ Requires-Dist: numpy>=1.20
28
29
  Requires-Dist: scipy>=1.7
29
30
  Requires-Dist: soundfile>=0.10
30
31
  Requires-Dist: libsvm-official>=3.25
32
+ Provides-Extra: accel
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"
31
39
  Provides-Extra: test
32
40
  Requires-Dist: pytest>=7.0; extra == "test"
33
41
  Provides-Extra: dev
34
42
  Requires-Dist: pytest>=7.0; extra == "dev"
35
43
  Requires-Dist: ruff>=0.4; extra == "dev"
36
44
  Requires-Dist: mypy>=1.8; extra == "dev"
45
+ Requires-Dist: numba>=0.57; extra == "dev"
46
+ Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "dev"
37
47
  Dynamic: license-file
38
48
 
39
49
  # ViSQOL (Python)
@@ -43,19 +53,25 @@ Dynamic: license-file
43
53
  [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
44
54
  [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
45
55
 
46
- 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.
47
57
 
48
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**.
49
59
 
50
60
  ## Features
51
61
 
52
62
  - **Two modes**: Audio mode (music/general audio at 48 kHz) and Speech mode (speech at 16 kHz)
53
- - **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
54
64
  - Audio mode: 9/10 tests produce **identical** MOS scores (diff = 0.000000), 1 test diff = 0.000117
55
- - Speech mode: diff = 0.006715
56
- - **Pure Python**: no C/C++ compilation required
57
- - **Minimal dependencies**: only 4 pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
58
- - **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
59
75
 
60
76
  ## Installation
61
77
 
@@ -63,14 +79,34 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
63
79
  pip install visqol-python
64
80
  ```
65
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
+
66
100
  Or install from source:
67
101
 
68
102
  ```bash
69
103
  git clone https://github.com/talker93/visqol-python.git
70
104
  cd visqol-python
71
- pip install -e .
105
+ pip install -e ".[dev]"
72
106
  ```
73
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
+
74
110
  ## Quick Start
75
111
 
76
112
  ### Python API
@@ -121,12 +157,15 @@ file_pairs = [
121
157
  ("ref3.wav", "deg3.wav"),
122
158
  ]
123
159
 
124
- # Optional progress callback
160
+ # Sequential with progress callback
125
161
  results = api.measure_batch(
126
162
  file_pairs,
127
163
  progress_callback=lambda done, total: print(f"{done}/{total}"),
128
164
  )
129
165
 
166
+ # Multi-process parallel (uses all CPU cores)
167
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
168
+
130
169
  for pair, result in zip(file_pairs, results):
131
170
  if isinstance(result, Exception):
132
171
  print(f"{pair}: FAILED — {result}")
@@ -153,7 +192,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
153
192
  |------|-------------|
154
193
  | `-r`, `--reference` | Path to reference WAV file (required) |
155
194
  | `-d`, `--degraded` | Path to degraded WAV file (required) |
156
- | `--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) |
157
199
  | `--model` | Custom SVR model file path (audio mode only) |
158
200
  | `--search_window` | Search window radius (default: 60) |
159
201
  | `--verbose`, `-v` | Show detailed per-patch results |
@@ -181,21 +223,34 @@ The `measure()` method returns a `SimilarityResult` object with:
181
223
 
182
224
  ### Speech Mode
183
225
  - Target sample rate: **16 kHz**
184
- - 32 Gammatone frequency bands (50 Hz – 8 000 Hz)
185
- - Quality mapping: exponential polynomial fit
226
+ - 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
186
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`
187
233
  - Best for: speech, VoIP, telephony
188
234
 
189
235
  ## Performance
190
236
 
191
237
  Measured on Apple M-series, Python 3.13:
192
238
 
239
+ ### Without Numba (pure Python + NumPy/SciPy)
240
+
193
241
  | Mode | Avg RTF | Typical Time |
194
242
  |------|---------|-------------|
195
- | Audio (48 kHz) | **0.71x** | 7 – 12 s per file pair |
243
+ | Audio (48 kHz) | **0.18x** | ~2.2 s per file pair |
196
244
  | Speech (16 kHz) | **0.38x** | ~1 s per file pair |
197
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
+
198
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).
199
254
 
200
255
  ## Project Structure
201
256
 
@@ -215,18 +270,23 @@ visqol-python/
215
270
  │ ├── alignment.py # Global alignment via cross-correlation
216
271
  │ ├── nsim.py # NSIM similarity metric
217
272
  │ ├── quality_mapper.py # SVR & exponential quality mapping
273
+ │ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
218
274
  │ ├── __main__.py # CLI entry point
275
+ │ ├── py.typed # PEP 561 type marker
219
276
  │ └── model/ # Bundled SVR model
220
277
  │ └── libsvm_nu_svr_model.txt
221
- ├── tests/ # Tests (pytest)
278
+ ├── tests/ # Tests & benchmarks (pytest)
222
279
  │ ├── conftest.py # Shared fixtures & CLI options
223
280
  │ ├── test_quick.py # Smoke tests (no external data needed)
224
- └── 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
225
284
  ├── .github/workflows/
226
- │ ├── ci.yml # CI: test on Python 3.9–3.13
285
+ │ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
227
286
  │ └── publish.yml # Auto-publish to PyPI on tag push
228
287
  ├── pyproject.toml # Package metadata & build config
229
288
  ├── CHANGELOG.md
289
+ ├── CONTRIBUTING.md
230
290
  ├── LICENSE
231
291
  └── README.md
232
292
  ```
@@ -247,7 +307,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
247
307
  | glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
248
308
  | contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
249
309
  | castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
250
- | 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.
251
314
 
252
315
  ## References
253
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",
@@ -37,11 +39,19 @@ dependencies = [
37
39
  ]
38
40
 
39
41
  [project.optional-dependencies]
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
+ ]
40
48
  test = ["pytest>=7.0"]
41
49
  dev = [
42
50
  "pytest>=7.0",
43
51
  "ruff>=0.4",
44
52
  "mypy>=1.8",
53
+ "numba>=0.57",
54
+ "ai-edge-litert>=2.1.5; python_version >= '3.10'",
45
55
  ]
46
56
 
47
57
  [project.urls]
@@ -61,7 +71,7 @@ version = {attr = "visqol.__version__"}
61
71
  exclude = ["tests*"]
62
72
 
63
73
  [tool.setuptools.package-data]
64
- visqol = ["model/*.txt", "py.typed"]
74
+ visqol = ["model/*.txt", "model/*.tflite", "py.typed"]
65
75
 
66
76
  [tool.pytest.ini_options]
67
77
  testpaths = ["tests"]
@@ -81,7 +91,16 @@ select = [
81
91
  "SIM", # flake8-simplify
82
92
  "RUF", # ruff-specific rules
83
93
  ]
84
- ignore = ["E501"] # line length handled by formatter
94
+ ignore = [
95
+ "E501", # line length handled by formatter
96
+ "RUF001", # ambiguous unicode in string (intentional mathematical symbols)
97
+ "RUF002", # ambiguous unicode in docstring (intentional: ×, −)
98
+ "RUF003", # ambiguous unicode in comment (intentional: ×, →)
99
+ ]
100
+
101
+ [tool.ruff.lint.per-file-ignores]
102
+ "tests/bench_*.py" = ["E402", "E702"] # bench scripts use sys.path manipulation
103
+ "tests/test_parallel_correctness.py" = ["E402"]
85
104
 
86
105
  [tool.ruff.lint.isort]
87
106
  known-first-party = ["visqol"]
@@ -92,5 +111,19 @@ warn_return_any = true
92
111
  warn_unused_configs = true
93
112
 
94
113
  [[tool.mypy.overrides]]
95
- module = ["svmutil.*", "libsvm.*", "soundfile.*"]
114
+ module = [
115
+ "svmutil.*",
116
+ "libsvm.*",
117
+ "soundfile.*",
118
+ "numba.*",
119
+ "ai_edge_litert.*",
120
+ ]
96
121
  ignore_missing_imports = true
122
+
123
+ [[tool.mypy.overrides]]
124
+ module = ["visqol.numba_accel"]
125
+ # numba @njit decorators are untyped; strict mode is not feasible here
126
+ strict = false
127
+ disallow_untyped_defs = false
128
+ disallow_untyped_decorators = false
129
+ warn_unused_ignores = false