visqol-python 3.6.0__tar.gz → 3.8.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 (67) hide show
  1. {visqol_python-3.6.0 → visqol_python-3.8.0}/CHANGELOG.md +81 -0
  2. {visqol_python-3.6.0 → visqol_python-3.8.0}/CONTRIBUTING.md +23 -0
  3. visqol_python-3.8.0/MANIFEST.in +18 -0
  4. {visqol_python-3.6.0/visqol_python.egg-info → visqol_python-3.8.0}/PKG-INFO +70 -51
  5. {visqol_python-3.6.0 → visqol_python-3.8.0}/README.md +65 -45
  6. visqol_python-3.8.0/docs/benchmarks/2026-09-16-linux-x86_64.json +45 -0
  7. visqol_python-3.8.0/docs/benchmarks/2026-09-16-macos-arm64.json +45 -0
  8. visqol_python-3.8.0/docs/benchmarks/2026-09-20-windows-v3.8.0-workloads.json +201 -0
  9. visqol_python-3.8.0/docs/benchmarks/2026-09-20-windows-validation.json +61 -0
  10. visqol_python-3.8.0/docs/benchmarks/2026-09-20-windows-x86_64.json +45 -0
  11. visqol_python-3.8.0/docs/benchmarks/windows-2026-09-20.json +192 -0
  12. visqol_python-3.8.0/docs/portable-cpu-optimization.md +204 -0
  13. visqol_python-3.8.0/docs/portable-cpu-windows-validation.md +121 -0
  14. visqol_python-3.8.0/docs/releasing.md +102 -0
  15. visqol_python-3.8.0/docs/v3.8.0-validation.md +78 -0
  16. visqol_python-3.8.0/docs/windows-validation.md +164 -0
  17. {visqol_python-3.6.0 → visqol_python-3.8.0}/pyproject.toml +14 -8
  18. visqol_python-3.8.0/tests/_fft_worker.py +9 -0
  19. visqol_python-3.8.0/tests/_legacy_kernels.py +188 -0
  20. visqol_python-3.8.0/tests/_result_comparison.py +23 -0
  21. visqol_python-3.8.0/tests/bench_platform.py +230 -0
  22. visqol_python-3.8.0/tests/bench_portable.py +111 -0
  23. visqol_python-3.8.0/tests/bench_release.py +239 -0
  24. visqol_python-3.8.0/tests/check_validation.py +43 -0
  25. visqol_python-3.8.0/tests/conformance_files.json +111 -0
  26. visqol_python-3.8.0/tests/conftest.py +49 -0
  27. visqol_python-3.8.0/tests/fetch_conformance_data.py +57 -0
  28. visqol_python-3.8.0/tests/requirements-min-accel.txt +8 -0
  29. visqol_python-3.8.0/tests/requirements-min-base.txt +5 -0
  30. visqol_python-3.8.0/tests/test_acceleration.py +114 -0
  31. {visqol_python-3.6.0 → visqol_python-3.8.0}/tests/test_conformance.py +38 -9
  32. visqol_python-3.8.0/tests/test_parallel_correctness.py +68 -0
  33. visqol_python-3.8.0/tests/test_platform.py +220 -0
  34. visqol_python-3.8.0/tests/test_process_pool.py +39 -0
  35. visqol_python-3.8.0/tests/verify_distribution.py +180 -0
  36. visqol_python-3.8.0/tests/verify_release.py +62 -0
  37. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/__init__.py +1 -1
  38. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/api.py +11 -4
  39. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/numba_accel.py +167 -95
  40. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/patch_selector.py +16 -15
  41. visqol_python-3.8.0/visqol/py.typed +0 -0
  42. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/quality_mapper.py +83 -7
  43. {visqol_python-3.6.0 → visqol_python-3.8.0/visqol_python.egg-info}/PKG-INFO +70 -51
  44. visqol_python-3.8.0/visqol_python.egg-info/SOURCES.txt +62 -0
  45. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol_python.egg-info/requires.txt +2 -8
  46. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol_python.egg-info/top_level.txt +0 -1
  47. visqol_python-3.6.0/MANIFEST.in +0 -5
  48. visqol_python-3.6.0/tests/test_parallel_correctness.py +0 -96
  49. visqol_python-3.6.0/visqol_python.egg-info/SOURCES.txt +0 -33
  50. {visqol_python-3.6.0 → visqol_python-3.8.0}/LICENSE +0 -0
  51. {visqol_python-3.6.0 → visqol_python-3.8.0}/setup.cfg +0 -0
  52. /visqol_python-3.6.0/visqol/py.typed → /visqol_python-3.8.0/tests/__init__.py +0 -0
  53. {visqol_python-3.6.0 → visqol_python-3.8.0}/tests/test_quick.py +0 -0
  54. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/__main__.py +0 -0
  55. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/alignment.py +0 -0
  56. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/analysis_window.py +0 -0
  57. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/audio_utils.py +0 -0
  58. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/gammatone.py +0 -0
  59. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/model/lattice_tcditugenmeetpackhref_ls2_nl60_lr12_bs2048_learn.005_ep2400_train1_7_raw.tflite +0 -0
  60. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  61. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/nsim.py +0 -0
  62. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/patch_creator.py +0 -0
  63. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/signal_utils.py +0 -0
  64. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/visqol_core.py +0 -0
  65. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol/visqol_manager.py +0 -0
  66. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol_python.egg-info/dependency_links.txt +0 -0
  67. {visqol_python-3.6.0 → visqol_python-3.8.0}/visqol_python.egg-info/entry_points.txt +0 -0
