visqol-python 3.3.5__tar.gz → 3.4.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 (36) hide show
  1. visqol_python-3.4.0/CHANGELOG.md +100 -0
  2. visqol_python-3.4.0/CONTRIBUTING.md +77 -0
  3. {visqol_python-3.3.5 → visqol_python-3.4.0}/MANIFEST.in +2 -1
  4. {visqol_python-3.3.5/visqol_python.egg-info → visqol_python-3.4.0}/PKG-INFO +35 -1
  5. {visqol_python-3.3.5 → visqol_python-3.4.0}/README.md +27 -0
  6. {visqol_python-3.3.5 → visqol_python-3.4.0}/pyproject.toml +48 -0
  7. {visqol_python-3.3.5 → visqol_python-3.4.0}/tests/test_conformance.py +44 -23
  8. visqol_python-3.4.0/tests/test_parallel_correctness.py +96 -0
  9. {visqol_python-3.3.5 → visqol_python-3.4.0}/tests/test_quick.py +98 -5
  10. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/__init__.py +11 -4
  11. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/__main__.py +49 -28
  12. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/alignment.py +7 -11
  13. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/analysis_window.py +3 -6
  14. visqol_python-3.4.0/visqol/api.py +346 -0
  15. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/audio_utils.py +16 -11
  16. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/gammatone.py +176 -94
  17. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/nsim.py +26 -17
  18. visqol_python-3.4.0/visqol/numba_accel.py +619 -0
  19. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/patch_creator.py +25 -28
  20. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/patch_selector.py +224 -61
  21. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/quality_mapper.py +13 -14
  22. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/signal_utils.py +3 -3
  23. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/visqol_core.py +48 -31
  24. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/visqol_manager.py +16 -14
  25. {visqol_python-3.3.5 → visqol_python-3.4.0/visqol_python.egg-info}/PKG-INFO +35 -1
  26. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/SOURCES.txt +4 -1
  27. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/requires.txt +9 -0
  28. visqol_python-3.3.5/requirements.txt +0 -4
  29. visqol_python-3.3.5/visqol/api.py +0 -181
  30. {visqol_python-3.3.5 → visqol_python-3.4.0}/LICENSE +0 -0
  31. {visqol_python-3.3.5 → visqol_python-3.4.0}/setup.cfg +0 -0
  32. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  33. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/py.typed +0 -0
  34. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/dependency_links.txt +0 -0
  35. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/entry_points.txt +0 -0
  36. {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/top_level.txt +0 -0
@@ -0,0 +1,100 @@
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.4.0] - 2026-03-23
8
+
9
+ ### Added
10
+ - **Numba JIT acceleration** (`pip install visqol-python[accel]`):
11
+ - DP patch matching inner loops compiled to machine code via `@njit`
12
+ - Gammatone IIR filterbank compiled with `parallel=True` + `fastmath=True` — frames processed in parallel across all CPU cores
13
+ - NSIM similarity kernel JIT-compiled
14
+ - Automatic `NUMBA_THREADING_LAYER=workqueue` setup for macOS compatibility
15
+ - Zero-loss parallel accuracy (each frame's IIR state is independent)
16
+ - **Batch evaluation API**: `VisqolApi.measure_batch()` with optional `parallel=True` and `max_workers` for multi-process execution
17
+ - Exported `PatchSimilarityResult` and `ProgressCallback` from top-level package
18
+
19
+ ### Performance
20
+ - **12x Gammatone speedup** via parallel + fastmath (1.53s → 0.13s per signal pair)
21
+ - **8.7x DP patch matching speedup** via Numba JIT (3.5s → 0.40s)
22
+ - **Overall 9x speedup**: RTF 0.58 → 0.064 (surpasses C++ estimate of 0.093)
23
+ - Fine alignment skip optimization: 29x speedup when lag == 0
24
+
25
+ ### Improved
26
+ - `__repr__` / `__str__` for `SimilarityResult`, `AudioSignal`, `PatchSimilarityResult`, `Spectrogram`
27
+ - Logging replaces print statements in CLI verbose output
28
+ - Development tooling: ruff lint/format + mypy strict type checking in CI
29
+
30
+ ### Fixed
31
+ - **CI failures**: resolved all ruff lint (308 errors), ruff format (24 files), and mypy (24 errors) issues
32
+ - Added `per-file-ignores` for benchmark test scripts (E402, E702)
33
+ - Added mypy override for `numba_accel.py` (untyped `@njit` decorators)
34
+ - Fixed `no-any-return` errors across `audio_utils.py`, `gammatone.py`, `visqol_core.py`, `api.py`
35
+ - Added `TYPE_CHECKING` imports for `ImagePatchCreator` / `VadPatchCreator` in `visqol_core.py`
36
+
37
+ ## [3.3.6] - 2026-03-23
38
+
39
+ ### Added
40
+ - **Batch evaluation API**: `VisqolApi.measure_batch()` with `progress_callback` support
41
+ - **Numba optional acceleration**: `visqol/numba_accel.py` with JIT-compiled DP forward pass and NSIM kernel
42
+ - `[accel]` optional dependency group: `pip install visqol-python[accel]`
43
+
44
+ ### Improved
45
+ - `GammatoneFilterBank.apply_filter()` pre-builds coefficient arrays (avoids per-channel allocation)
46
+ - `prepare_spectrograms_for_comparison()` vectorized per-frame noise floor
47
+ - Ruff lint/format configuration added to `pyproject.toml`
48
+ - CI enhanced with lint and type-check jobs
49
+ - Development dependencies: `[project.optional-dependencies] dev`
50
+
51
+ ## [3.3.5] - 2026-03-23
52
+
53
+ ### Added
54
+ - **Type hints** on all public and internal APIs (`from __future__ import annotations`)
55
+ - **`py.typed`** marker (PEP 561) — mypy / pyright can now type-check dependents
56
+ - **CONTRIBUTING.md** with development setup, code style, and PR guidelines
57
+ - Exported `SimilarityResult` and `AudioSignal` from top-level `visqol` package
58
+ - `mypy` configuration in `pyproject.toml`
59
+
60
+ ### Improved
61
+ - **Error handling**: friendly `ValueError` / `FileNotFoundError` / `TypeError` throughout:
62
+ - `VisqolApi.create()` now validates mode, search_window, and model_path
63
+ - `VisqolApi.measure()` checks file existence before processing
64
+ - `VisqolApi.measure_from_arrays()` validates array types, emptiness, and sample rate
65
+ - `AudioSignal` validates sample rate on construction
66
+ - `AnalysisWindow` validates sample_rate and overlap range
67
+ - CLI now catches exceptions and prints user-friendly error messages
68
+ - `AnalysisWindow.apply_hann_window()` uses `ValueError` instead of bare `assert`
69
+
70
+ ## [3.3.4] - 2026-03-23
71
+
72
+ ### Improved
73
+ - Tests rewritten in **pytest** format with `parametrize` and fixtures
74
+ - Added **CI workflow** (GitHub Actions): auto-test on Python 3.9–3.13 for every push/PR
75
+ - Added **smoke tests** (`test_quick.py`) that run without external testdata
76
+ - Version number now managed in a single place (`visqol/__init__.py`)
77
+ - Removed redundant `setup.py` — `pyproject.toml` is the single source of truth
78
+ - Added this CHANGELOG
79
+ - README: added PyPI / CI / License badges
80
+
81
+ ### Fixed
82
+ - `requires-python` updated from `>=3.8` to `>=3.9` (numpy/scipy dropped 3.8 support)
83
+
84
+ ## [3.3.3] - 2026-03-23
85
+
86
+ ### Added
87
+ - Initial PyPI release as `visqol-python`
88
+ - Pure Python port of [Google's ViSQOL v3.3.3](https://github.com/google/visqol)
89
+ - **Audio mode** (48 kHz, SVR quality mapping) — 10/10 conformance tests pass
90
+ - **Speech mode** (16 kHz, exponential polynomial mapping) — 1/1 conformance test passes
91
+ - Python API: `VisqolApi.measure()` and `VisqolApi.measure_from_arrays()`
92
+ - CLI: `python -m visqol` / `visqol` command
93
+ - Bundled SVR model (`libsvm_nu_svr_model.txt`)
94
+ - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
95
+
96
+ [3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
97
+ [3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
98
+ [3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
99
+ [3.3.4]: https://github.com/talker93/visqol-python/compare/v3.3.3...v3.3.4
100
+ [3.3.3]: https://github.com/talker93/visqol-python/releases/tag/v3.3.3
@@ -0,0 +1,77 @@
1
+ # Contributing to ViSQOL (Python)
2
+
3
+ Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to this project.
4
+
5
+ ## Getting Started
6
+
7
+ 1. **Fork** the repository on GitHub
8
+ 2. **Clone** your fork locally:
9
+ ```bash
10
+ git clone https://github.com/<your-username>/visqol-python.git
11
+ cd visqol-python
12
+ ```
13
+ 3. **Create a branch** for your changes:
14
+ ```bash
15
+ git checkout -b feature/my-improvement
16
+ ```
17
+
18
+ ## Development Setup
19
+
20
+ ```bash
21
+ # Create a virtual environment
22
+ python -m venv venv
23
+ source venv/bin/activate # macOS/Linux
24
+ # venv\Scripts\activate # Windows
25
+
26
+ # Install in development mode with test dependencies
27
+ pip install -e ".[test]"
28
+ ```
29
+
30
+ ## Running Tests
31
+
32
+ ```bash
33
+ # Quick smoke tests (no external data needed)
34
+ pytest tests/test_quick.py -v
35
+
36
+ # Full conformance tests (requires testdata directory)
37
+ pytest tests/test_conformance.py -v --testdata /path/to/visqol/testdata
38
+ ```
39
+
40
+ ## Code Style
41
+
42
+ - **Type hints**: All public functions and methods must include type annotations.
43
+ - **Docstrings**: Use Google-style docstrings for all public APIs.
44
+ - **Imports**: Use `from __future__ import annotations` at the top of every module.
45
+ - Keep line length ≤ 99 characters where practical.
46
+
47
+ ## Making Changes
48
+
49
+ 1. Write clean, well-documented code with type hints.
50
+ 2. Add or update tests for any new functionality.
51
+ 3. Ensure all existing tests still pass.
52
+ 4. Update `CHANGELOG.md` under an `[Unreleased]` section.
53
+
54
+ ## Pull Request Process
55
+
56
+ 1. Update the `CHANGELOG.md` with details of your changes.
57
+ 2. Ensure all tests pass locally.
58
+ 3. Submit a pull request with a clear description of the changes.
59
+ 4. Link any relevant issues.
60
+
61
+ ## Reporting Bugs
62
+
63
+ Please open an [issue](https://github.com/talker93/visqol-python/issues) with:
64
+
65
+ - A clear, descriptive title
66
+ - Steps to reproduce the problem
67
+ - Expected vs. actual behavior
68
+ - Python version and OS
69
+ - Relevant audio file details (sample rate, duration, format)
70
+
71
+ ## Versioning
72
+
73
+ This project follows [Semantic Versioning](https://semver.org/). The single source of truth for the version number is `visqol/__init__.py`.
74
+
75
+ ## License
76
+
77
+ By contributing, you agree that your contributions will be licensed under the [Apache License 2.0](LICENSE).
@@ -1,4 +1,5 @@
1
1
  include README.md
2
2
  include LICENSE
3
- include requirements.txt
3
+ include CHANGELOG.md
4
+ include CONTRIBUTING.md
4
5
  recursive-include visqol/model *.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.3.5
3
+ Version: 3.4.0
4
4
  Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
5
5
  Author: Shan Jiang
6
6
  License-Expression: Apache-2.0
@@ -28,8 +28,15 @@ Requires-Dist: numpy>=1.20
28
28
  Requires-Dist: scipy>=1.7
29
29
  Requires-Dist: soundfile>=0.10
30
30
  Requires-Dist: libsvm-official>=3.25
31
+ Provides-Extra: accel
32
+ Requires-Dist: numba>=0.57; extra == "accel"
31
33
  Provides-Extra: test
32
34
  Requires-Dist: pytest>=7.0; extra == "test"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0; extra == "dev"
37
+ Requires-Dist: ruff>=0.4; extra == "dev"
38
+ Requires-Dist: mypy>=1.8; extra == "dev"
39
+ Requires-Dist: numba>=0.57; extra == "dev"
33
40
  Dynamic: license-file
34
41
 
35
42
  # ViSQOL (Python)
@@ -103,6 +110,33 @@ result = api.measure_from_arrays(ref, deg, sample_rate=sr)
103
110
  print(f"MOS-LQO: {result.moslqo:.4f}")
104
111
  ```
105
112
 
113
+ ### Batch Evaluation
114
+
115
+ ```python
116
+ from visqol import VisqolApi
117
+
118
+ api = VisqolApi()
119
+ api.create(mode="audio")
120
+
121
+ file_pairs = [
122
+ ("ref1.wav", "deg1.wav"),
123
+ ("ref2.wav", "deg2.wav"),
124
+ ("ref3.wav", "deg3.wav"),
125
+ ]
126
+
127
+ # Optional progress callback
128
+ results = api.measure_batch(
129
+ file_pairs,
130
+ progress_callback=lambda done, total: print(f"{done}/{total}"),
131
+ )
132
+
133
+ for pair, result in zip(file_pairs, results):
134
+ if isinstance(result, Exception):
135
+ print(f"{pair}: FAILED — {result}")
136
+ else:
137
+ print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
138
+ ```
139
+
106
140
  ### Command Line
107
141
 
108
142
  ```bash
@@ -69,6 +69,33 @@ result = api.measure_from_arrays(ref, deg, sample_rate=sr)
69
69
  print(f"MOS-LQO: {result.moslqo:.4f}")
70
70
  ```
71
71
 
72
+ ### Batch Evaluation
73
+
74
+ ```python
75
+ from visqol import VisqolApi
76
+
77
+ api = VisqolApi()
78
+ api.create(mode="audio")
79
+
80
+ file_pairs = [
81
+ ("ref1.wav", "deg1.wav"),
82
+ ("ref2.wav", "deg2.wav"),
83
+ ("ref3.wav", "deg3.wav"),
84
+ ]
85
+
86
+ # Optional progress callback
87
+ results = api.measure_batch(
88
+ file_pairs,
89
+ progress_callback=lambda done, total: print(f"{done}/{total}"),
90
+ )
91
+
92
+ for pair, result in zip(file_pairs, results):
93
+ if isinstance(result, Exception):
94
+ print(f"{pair}: FAILED — {result}")
95
+ else:
96
+ print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
97
+ ```
98
+
72
99
  ### Command Line
73
100
 
74
101
  ```bash
@@ -37,7 +37,14 @@ dependencies = [
37
37
  ]
38
38
 
39
39
  [project.optional-dependencies]
40
+ accel = ["numba>=0.57"]
40
41
  test = ["pytest>=7.0"]
42
+ dev = [
43
+ "pytest>=7.0",
44
+ "ruff>=0.4",
45
+ "mypy>=1.8",
46
+ "numba>=0.57",
47
+ ]
41
48
 
42
49
  [project.urls]
43
50
  Homepage = "https://github.com/talker93/visqol-python"
@@ -61,7 +68,48 @@ visqol = ["model/*.txt", "py.typed"]
61
68
  [tool.pytest.ini_options]
62
69
  testpaths = ["tests"]
63
70
 
71
+ [tool.ruff]
72
+ target-version = "py39"
73
+ line-length = 95
74
+
75
+ [tool.ruff.lint]
76
+ select = [
77
+ "E", # pycodestyle errors
78
+ "W", # pycodestyle warnings
79
+ "F", # pyflakes
80
+ "I", # isort
81
+ "UP", # pyupgrade
82
+ "B", # flake8-bugbear
83
+ "SIM", # flake8-simplify
84
+ "RUF", # ruff-specific rules
85
+ ]
86
+ ignore = [
87
+ "E501", # line length handled by formatter
88
+ "RUF001", # ambiguous unicode in string (intentional mathematical symbols)
89
+ "RUF002", # ambiguous unicode in docstring (intentional: ×, −)
90
+ "RUF003", # ambiguous unicode in comment (intentional: ×, →)
91
+ ]
92
+
93
+ [tool.ruff.lint.per-file-ignores]
94
+ "tests/bench_*.py" = ["E402", "E702"] # bench scripts use sys.path manipulation
95
+ "tests/test_parallel_correctness.py" = ["E402"]
96
+
97
+ [tool.ruff.lint.isort]
98
+ known-first-party = ["visqol"]
99
+
64
100
  [tool.mypy]
65
101
  strict = true
66
102
  warn_return_any = true
67
103
  warn_unused_configs = true
104
+
105
+ [[tool.mypy.overrides]]
106
+ module = ["svmutil.*", "libsvm.*", "soundfile.*", "numba.*"]
107
+ ignore_missing_imports = true
108
+
109
+ [[tool.mypy.overrides]]
110
+ module = ["visqol.numba_accel"]
111
+ # numba @njit decorators are untyped; strict mode is not feasible here
112
+ strict = false
113
+ disallow_untyped_defs = false
114
+ disallow_untyped_decorators = false
115
+ warn_unused_ignores = false
@@ -18,9 +18,9 @@ import pytest
18
18
 
19
19
  from visqol import VisqolApi
20
20
 
21
-
22
21
  # ── Fixtures ──
23
22
 
23
+
24
24
  @pytest.fixture(scope="session")
25
25
  def testdata_dir(request):
26
26
  """Resolve testdata directory from --testdata or auto-detect."""
@@ -63,36 +63,56 @@ def speech_api():
63
63
  TOLERANCE = 0.05
64
64
 
65
65
  AUDIO_CASES = [
66
- ("strauss48_stereo.wav", "strauss48_stereo_lp35.wav",
67
- 1.3888791489130758, "strauss_lp35"),
68
- ("steely48_stereo.wav", "steely48_stereo_lp7.wav",
69
- 2.2501683734385183, "steely_lp7"),
70
- ("sopr48_stereo.wav", "sopr48_stereo_256kbps_aac.wav",
71
- 4.68228969737946, "sopr_256aac"),
72
- ("ravel48_stereo.wav", "ravel48_stereo_128kbps_opus.wav",
73
- 4.465141897255348, "ravel_128opus"),
74
- ("moonlight48_stereo.wav", "moonlight48_stereo_128kbps_aac.wav",
75
- 4.684292801646114, "moonlight_128aac"),
76
- ("harpsichord48_stereo.wav", "harpsichord48_stereo_96kbps_mp3.wav",
77
- 4.22374532766003, "harpsichord_96mp3"),
78
- ("guitar48_stereo.wav", "guitar48_stereo_64kbps_aac.wav",
79
- 4.349722308064298, "guitar_64aac"),
80
- ("glock48_stereo.wav", "glock48_stereo_48kbps_aac.wav",
81
- 4.332452943882108, "glock_48aac"),
82
- ("contrabassoon48_stereo.wav", "contrabassoon48_stereo_24kbps_aac.wav",
83
- 2.346868205375293, "contrabassoon_24aac"),
84
- ("castanets48_stereo.wav", "castanets48_stereo.wav",
85
- 4.732101253042348, "castanets_identity"),
66
+ ("strauss48_stereo.wav", "strauss48_stereo_lp35.wav", 1.3888791489130758, "strauss_lp35"),
67
+ ("steely48_stereo.wav", "steely48_stereo_lp7.wav", 2.2501683734385183, "steely_lp7"),
68
+ ("sopr48_stereo.wav", "sopr48_stereo_256kbps_aac.wav", 4.68228969737946, "sopr_256aac"),
69
+ (
70
+ "ravel48_stereo.wav",
71
+ "ravel48_stereo_128kbps_opus.wav",
72
+ 4.465141897255348,
73
+ "ravel_128opus",
74
+ ),
75
+ (
76
+ "moonlight48_stereo.wav",
77
+ "moonlight48_stereo_128kbps_aac.wav",
78
+ 4.684292801646114,
79
+ "moonlight_128aac",
80
+ ),
81
+ (
82
+ "harpsichord48_stereo.wav",
83
+ "harpsichord48_stereo_96kbps_mp3.wav",
84
+ 4.22374532766003,
85
+ "harpsichord_96mp3",
86
+ ),
87
+ (
88
+ "guitar48_stereo.wav",
89
+ "guitar48_stereo_64kbps_aac.wav",
90
+ 4.349722308064298,
91
+ "guitar_64aac",
92
+ ),
93
+ ("glock48_stereo.wav", "glock48_stereo_48kbps_aac.wav", 4.332452943882108, "glock_48aac"),
94
+ (
95
+ "contrabassoon48_stereo.wav",
96
+ "contrabassoon48_stereo_24kbps_aac.wav",
97
+ 2.346868205375293,
98
+ "contrabassoon_24aac",
99
+ ),
100
+ (
101
+ "castanets48_stereo.wav",
102
+ "castanets48_stereo.wav",
103
+ 4.732101253042348,
104
+ "castanets_identity",
105
+ ),
86
106
  ]
87
107
 
88
108
  SPEECH_CASES = [
89
- ("CA01_01.wav", "transcoded_CA01_01.wav",
90
- 3.374505555111911, "CA01_transcoded"),
109
+ ("CA01_01.wav", "transcoded_CA01_01.wav", 3.374505555111911, "CA01_transcoded"),
91
110
  ]
92
111
 
93
112
 
94
113
  # ── Audio mode tests ──
95
114
 
115
+
96
116
  @pytest.mark.parametrize(
97
117
  "ref_name, deg_name, expected_mos, test_id",
98
118
  AUDIO_CASES,
@@ -110,6 +130,7 @@ def test_audio_conformance(audio_api, conf_dir, ref_name, deg_name, expected_mos
110
130
 
111
131
  # ── Speech mode tests ──
112
132
 
133
+
113
134
  @pytest.mark.parametrize(
114
135
  "ref_name, deg_name, expected_mos, test_id",
115
136
  SPEECH_CASES,
@@ -0,0 +1,96 @@
1
+ """
2
+ Correctness test: verify that the parallel+fastmath Gammatone spectrogram
3
+ produces results consistent with the original serial version.
4
+
5
+ Approach: run the full ViSQOL pipeline on a known test file and check
6
+ that the MOS score is within expected tolerance.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import time
12
+
13
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
14
+ VISQOL_ROOT = os.path.dirname(ROOT)
15
+ sys.path.insert(0, ROOT)
16
+
17
+
18
+ # --- Quick sanity: does the module load without errors? ---
19
+ print("=" * 60)
20
+ print("1) Import & warmup")
21
+ print("=" * 60)
22
+ from visqol import numba_accel
23
+
24
+ print(f" _HAS_NUMBA = {numba_accel._HAS_NUMBA}")
25
+ print(f" _HAS_NUMBA_PARALLEL = {numba_accel._HAS_NUMBA_PARALLEL}")
26
+ print(f" NUMBA_THREADING_LAYER = {os.environ.get('NUMBA_THREADING_LAYER', '(not set)')}")
27
+
28
+ t0 = time.perf_counter()
29
+ numba_accel.warmup()
30
+ t1 = time.perf_counter()
31
+ print(f" warmup completed in {t1 - t0:.2f}s")
32
+
33
+ # --- Full pipeline correctness test ---
34
+ print()
35
+ print("=" * 60)
36
+ print("2) Full pipeline MOS score test")
37
+ print("=" * 60)
38
+
39
+ from visqol.api import VisqolApi
40
+
41
+ TESTDATA = os.path.join(VISQOL_ROOT, "testdata", "conformance_testdata_subset")
42
+ ref_file = os.path.join(TESTDATA, "guitar48_stereo.wav")
43
+ deg_file = os.path.join(TESTDATA, "guitar48_stereo_64kbps_aac.wav")
44
+
45
+ if not os.path.exists(ref_file):
46
+ import glob
47
+
48
+ testdata_root = os.path.join(VISQOL_ROOT, "testdata")
49
+ wavs = sorted(glob.glob(os.path.join(testdata_root, "**", "*.wav"), recursive=True))
50
+ if len(wavs) >= 2:
51
+ ref_file = wavs[0]
52
+ deg_file = wavs[1]
53
+ else:
54
+ print(" ERROR: No test WAV files found")
55
+ sys.exit(1)
56
+
57
+ print(f" ref: {os.path.basename(ref_file)}")
58
+ print(f" deg: {os.path.basename(deg_file)}")
59
+
60
+ v = VisqolApi()
61
+ v.create(mode="audio")
62
+
63
+ # Run 1: get score (also triggers JIT compilation if not warmed up)
64
+ t0 = time.perf_counter()
65
+ result1 = v.measure(ref_file, deg_file)
66
+ t1 = time.perf_counter()
67
+ print(f" Run 1: MOS = {result1.moslqo:.6f} ({t1 - t0:.3f}s)")
68
+
69
+ # Run 2: steady-state (JIT cached)
70
+ t0 = time.perf_counter()
71
+ result2 = v.measure(ref_file, deg_file)
72
+ t1 = time.perf_counter()
73
+ print(f" Run 2: MOS = {result2.moslqo:.6f} ({t1 - t0:.3f}s)")
74
+
75
+ # Check reproducibility
76
+ diff = abs(result1.moslqo - result2.moslqo)
77
+ print(f" Run1 vs Run2 diff: {diff:.2e}")
78
+ assert diff < 1e-10, f"Non-reproducible results: diff={diff}"
79
+ print(" ✅ Results are reproducible across runs")
80
+
81
+ # Check MOS is in a reasonable range
82
+ print(f" MOS value: {result1.moslqo:.6f}")
83
+ assert 1.0 <= result1.moslqo <= 5.0, f"MOS out of range: {result1.moslqo}"
84
+ print(" ✅ MOS in valid range [1, 5]")
85
+
86
+ # Run 3: timing
87
+ t0 = time.perf_counter()
88
+ result3 = v.measure(ref_file, deg_file)
89
+ t1 = time.perf_counter()
90
+ print(f" Run 3: MOS = {result3.moslqo:.6f} ({t1 - t0:.3f}s)")
91
+
92
+ print()
93
+ print("=" * 60)
94
+ print("ALL CORRECTNESS CHECKS PASSED ✅")
95
+ print(f"Steady-state latency: {t1 - t0:.3f}s")
96
+ print("=" * 60)
@@ -7,8 +7,12 @@ These tests verify basic API functionality without requiring external testdata.
7
7
  import numpy as np
8
8
  import pytest
9
9
 
10
- from visqol import VisqolApi, SimilarityResult, AudioSignal
11
-
10
+ from visqol import (
11
+ AudioSignal,
12
+ PatchSimilarityResult,
13
+ SimilarityResult,
14
+ VisqolApi,
15
+ )
12
16
 
13
17
  # ── API creation ──
14
18
 
@@ -120,9 +124,7 @@ class TestMeasureFromArrays:
120
124
  rng = np.random.default_rng(42)
121
125
  deg = ref + 0.3 * rng.standard_normal(len(ref))
122
126
  result = api.measure_from_arrays(ref, deg, sample_rate=sr)
123
- assert 1.0 <= result.moslqo <= 5.0, (
124
- f"MOS should be in [1, 5], got {result.moslqo:.4f}"
125
- )
127
+ assert 1.0 <= result.moslqo <= 5.0, f"MOS should be in [1, 5], got {result.moslqo:.4f}"
126
128
 
127
129
 
128
130
  # ── Result fields ──
@@ -147,6 +149,93 @@ class TestResultFields:
147
149
  assert hasattr(result, "patch_sims")
148
150
 
149
151
 
152
+ # ── __repr__ / __str__ ──
153
+
154
+
155
+ class TestReprStr:
156
+ """Test readable string representations."""
157
+
158
+ def test_audio_signal_repr(self):
159
+ sig = AudioSignal(np.zeros(16000), 16000)
160
+ r = repr(sig)
161
+ assert "AudioSignal" in r
162
+ assert "16000" in r
163
+ assert "1.000" in r
164
+
165
+ def test_audio_signal_str(self):
166
+ sig = AudioSignal(np.zeros(48000), 48000)
167
+ s = str(sig)
168
+ assert "1.000s" in s
169
+ assert "48000" in s
170
+
171
+ def test_similarity_result_str(self):
172
+ res = SimilarityResult(moslqo=4.5, vnsim=0.95)
173
+ s = str(res)
174
+ assert "4.5" in s
175
+ assert "0.95" in s
176
+
177
+ def test_similarity_result_repr(self):
178
+ res = SimilarityResult(moslqo=4.5, vnsim=0.95)
179
+ r = repr(res)
180
+ assert "SimilarityResult" in r
181
+ assert "moslqo" in r
182
+
183
+ def test_patch_similarity_result_str(self):
184
+ p = PatchSimilarityResult(similarity=0.85)
185
+ s = str(p)
186
+ assert "0.85" in s
187
+
188
+ def test_patch_similarity_result_repr(self):
189
+ p = PatchSimilarityResult(similarity=0.85)
190
+ r = repr(p)
191
+ assert "PatchSimilarityResult" in r
192
+
193
+
194
+ # ── measure_batch ──
195
+
196
+
197
+ class TestMeasureBatch:
198
+ """Test batch evaluation API."""
199
+
200
+ def test_batch_before_create_raises(self):
201
+ api = VisqolApi()
202
+ with pytest.raises(RuntimeError, match="create"):
203
+ api.measure_batch([("/a.wav", "/b.wav")])
204
+
205
+ def test_batch_nonexistent_files_returns_exceptions(self):
206
+ api = VisqolApi()
207
+ api.create(mode="speech")
208
+ results = api.measure_batch(
209
+ [
210
+ ("/nonexistent/a.wav", "/nonexistent/b.wav"),
211
+ ("/nonexistent/c.wav", "/nonexistent/d.wav"),
212
+ ]
213
+ )
214
+ assert len(results) == 2
215
+ assert all(isinstance(r, Exception) for r in results)
216
+
217
+ def test_batch_progress_callback(self):
218
+ api = VisqolApi()
219
+ api.create(mode="speech")
220
+ progress_log: list[tuple[int, int]] = []
221
+
222
+ def cb(done: int, total: int) -> None:
223
+ progress_log.append((done, total))
224
+
225
+ results = api.measure_batch(
226
+ [("/nonexistent/a.wav", "/nonexistent/b.wav")],
227
+ progress_callback=cb,
228
+ )
229
+ assert len(results) == 1
230
+ assert progress_log == [(1, 1)]
231
+
232
+ def test_batch_empty(self):
233
+ api = VisqolApi()
234
+ api.create(mode="speech")
235
+ results = api.measure_batch([])
236
+ assert results == []
237
+
238
+
150
239
  # ── Package metadata ──
151
240
 
152
241
 
@@ -155,6 +244,7 @@ class TestVersion:
155
244
 
156
245
  def test_version_string(self):
157
246
  import visqol
247
+
158
248
  assert hasattr(visqol, "__version__")
159
249
  assert isinstance(visqol.__version__, str)
160
250
  parts = visqol.__version__.split(".")
@@ -163,6 +253,9 @@ class TestVersion:
163
253
  def test_public_exports(self):
164
254
  """Package should export key classes."""
165
255
  import visqol
256
+
166
257
  assert hasattr(visqol, "VisqolApi")
167
258
  assert hasattr(visqol, "SimilarityResult")
168
259
  assert hasattr(visqol, "AudioSignal")
260
+ assert hasattr(visqol, "PatchSimilarityResult")
261
+ assert hasattr(visqol, "ProgressCallback")