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.
- visqol_python-3.4.0/CHANGELOG.md +100 -0
- visqol_python-3.4.0/CONTRIBUTING.md +77 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/MANIFEST.in +2 -1
- {visqol_python-3.3.5/visqol_python.egg-info → visqol_python-3.4.0}/PKG-INFO +35 -1
- {visqol_python-3.3.5 → visqol_python-3.4.0}/README.md +27 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/pyproject.toml +48 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/tests/test_conformance.py +44 -23
- visqol_python-3.4.0/tests/test_parallel_correctness.py +96 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/tests/test_quick.py +98 -5
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/__init__.py +11 -4
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/__main__.py +49 -28
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/alignment.py +7 -11
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/analysis_window.py +3 -6
- visqol_python-3.4.0/visqol/api.py +346 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/audio_utils.py +16 -11
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/gammatone.py +176 -94
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/nsim.py +26 -17
- visqol_python-3.4.0/visqol/numba_accel.py +619 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/patch_creator.py +25 -28
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/patch_selector.py +224 -61
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/quality_mapper.py +13 -14
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/signal_utils.py +3 -3
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/visqol_core.py +48 -31
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/visqol_manager.py +16 -14
- {visqol_python-3.3.5 → visqol_python-3.4.0/visqol_python.egg-info}/PKG-INFO +35 -1
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/SOURCES.txt +4 -1
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/requires.txt +9 -0
- visqol_python-3.3.5/requirements.txt +0 -4
- visqol_python-3.3.5/visqol/api.py +0 -181
- {visqol_python-3.3.5 → visqol_python-3.4.0}/LICENSE +0 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/setup.cfg +0 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol/py.typed +0 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/dependency_links.txt +0 -0
- {visqol_python-3.3.5 → visqol_python-3.4.0}/visqol_python.egg-info/entry_points.txt +0 -0
- {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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: visqol-python
|
|
3
|
-
Version: 3.
|
|
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
|
-
|
|
68
|
-
("
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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")
|