@@ -4,6 +4,84 @@ 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
+ ## [Unreleased]
8
+
9
+ ## [3.8.0] - 2026-09-20
10
+
11
+ ### Performance
12
+ - Cache reference-patch statistics, replace DP predecessor scans with prefix
13
+ maxima, and evaluate independent candidates in parallel. Preserve the original
14
+ tie rule and reduction order.
15
+ - Fuse four Gammatone IIR stages with RMS accumulation, removing the large
16
+ intermediate buffer while preserving FP64 arithmetic and filter resets.
17
+ - Keep scoring models, search windows, alignment stages and public API unchanged.
18
+ Recorded warmed comparisons against accelerated v3.7.0 are 2.57× on Apple M4
19
+ Pro, 2.80× on AMD EPYC 9K84 and 2.29× on Windows Core Ultra 9 285H; see the
20
+ [protocol and raw measurements](docs/portable-cpu-windows-validation.md).
21
+
22
+ ### Fixed
23
+ - Support Windows Unicode SVR model paths using the native filename encoding,
24
+ short filenames and temporary copies. Try the system temp directory if the
25
+ user's temp path is also unusable. Invalid models fail during initialization.
26
+ - Read lattice model bytes through Python so Unicode installation paths work.
27
+ - Cap automatic Windows process counts at 61 and document the spawn entry-point
28
+ guard and the cost of starting pools for small batches.
29
+ - Keep benchmark scripts out of pytest collection and reject invalid explicit
30
+ conformance-data paths. Resolve NumPy/SciPy together and exclude unsupported
31
+ NumPy 1.x/Python 3.13 combinations.
32
+
33
+ ### Tests and release validation
34
+ - Retain the frozen v3.7.0 kernel oracles, exact output checks, backend numerical
35
+ comparisons, real process-pool tests and Windows filename/CLI regressions.
36
+ - Tighten C++ MOS tolerances to 0.0002 for audio and 0.003 for speech, separately
37
+ from the exact old/new checks. Verify the official audio data by Git blob SHA.
38
+ - Validate both FFT backends on Linux, macOS and Windows; expand Windows to
39
+ Python 3.10–3.13 and exercise older compatible dependency combinations.
40
+ - Add long-audio, speech, batch and fresh-cache comparisons. Include the test
41
+ helpers, benchmarks and validation documents in sdists; keep wheels package-only.
42
+ - Gate publication on tag/version/changelog checks, full CI of the tagged commit
43
+ and installation of the built wheel in Unicode environments on three platforms.
44
+ Publish the tested artifact without rebuilding it. See [releasing](docs/releasing.md).
45
+
46
+ ## [3.7.0] - 2026-05-30
47
+
48
+ ### Fixed
49
+ - **Audio-mode C++ parity in fine realignment** (`finely_align_and_recreate_patches`):
50
+ the previous "B2 optimisation" skipped the per-patch spectrogram rebuild
51
+ whenever the fine-alignment lag was zero, on the assumption that the rebuilt
52
+ spectrogram would be identical to the original patch. That assumption is
53
+ wrong. The original patch is sliced out of the **full-signal** Gammatone
54
+ spectrogram (warm IIR filter state, carrying history), whereas the rebuild
55
+ runs Gammatone over the **sliced** audio from a cold filter state — so the
56
+ leading frames differ even when no shift is applied. C++ ViSQOL always
57
+ rebuilds and keeps whichever scores higher, so skipping it silently dropped
58
+ that improvement. The skip is removed; every matched patch is now rebuilt and
59
+ compared, exactly as C++ does.
60
+ - **Impact:** audio MOS-LQO now matches the C++ binary to ULP. Max diff over
61
+ the 10 conformance cases dropped from **2.41 × 10⁻² → 1.17 × 10⁻⁴**; 9 of 10
62
+ cases are bit-exact (~10⁻¹⁴). The lone residual (`contrabassoon_24aac`,
63
+ 1.17 × 10⁻⁴) is an unrelated sub-sample boundary-patch alignment rounding,
64
+ now smaller than the speech-mode parity gap.
65
+ - Existing audio scores shift by up to ~0.024 MOS where they were previously
66
+ too low; this is a move **toward** the C++ ground truth, not away from it.
67
+ - Speech polynomial and lattice scores are unaffected (bit-identical before
68
+ and after).
69
+
70
+ ### Performance
71
+ - Fine realignment now rebuilds the spectrogram for every patch (including
72
+ `lag == 0`), so it no longer benefits from the zero-lag skip. Net effect is
73
+ roughly **1.4–1.5× slower realignment** (e.g. `guitar48_stereo` 0.44 s → 0.65 s
74
+ end-to-end with Numba + pyFFTW). This is the cost of exact C++ parity and is
75
+ consistent with the project's precision-first policy. Audio-mode RTF rises
76
+ from ~0.036 (3.6.0) to ~0.054 — still ~1.7× faster than the C++ binary
77
+ (~0.093) and faster than 3.5.0 (~0.067).
78
+
79
+ ### Changed
80
+ - **Dropped Python 3.9 support** (`requires-python = ">=3.10"`). Downstream
81
+ projects still on 3.9 should pin `visqol-python<3.7`.
82
+ - CI: bumped GitHub Actions to Node-24-compatible major versions
83
+ (checkout v5, setup-python v6, upload/download-artifact v5).
84
+
7
85
  ## [3.6.0] - 2026-05-27
8
86
 
9
87
  ### Added
@@ -218,6 +296,9 @@ All v3.5.0 conformance baselines preserved within ULP precision:
218
296
  - Bundled SVR model (`libsvm_nu_svr_model.txt`)
219
297
  - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
220
298
 
299
+ [Unreleased]: https://github.com/talker93/visqol-python/compare/v3.7.0...HEAD
300
+ [3.8.0]: https://github.com/talker93/visqol-python/compare/v3.7.0...v3.8.0
301
+ [3.7.0]: https://github.com/talker93/visqol-python/compare/v3.6.0...v3.7.0
221
302
  [3.6.0]: https://github.com/talker93/visqol-python/compare/v3.5.0...v3.6.0
222
303
  [3.5.0]: https://github.com/talker93/visqol-python/compare/v3.4.0...v3.5.0
223
304
  [3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
@@ -37,6 +37,29 @@ pytest tests/test_quick.py -v
37
37
  pytest tests/test_conformance.py -v --testdata /path/to/visqol/testdata
38
38
  ```
39
39
 
40
+ On Windows PowerShell, an isolated setup with all optional backends is:
41
+
42
+ ```powershell
43
+ python -m venv .venv
44
+ .\.venv\Scripts\python.exe -m pip install -e ".[dev]"
45
+ .\.venv\Scripts\python.exe -m pytest tests -v
46
+ ```
47
+
48
+ The normal test suite includes real `spawn` multiprocessing, Unicode file
49
+ paths, and numerical comparisons of Numba/FFTW against the fallback paths.
50
+ Optional backend tests skip when their dependencies are absent; conformance
51
+ tests need the official testdata. Experimental `bench_*.py` scripts are not
52
+ collected by pytest.
53
+
54
+ For official testdata setup, repeatable performance measurements and the
55
+ Windows compatibility results, see [Windows validation](docs/windows-validation.md).
56
+
57
+ For accelerated changes, fetch verified official data with
58
+ `python tests/fetch_conformance_data.py` and run both FFT backends as described
59
+ in [release validation](docs/releasing.md). Preserve the frozen v3.7.0 kernels;
60
+ compare every public result field against them. The reusable CI workflow also
61
+ checks installed wheels and older compatible dependency combinations.
62
+
40
63
  ## Code Style
41
64
 
42
65
  - **Type hints**: All public functions and methods must include type annotations.
@@ -0,0 +1,18 @@
1
+ include README.md
2
+ include LICENSE
3
+ include CHANGELOG.md
4
+ include CONTRIBUTING.md
5
+ recursive-include visqol/model *.txt
6
+ recursive-include docs *.md *.json
7
+ include tests/_*.py
8
+ include tests/conftest.py
9
+ include tests/test_*.py
10
+ include tests/bench_portable.py
11
+ include tests/bench_platform.py
12
+ include tests/bench_release.py
13
+ include tests/check_validation.py
14
+ include tests/verify_distribution.py
15
+ include tests/verify_release.py
16
+ include tests/requirements-min-*.txt
17
+ include tests/fetch_conformance_data.py
18
+ include tests/conformance_files.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.6.0
3
+ Version: 3.8.0
4
4
  Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
5
5
  Author: Shan Jiang
6
6
  License-Expression: Apache-2.0
@@ -14,7 +14,6 @@ Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Intended Audience :: Developers
15
15
  Classifier: Intended Audience :: Science/Research
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
@@ -22,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.13
22
21
  Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
23
22
  Classifier: Topic :: Scientific/Engineering
24
23
  Classifier: Typing :: Typed
25
- Requires-Python: >=3.9
24
+ Requires-Python: >=3.10
26
25
  Description-Content-Type: text/markdown
27
26
  License-File: LICENSE
28
27
  Requires-Dist: numpy>=1.20
@@ -32,12 +31,12 @@ Requires-Dist: libsvm-official>=3.25
32
31
  Provides-Extra: accel
33
32
  Requires-Dist: numba>=0.57; extra == "accel"
34
33
  Provides-Extra: lattice
35
- Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "lattice"
34
+ Requires-Dist: ai-edge-litert>=2.1.5; extra == "lattice"
36
35
  Provides-Extra: fftw
37
36
  Requires-Dist: pyfftw>=0.13; extra == "fftw"
38
37
  Provides-Extra: all
39
38
  Requires-Dist: numba>=0.57; extra == "all"
40
- Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "all"
39
+ Requires-Dist: ai-edge-litert>=2.1.5; extra == "all"
41
40
  Requires-Dist: pyfftw>=0.13; extra == "all"
42
41
  Provides-Extra: test
43
42
  Requires-Dist: pytest>=7.0; extra == "test"
@@ -46,7 +45,7 @@ Requires-Dist: pytest>=7.0; extra == "dev"
46
45
  Requires-Dist: ruff>=0.4; extra == "dev"
47
46
  Requires-Dist: mypy>=1.8; extra == "dev"
48
47
  Requires-Dist: numba>=0.57; extra == "dev"
49
- Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "dev"
48
+ Requires-Dist: ai-edge-litert>=2.1.5; extra == "dev"
50
49
  Requires-Dist: pyfftw>=0.13; extra == "dev"
51
50
  Dynamic: license-file
52
51
 
@@ -71,10 +70,10 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
71
70
  - **Two speech quality mappers** matching C++ ViSQOL:
72
71
  - **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
73
72
  - **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)
73
+ - **Python scoring implementation**: installable with pip; some native dependencies need a compiler when a wheel is unavailable (see [platform setup](docs/portable-cpu-optimization.md))
75
74
  - **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)
75
+ - **Optional Numba acceleration**: `pip install visqol-python[accel]` for parallel Gammatone filtering and DP patch matching, preserving strict FP64 arithmetic
76
+ - **Optional pyFFTW backend**: `pip install visqol-python[fftw]` routes alignment and cross-correlation FFTs through FFTW3; see [Performance](#performance) for version comparisons
78
77
  - **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
79
78
  - **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
80
79
 
@@ -159,31 +158,43 @@ print(f"MOS-LQO: {result.moslqo:.4f}")
159
158
  ```python
160
159
  from visqol import VisqolApi
161
160
 
162
- api = VisqolApi()
163
- api.create(mode="audio")
164
161
 
165
- file_pairs = [
166
- ("ref1.wav", "deg1.wav"),
167
- ("ref2.wav", "deg2.wav"),
168
- ("ref3.wav", "deg3.wav"),
169
- ]
170
-
171
- # Sequential with progress callback
172
- results = api.measure_batch(
173
- file_pairs,
174
- progress_callback=lambda done, total: print(f"{done}/{total}"),
175
- )
176
-
177
- # Multi-process parallel (uses all CPU cores)
178
- results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
179
-
180
- for pair, result in zip(file_pairs, results):
181
- if isinstance(result, Exception):
182
- print(f"{pair}: FAILED — {result}")
183
- else:
184
- print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
162
+ def main():
163
+ api = VisqolApi()
164
+ api.create(mode="audio")
165
+
166
+ file_pairs = [
167
+ ("ref1.wav", "deg1.wav"),
168
+ ("ref2.wav", "deg2.wav"),
169
+ ("ref3.wav", "deg3.wav"),
170
+ ]
171
+
172
+ # Sequential with progress callback
173
+ results = api.measure_batch(
174
+ file_pairs,
175
+ parallel=False,
176
+ progress_callback=lambda done, total: print(f"{done}/{total}"),
177
+ )
178
+
179
+ # Multi-process parallel
180
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
181
+
182
+ for pair, result in zip(file_pairs, results):
183
+ if isinstance(result, Exception):
184
+ print(f"{pair}: FAILED — {result}")
185
+ else:
186
+ print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
187
+
188
+
189
+ if __name__ == "__main__":
190
+ main()
185
191
  ```
186
192
 
193
+ Save parallel batch code in a Python script and keep the `__main__` guard:
194
+ Windows starts fresh Python processes that import the script. For notebooks
195
+ or interactive sessions, use `parallel=False`. Process startup can make small
196
+ batches slower than sequential evaluation.
197
+
187
198
  ### Command Line
188
199
 
189
200
  ```bash
@@ -245,26 +256,34 @@ The `measure()` method returns a `SimilarityResult` object with:
245
256
 
246
257
  ## Performance
247
258
 
248
- Measured on Apple M-series, Python 3.13, audio mode on the `guitar48_stereo` 12.5 s conformance case (3-run average):
249
-
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×** |
255
-
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% |
259
+ The v3.8.0 CPU kernels cache reference statistics, parallelize independent
260
+ patch candidates and fuse Gammatone filtering with energy accumulation.
261
+ They preserve scoring models, search ranges, both alignment stages and FP64
262
+ arithmetic. Install `visqol-python[accel,fftw]` to use the measured path.
263
+
264
+ Same-machine comparisons against **accelerated v3.7.0**, recorded during
265
+ kernel validation:
266
+
267
+ | Platform / CPU | v3.7.0 median | Optimized median | Speedup |
268
+ |---|---:|---:|---:|
269
+ | macOS arm64 / Apple M4 Pro | 1.080 s | 0.421 s | 2.57× |
270
+ | Linux x86-64 / AMD EPYC 9K84 | 1.510 s | 0.540 s | 2.80× |
271
+ | Windows x86-64 / Intel Core Ultra 9 285H | 1.074 s | 0.469 s | 2.29× |
272
+
273
+ Protocol: the same 12.454-second official guitar case, four Numba threads,
274
+ Numba 0.65.1, pyFFTW 0.15.0, five-run medians after warm-up, excluding WAV I/O
275
+ and first-use compilation. All tested old/new output fields were exactly equal
276
+ within each platform/backend. These ratios describe that workload; cold starts,
277
+ longer audio and batch process startup are measured separately by
278
+ `tests/bench_release.py`. Set `NUMBA_NUM_THREADS` before importing the package,
279
+ and budget workers × threads when using process pools.
280
+
281
+ See the [measurement records](docs/portable-cpu-windows-validation.md),
282
+ [integrated validation and workload results](docs/v3.8.0-validation.md), and
283
+ [release validation procedure](docs/releasing.md). The earlier Windows
284
+ [approximately 10× result](docs/windows-validation.md) compared accelerator
285
+ configurations within v3.7.0. Historical v3.6.0 measurements remain in the
286
+ [CHANGELOG](CHANGELOG.md); they predate the v3.7.0 alignment correction.
268
287
 
269
288
  ## Project Structure
270
289
 
@@ -19,10 +19,10 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
19
19
  - **Two speech quality mappers** matching C++ ViSQOL:
20
20
  - **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
21
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)
22
+ - **Python scoring implementation**: installable with pip; some native dependencies need a compiler when a wheel is unavailable (see [platform setup](docs/portable-cpu-optimization.md))
23
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)
24
+ - **Optional Numba acceleration**: `pip install visqol-python[accel]` for parallel Gammatone filtering and DP patch matching, preserving strict FP64 arithmetic
25
+ - **Optional pyFFTW backend**: `pip install visqol-python[fftw]` routes alignment and cross-correlation FFTs through FFTW3; see [Performance](#performance) for version comparisons
26
26
  - **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
27
27
  - **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
28
28
 
@@ -107,31 +107,43 @@ print(f"MOS-LQO: {result.moslqo:.4f}")
107
107
  ```python
108
108
  from visqol import VisqolApi
109
109
 
110
- api = VisqolApi()
111
- api.create(mode="audio")
112
110
 
113
- file_pairs = [
114
- ("ref1.wav", "deg1.wav"),
115
- ("ref2.wav", "deg2.wav"),
116
- ("ref3.wav", "deg3.wav"),
117
- ]
118
-
119
- # Sequential with progress callback
120
- results = api.measure_batch(
121
- file_pairs,
122
- progress_callback=lambda done, total: print(f"{done}/{total}"),
123
- )
124
-
125
- # Multi-process parallel (uses all CPU cores)
126
- results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
127
-
128
- for pair, result in zip(file_pairs, results):
129
- if isinstance(result, Exception):
130
- print(f"{pair}: FAILED — {result}")
131
- else:
132
- print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
111
+ def main():
112
+ api = VisqolApi()
113
+ api.create(mode="audio")
114
+
115
+ file_pairs = [
116
+ ("ref1.wav", "deg1.wav"),
117
+ ("ref2.wav", "deg2.wav"),
118
+ ("ref3.wav", "deg3.wav"),
119
+ ]
120
+
121
+ # Sequential with progress callback
122
+ results = api.measure_batch(
123
+ file_pairs,
124
+ parallel=False,
125
+ progress_callback=lambda done, total: print(f"{done}/{total}"),
126
+ )
127
+
128
+ # Multi-process parallel
129
+ results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
130
+
131
+ for pair, result in zip(file_pairs, results):
132
+ if isinstance(result, Exception):
133
+ print(f"{pair}: FAILED — {result}")
134
+ else:
135
+ print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
133
140
  ```
134
141
 
142
+ Save parallel batch code in a Python script and keep the `__main__` guard:
143
+ Windows starts fresh Python processes that import the script. For notebooks
144
+ or interactive sessions, use `parallel=False`. Process startup can make small
145
+ batches slower than sequential evaluation.
146
+
135
147
  ### Command Line
136
148
 
137
149
  ```bash
@@ -193,26 +205,34 @@ The `measure()` method returns a `SimilarityResult` object with:
193
205
 
194
206
  ## Performance
195
207
 
196
- Measured on Apple M-series, Python 3.13, audio mode on the `guitar48_stereo` 12.5 s conformance case (3-run average):
197
-
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×** |
203
-
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% |
208
+ The v3.8.0 CPU kernels cache reference statistics, parallelize independent
209
+ patch candidates and fuse Gammatone filtering with energy accumulation.
210
+ They preserve scoring models, search ranges, both alignment stages and FP64
211
+ arithmetic. Install `visqol-python[accel,fftw]` to use the measured path.
212
+
213
+ Same-machine comparisons against **accelerated v3.7.0**, recorded during
214
+ kernel validation:
215
+
216
+ | Platform / CPU | v3.7.0 median | Optimized median | Speedup |
217
+ |---|---:|---:|---:|
218
+ | macOS arm64 / Apple M4 Pro | 1.080 s | 0.421 s | 2.57× |
219
+ | Linux x86-64 / AMD EPYC 9K84 | 1.510 s | 0.540 s | 2.80× |
220
+ | Windows x86-64 / Intel Core Ultra 9 285H | 1.074 s | 0.469 s | 2.29× |
221
+
222
+ Protocol: the same 12.454-second official guitar case, four Numba threads,
223
+ Numba 0.65.1, pyFFTW 0.15.0, five-run medians after warm-up, excluding WAV I/O
224
+ and first-use compilation. All tested old/new output fields were exactly equal
225
+ within each platform/backend. These ratios describe that workload; cold starts,
226
+ longer audio and batch process startup are measured separately by
227
+ `tests/bench_release.py`. Set `NUMBA_NUM_THREADS` before importing the package,
228
+ and budget workers × threads when using process pools.
229
+
230
+ See the [measurement records](docs/portable-cpu-windows-validation.md),
231
+ [integrated validation and workload results](docs/v3.8.0-validation.md), and
232
+ [release validation procedure](docs/releasing.md). The earlier Windows
233
+ [approximately 10× result](docs/windows-validation.md) compared accelerator
234
+ configurations within v3.7.0. Historical v3.6.0 measurements remain in the
235
+ [CHANGELOG](CHANGELOG.md); they predate the v3.7.0 alignment correction.
216
236
 
217
237
  ## Project Structure
218
238
 
@@ -0,0 +1,45 @@
1
+ {
2
+ "baseline_commit": "1c3953ec4b6ed2e7fa2a2ce56eab3bad648d98d4",
3
+ "kernel_sha256": "dc39818c9518f8686b0cfd5711739ad28d14ac6de3921675767b91e3f5433071",
4
+ "platform": "Linux-5.4.241-1-tlinux4-0017.14-x86_64-with-glibc2.35",
5
+ "architecture": "x86_64",
6
+ "python": "3.10.20",
7
+ "versions": {
8
+ "numpy": "2.2.6",
9
+ "scipy": "1.15.3",
10
+ "numba": "0.65.1",
11
+ "llvmlite": "0.47.0",
12
+ "pyfftw": "0.15.0",
13
+ "ai-edge-litert": "2.2.0"
14
+ },
15
+ "threads": 4,
16
+ "threading_layer": "workqueue",
17
+ "fft_backend": "fftw",
18
+ "repeats": 5,
19
+ "case": "official guitar48_stereo_64kbps_aac",
20
+ "duration_sec": 12.453833333333334,
21
+ "protocol": "audio, FP64, search_window=60, both alignments enabled; warmed; excludes audio I/O",
22
+ "seconds": {
23
+ "optimized": [
24
+ 0.5394072169438004,
25
+ 0.5373025466687977,
26
+ 0.5457052960991859,
27
+ 0.5398548180237412,
28
+ 0.5398879866115749
29
+ ],
30
+ "legacy_3_7_0": [
31
+ 1.4971719882450998,
32
+ 1.5210356987081468,
33
+ 1.5065979771316051,
34
+ 1.5096658081747591,
35
+ 1.5108016063459218
36
+ ]
37
+ },
38
+ "median_sec": {
39
+ "optimized": 0.5398548180237412,
40
+ "legacy_3_7_0": 1.5096658081747591
41
+ },
42
+ "speedup": 2.7964292579646264,
43
+ "moslqo": 4.349722308064309,
44
+ "all_outputs_exact": true
45
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "baseline_commit": "1c3953ec4b6ed2e7fa2a2ce56eab3bad648d98d4",
3
+ "kernel_sha256": "dc39818c9518f8686b0cfd5711739ad28d14ac6de3921675767b91e3f5433071",
4
+ "platform": "macOS-15.7.3-arm64-arm-64bit-Mach-O",
5
+ "architecture": "arm64",
6
+ "python": "3.13.2",
7
+ "versions": {
8
+ "numpy": "2.4.3",
9
+ "scipy": "1.17.1",
10
+ "numba": "0.65.1",
11
+ "llvmlite": "0.47.0",
12
+ "pyfftw": "0.15.0",
13
+ "ai-edge-litert": "2.2.0"
14
+ },
15
+ "threads": 4,
16
+ "threading_layer": "workqueue",
17
+ "fft_backend": "fftw",
18
+ "repeats": 5,
19
+ "case": "official guitar48_stereo_64kbps_aac",
20
+ "duration_sec": 12.453833333333334,
21
+ "protocol": "audio, FP64, search_window=60, both alignments enabled; warmed; excludes audio I/O",
22
+ "seconds": {
23
+ "optimized": [
24
+ 0.40294154200091725,
25
+ 0.4215942089977034,
26
+ 0.40989537499990547,
27
+ 0.4250234579994867,
28
+ 0.42086304199983715
29
+ ],
30
+ "legacy_3_7_0": [
31
+ 1.0646232920007606,
32
+ 1.0958797089988366,
33
+ 1.092644124997605,
34
+ 1.0716607499998645,
35
+ 1.0800574999993842
36
+ ]
37
+ },
38
+ "median_sec": {
39
+ "optimized": 0.42086304199983715,
40
+ "legacy_3_7_0": 1.0800574999993842
41
+ },
42
+ "speedup": 2.566292100316573,
43
+ "moslqo": 4.349722308064313,
44
+ "all_outputs_exact": true
45
+ }