visqol-python 3.3.4__tar.gz → 3.3.6__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 (38) hide show
  1. visqol_python-3.3.6/CHANGELOG.md +55 -0
  2. visqol_python-3.3.6/CONTRIBUTING.md +77 -0
  3. {visqol_python-3.3.4 → visqol_python-3.3.6}/MANIFEST.in +2 -1
  4. {visqol_python-3.3.4/visqol_python.egg-info → visqol_python-3.3.6}/PKG-INFO +32 -1
  5. {visqol_python-3.3.4 → visqol_python-3.3.6}/README.md +27 -0
  6. {visqol_python-3.3.4 → visqol_python-3.3.6}/pyproject.toml +35 -1
  7. visqol_python-3.3.6/tests/test_quick.py +261 -0
  8. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/__init__.py +12 -3
  9. visqol_python-3.3.6/visqol/__main__.py +115 -0
  10. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/alignment.py +22 -12
  11. visqol_python-3.3.6/visqol/analysis_window.py +76 -0
  12. visqol_python-3.3.6/visqol/api.py +231 -0
  13. visqol_python-3.3.6/visqol/audio_utils.py +138 -0
  14. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/gammatone.py +143 -138
  15. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/nsim.py +58 -44
  16. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/patch_creator.py +95 -63
  17. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/patch_selector.py +101 -72
  18. visqol_python-3.3.6/visqol/py.typed +0 -0
  19. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/quality_mapper.py +56 -45
  20. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/signal_utils.py +26 -18
  21. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/visqol_core.py +84 -52
  22. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/visqol_manager.py +94 -64
  23. {visqol_python-3.3.4 → visqol_python-3.3.6/visqol_python.egg-info}/PKG-INFO +32 -1
  24. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol_python.egg-info/SOURCES.txt +3 -1
  25. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol_python.egg-info/requires.txt +5 -0
  26. visqol_python-3.3.4/requirements.txt +0 -4
  27. visqol_python-3.3.4/tests/test_quick.py +0 -89
  28. visqol_python-3.3.4/visqol/__main__.py +0 -92
  29. visqol_python-3.3.4/visqol/analysis_window.py +0 -52
  30. visqol_python-3.3.4/visqol/api.py +0 -110
  31. visqol_python-3.3.4/visqol/audio_utils.py +0 -90
  32. {visqol_python-3.3.4 → visqol_python-3.3.6}/LICENSE +0 -0
  33. {visqol_python-3.3.4 → visqol_python-3.3.6}/setup.cfg +0 -0
  34. {visqol_python-3.3.4 → visqol_python-3.3.6}/tests/test_conformance.py +0 -0
  35. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  36. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol_python.egg-info/dependency_links.txt +0 -0
  37. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol_python.egg-info/entry_points.txt +0 -0
  38. {visqol_python-3.3.4 → visqol_python-3.3.6}/visqol_python.egg-info/top_level.txt +0 -0
@@ -0,0 +1,55 @@
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.3.5] - 2026-03-23
8
+
9
+ ### Added
10
+ - **Type hints** on all public and internal APIs (`from __future__ import annotations`)
11
+ - **`py.typed`** marker (PEP 561) — mypy / pyright can now type-check dependents
12
+ - **CONTRIBUTING.md** with development setup, code style, and PR guidelines
13
+ - Exported `SimilarityResult` and `AudioSignal` from top-level `visqol` package
14
+ - `mypy` configuration in `pyproject.toml`
15
+
16
+ ### Improved
17
+ - **Error handling**: friendly `ValueError` / `FileNotFoundError` / `TypeError` throughout:
18
+ - `VisqolApi.create()` now validates mode, search_window, and model_path
19
+ - `VisqolApi.measure()` checks file existence before processing
20
+ - `VisqolApi.measure_from_arrays()` validates array types, emptiness, and sample rate
21
+ - `AudioSignal` validates sample rate on construction
22
+ - `AnalysisWindow` validates sample_rate and overlap range
23
+ - CLI now catches exceptions and prints user-friendly error messages
24
+ - `AnalysisWindow.apply_hann_window()` uses `ValueError` instead of bare `assert`
25
+
26
+ ## [3.3.4] - 2026-03-23
27
+
28
+ ### Improved
29
+ - Tests rewritten in **pytest** format with `parametrize` and fixtures
30
+ - Added **CI workflow** (GitHub Actions): auto-test on Python 3.9–3.13 for every push/PR
31
+ - Added **smoke tests** (`test_quick.py`) that run without external testdata
32
+ - Version number now managed in a single place (`visqol/__init__.py`)
33
+ - Removed redundant `setup.py` — `pyproject.toml` is the single source of truth
34
+ - Added this CHANGELOG
35
+ - README: added PyPI / CI / License badges
36
+
37
+ ### Fixed
38
+ - `requires-python` updated from `>=3.8` to `>=3.9` (numpy/scipy dropped 3.8 support)
39
+
40
+ ## [3.3.3] - 2026-03-23
41
+
42
+ ### Added
43
+ - Initial PyPI release as `visqol-python`
44
+ - Pure Python port of [Google's ViSQOL v3.3.3](https://github.com/google/visqol)
45
+ - **Audio mode** (48 kHz, SVR quality mapping) — 10/10 conformance tests pass
46
+ - **Speech mode** (16 kHz, exponential polynomial mapping) — 1/1 conformance test passes
47
+ - Python API: `VisqolApi.measure()` and `VisqolApi.measure_from_arrays()`
48
+ - CLI: `python -m visqol` / `visqol` command
49
+ - Bundled SVR model (`libsvm_nu_svr_model.txt`)
50
+ - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
51
+
52
+ [3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
53
+ [3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
54
+ [3.3.4]: https://github.com/talker93/visqol-python/compare/v3.3.3...v3.3.4
55
+ [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.4
3
+ Version: 3.3.6
4
4
  Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
5
5
  Author: Shan Jiang
6
6
  License-Expression: Apache-2.0
@@ -30,6 +30,10 @@ Requires-Dist: soundfile>=0.10
30
30
  Requires-Dist: libsvm-official>=3.25
31
31
  Provides-Extra: test
32
32
  Requires-Dist: pytest>=7.0; extra == "test"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=7.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.4; extra == "dev"
36
+ Requires-Dist: mypy>=1.8; extra == "dev"
33
37
  Dynamic: license-file
34
38
 
35
39
  # ViSQOL (Python)
@@ -103,6 +107,33 @@ result = api.measure_from_arrays(ref, deg, sample_rate=sr)
103
107
  print(f"MOS-LQO: {result.moslqo:.4f}")
104
108
  ```
105
109
 
110
+ ### Batch Evaluation
111
+
112
+ ```python
113
+ from visqol import VisqolApi
114
+
115
+ api = VisqolApi()
116
+ api.create(mode="audio")
117
+
118
+ file_pairs = [
119
+ ("ref1.wav", "deg1.wav"),
120
+ ("ref2.wav", "deg2.wav"),
121
+ ("ref3.wav", "deg3.wav"),
122
+ ]
123
+
124
+ # Optional progress callback
125
+ results = api.measure_batch(
126
+ file_pairs,
127
+ progress_callback=lambda done, total: print(f"{done}/{total}"),
128
+ )
129
+
130
+ for pair, result in zip(file_pairs, results):
131
+ if isinstance(result, Exception):
132
+ print(f"{pair}: FAILED — {result}")
133
+ else:
134
+ print(f"{pair}: MOS-LQO = {result.moslqo:.4f}")
135
+ ```
136
+
106
137
  ### Command Line
107
138
 
108
139
  ```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
@@ -38,6 +38,11 @@ dependencies = [
38
38
 
39
39
  [project.optional-dependencies]
40
40
  test = ["pytest>=7.0"]
41
+ dev = [
42
+ "pytest>=7.0",
43
+ "ruff>=0.4",
44
+ "mypy>=1.8",
45
+ ]
41
46
 
42
47
  [project.urls]
43
48
  Homepage = "https://github.com/talker93/visqol-python"
@@ -56,7 +61,36 @@ version = {attr = "visqol.__version__"}
56
61
  exclude = ["tests*"]
57
62
 
58
63
  [tool.setuptools.package-data]
59
- visqol = ["model/*.txt"]
64
+ visqol = ["model/*.txt", "py.typed"]
60
65
 
61
66
  [tool.pytest.ini_options]
62
67
  testpaths = ["tests"]
68
+
69
+ [tool.ruff]
70
+ target-version = "py39"
71
+ line-length = 95
72
+
73
+ [tool.ruff.lint]
74
+ select = [
75
+ "E", # pycodestyle errors
76
+ "W", # pycodestyle warnings
77
+ "F", # pyflakes
78
+ "I", # isort
79
+ "UP", # pyupgrade
80
+ "B", # flake8-bugbear
81
+ "SIM", # flake8-simplify
82
+ "RUF", # ruff-specific rules
83
+ ]
84
+ ignore = ["E501"] # line length handled by formatter
85
+
86
+ [tool.ruff.lint.isort]
87
+ known-first-party = ["visqol"]
88
+
89
+ [tool.mypy]
90
+ strict = true
91
+ warn_return_any = true
92
+ warn_unused_configs = true
93
+
94
+ [[tool.mypy.overrides]]
95
+ module = ["svmutil.*", "libsvm.*", "soundfile.*"]
96
+ ignore_missing_imports = true
@@ -0,0 +1,261 @@
1
+ """
2
+ Quick smoke tests for ViSQOL Python.
3
+
4
+ These tests verify basic API functionality without requiring external testdata.
5
+ """
6
+
7
+ import numpy as np
8
+ import pytest
9
+
10
+ from visqol import (
11
+ AudioSignal,
12
+ PatchSimilarityResult,
13
+ ProgressCallback,
14
+ SimilarityResult,
15
+ VisqolApi,
16
+ )
17
+
18
+
19
+ # ── API creation ──
20
+
21
+
22
+ class TestApiCreation:
23
+ """Test that VisqolApi can be created in different modes."""
24
+
25
+ def test_create_audio_mode(self):
26
+ api = VisqolApi()
27
+ api.create(mode="audio")
28
+
29
+ def test_create_speech_mode(self):
30
+ api = VisqolApi()
31
+ api.create(mode="speech")
32
+
33
+ def test_create_default_mode(self):
34
+ """Default mode (no argument) should work as audio mode."""
35
+ api = VisqolApi()
36
+ api.create()
37
+
38
+ def test_create_case_insensitive(self):
39
+ api = VisqolApi()
40
+ api.create(mode="SPEECH")
41
+
42
+ def test_create_invalid_mode_raises(self):
43
+ api = VisqolApi()
44
+ with pytest.raises(ValueError, match="Invalid mode"):
45
+ api.create(mode="invalid")
46
+
47
+ def test_create_negative_search_window_raises(self):
48
+ api = VisqolApi()
49
+ with pytest.raises(ValueError, match="search_window"):
50
+ api.create(search_window=-1)
51
+
52
+ def test_create_missing_model_raises(self):
53
+ api = VisqolApi()
54
+ with pytest.raises(FileNotFoundError):
55
+ api.create(mode="audio", model_path="/nonexistent/model.txt")
56
+
57
+
58
+ # ── Measure guards ──
59
+
60
+
61
+ class TestMeasureGuards:
62
+ """Test that measure() raises helpful errors for bad inputs."""
63
+
64
+ def test_measure_before_create_raises(self):
65
+ api = VisqolApi()
66
+ with pytest.raises(RuntimeError, match="create"):
67
+ api.measure("a.wav", "b.wav")
68
+
69
+ def test_measure_nonexistent_ref_raises(self):
70
+ api = VisqolApi()
71
+ api.create(mode="speech")
72
+ with pytest.raises(FileNotFoundError, match="Reference"):
73
+ api.measure("/nonexistent/ref.wav", "/nonexistent/deg.wav")
74
+
75
+ def test_measure_from_arrays_before_create_raises(self):
76
+ api = VisqolApi()
77
+ with pytest.raises(RuntimeError, match="create"):
78
+ api.measure_from_arrays(np.zeros(100), np.zeros(100), 16000)
79
+
80
+ def test_measure_from_arrays_bad_type_raises(self):
81
+ api = VisqolApi()
82
+ api.create(mode="speech")
83
+ with pytest.raises(TypeError, match="numpy array"):
84
+ api.measure_from_arrays([1, 2, 3], np.zeros(100), 16000) # type: ignore[arg-type]
85
+
86
+ def test_measure_from_arrays_empty_raises(self):
87
+ api = VisqolApi()
88
+ api.create(mode="speech")
89
+ with pytest.raises(ValueError, match="empty"):
90
+ api.measure_from_arrays(np.array([]), np.zeros(100), 16000)
91
+
92
+ def test_measure_from_arrays_bad_sr_raises(self):
93
+ api = VisqolApi()
94
+ api.create(mode="speech")
95
+ with pytest.raises(ValueError, match="sample_rate"):
96
+ api.measure_from_arrays(np.zeros(100), np.zeros(100), 0)
97
+
98
+
99
+ # ── measure_from_arrays ──
100
+
101
+
102
+ class TestMeasureFromArrays:
103
+ """Test measure_from_arrays with synthetic signals."""
104
+
105
+ def test_identical_signal_high_score(self):
106
+ """Identical signals should produce a high MOS score."""
107
+ api = VisqolApi()
108
+ api.create(mode="speech")
109
+ sr = 16000
110
+ duration = 3.0
111
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
112
+ signal = 0.5 * np.sin(2 * np.pi * 440 * t)
113
+ result = api.measure_from_arrays(signal, signal, sample_rate=sr)
114
+ assert result.moslqo >= 4.0, (
115
+ f"Identical signal should give MOS >= 4.0, got {result.moslqo:.4f}"
116
+ )
117
+
118
+ def test_degraded_signal_lower_score(self):
119
+ """Adding noise to a signal should produce a lower MOS score."""
120
+ api = VisqolApi()
121
+ api.create(mode="speech")
122
+ sr = 16000
123
+ duration = 3.0
124
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
125
+ ref = 0.5 * np.sin(2 * np.pi * 440 * t)
126
+ rng = np.random.default_rng(42)
127
+ deg = ref + 0.3 * rng.standard_normal(len(ref))
128
+ result = api.measure_from_arrays(ref, deg, sample_rate=sr)
129
+ assert 1.0 <= result.moslqo <= 5.0, (
130
+ f"MOS should be in [1, 5], got {result.moslqo:.4f}"
131
+ )
132
+
133
+
134
+ # ── Result fields ──
135
+
136
+
137
+ class TestResultFields:
138
+ """Test that SimilarityResult has all expected fields."""
139
+
140
+ def test_result_has_expected_fields(self):
141
+ api = VisqolApi()
142
+ api.create(mode="speech")
143
+ sr = 16000
144
+ duration = 3.0
145
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
146
+ signal = 0.5 * np.sin(2 * np.pi * 440 * t)
147
+ result = api.measure_from_arrays(signal, signal, sample_rate=sr)
148
+ assert hasattr(result, "moslqo")
149
+ assert hasattr(result, "vnsim")
150
+ assert hasattr(result, "fvnsim")
151
+ assert hasattr(result, "fstdnsim")
152
+ assert hasattr(result, "fvdegenergy")
153
+ assert hasattr(result, "patch_sims")
154
+
155
+
156
+ # ── __repr__ / __str__ ──
157
+
158
+
159
+ class TestReprStr:
160
+ """Test readable string representations."""
161
+
162
+ def test_audio_signal_repr(self):
163
+ sig = AudioSignal(np.zeros(16000), 16000)
164
+ r = repr(sig)
165
+ assert "AudioSignal" in r
166
+ assert "16000" in r
167
+ assert "1.000" in r
168
+
169
+ def test_audio_signal_str(self):
170
+ sig = AudioSignal(np.zeros(48000), 48000)
171
+ s = str(sig)
172
+ assert "1.000s" in s
173
+ assert "48000" in s
174
+
175
+ def test_similarity_result_str(self):
176
+ res = SimilarityResult(moslqo=4.5, vnsim=0.95)
177
+ s = str(res)
178
+ assert "4.5" in s
179
+ assert "0.95" in s
180
+
181
+ def test_similarity_result_repr(self):
182
+ res = SimilarityResult(moslqo=4.5, vnsim=0.95)
183
+ r = repr(res)
184
+ assert "SimilarityResult" in r
185
+ assert "moslqo" in r
186
+
187
+ def test_patch_similarity_result_str(self):
188
+ p = PatchSimilarityResult(similarity=0.85)
189
+ s = str(p)
190
+ assert "0.85" in s
191
+
192
+ def test_patch_similarity_result_repr(self):
193
+ p = PatchSimilarityResult(similarity=0.85)
194
+ r = repr(p)
195
+ assert "PatchSimilarityResult" in r
196
+
197
+
198
+ # ── measure_batch ──
199
+
200
+
201
+ class TestMeasureBatch:
202
+ """Test batch evaluation API."""
203
+
204
+ def test_batch_before_create_raises(self):
205
+ api = VisqolApi()
206
+ with pytest.raises(RuntimeError, match="create"):
207
+ api.measure_batch([("/a.wav", "/b.wav")])
208
+
209
+ def test_batch_nonexistent_files_returns_exceptions(self):
210
+ api = VisqolApi()
211
+ api.create(mode="speech")
212
+ results = api.measure_batch([
213
+ ("/nonexistent/a.wav", "/nonexistent/b.wav"),
214
+ ("/nonexistent/c.wav", "/nonexistent/d.wav"),
215
+ ])
216
+ assert len(results) == 2
217
+ assert all(isinstance(r, Exception) for r in results)
218
+
219
+ def test_batch_progress_callback(self):
220
+ api = VisqolApi()
221
+ api.create(mode="speech")
222
+ progress_log: list[tuple[int, int]] = []
223
+
224
+ def cb(done: int, total: int) -> None:
225
+ progress_log.append((done, total))
226
+
227
+ results = api.measure_batch(
228
+ [("/nonexistent/a.wav", "/nonexistent/b.wav")],
229
+ progress_callback=cb,
230
+ )
231
+ assert len(results) == 1
232
+ assert progress_log == [(1, 1)]
233
+
234
+ def test_batch_empty(self):
235
+ api = VisqolApi()
236
+ api.create(mode="speech")
237
+ results = api.measure_batch([])
238
+ assert results == []
239
+
240
+
241
+ # ── Package metadata ──
242
+
243
+
244
+ class TestVersion:
245
+ """Test package version is accessible."""
246
+
247
+ def test_version_string(self):
248
+ import visqol
249
+ assert hasattr(visqol, "__version__")
250
+ assert isinstance(visqol.__version__, str)
251
+ parts = visqol.__version__.split(".")
252
+ assert len(parts) >= 2, "Version should have at least major.minor"
253
+
254
+ def test_public_exports(self):
255
+ """Package should export key classes."""
256
+ import visqol
257
+ assert hasattr(visqol, "VisqolApi")
258
+ assert hasattr(visqol, "SimilarityResult")
259
+ assert hasattr(visqol, "AudioSignal")
260
+ assert hasattr(visqol, "PatchSimilarityResult")
261
+ assert hasattr(visqol, "ProgressCallback")
@@ -13,8 +13,17 @@ Usage:
13
13
  print(f"MOS-LQO: {result.moslqo}")
14
14
  """
15
15
 
16
- __version__ = "3.3.4"
16
+ __version__: str = "3.3.6"
17
17
 
18
- from visqol.api import VisqolApi
18
+ from visqol.api import ProgressCallback, VisqolApi
19
+ from visqol.audio_utils import AudioSignal
20
+ from visqol.nsim import PatchSimilarityResult
21
+ from visqol.visqol_core import SimilarityResult
19
22
 
20
- __all__ = ["VisqolApi"]
23
+ __all__: list[str] = [
24
+ "VisqolApi",
25
+ "SimilarityResult",
26
+ "AudioSignal",
27
+ "PatchSimilarityResult",
28
+ "ProgressCallback",
29
+ ]
@@ -0,0 +1,115 @@
1
+ """
2
+ ViSQOL command-line interface.
3
+
4
+ Usage::
5
+
6
+ python -m visqol --reference ref.wav --degraded deg.wav [--speech_mode]
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import logging
13
+ import sys
14
+
15
+ from visqol.api import VisqolApi
16
+
17
+ logger = logging.getLogger("visqol")
18
+
19
+
20
+ def main() -> None:
21
+ """Entry point for the ``visqol`` CLI."""
22
+ parser = argparse.ArgumentParser(
23
+ description="ViSQOL - Virtual Speech Quality Objective Listener (Python)",
24
+ )
25
+ parser.add_argument(
26
+ "--reference", "-r", required=True,
27
+ help="Path to reference audio file (WAV)",
28
+ )
29
+ parser.add_argument(
30
+ "--degraded", "-d", required=True,
31
+ help="Path to degraded audio file (WAV)",
32
+ )
33
+ parser.add_argument(
34
+ "--speech_mode", action="store_true",
35
+ help="Use speech mode (16 kHz, exponential mapping)",
36
+ )
37
+ parser.add_argument(
38
+ "--model", default=None,
39
+ help="Path to SVR model file (Audio mode only)",
40
+ )
41
+ parser.add_argument(
42
+ "--search_window", type=int, default=60,
43
+ help="Search window radius (default: 60)",
44
+ )
45
+ parser.add_argument(
46
+ "--unscaled_speech", action="store_true",
47
+ help="Don't scale speech MOS to max 5.0",
48
+ )
49
+ parser.add_argument(
50
+ "--no_alignment", action="store_true",
51
+ help="Disable global alignment",
52
+ )
53
+ parser.add_argument(
54
+ "--no_realignment", action="store_true",
55
+ help="Disable fine realignment",
56
+ )
57
+ parser.add_argument(
58
+ "--verbose", "-v", action="store_true",
59
+ help="Enable verbose output",
60
+ )
61
+
62
+ args = parser.parse_args()
63
+
64
+ # Setup logging
65
+ level = logging.DEBUG if args.verbose else logging.WARNING
66
+ logging.basicConfig(
67
+ level=level,
68
+ format="%(levelname)s: %(message)s",
69
+ stream=sys.stderr,
70
+ )
71
+
72
+ # Run ViSQOL
73
+ mode: str = "speech" if args.speech_mode else "audio"
74
+
75
+ try:
76
+ api = VisqolApi()
77
+ api.create(
78
+ mode=mode,
79
+ model_path=args.model,
80
+ search_window=args.search_window,
81
+ use_unscaled_speech=args.unscaled_speech,
82
+ disable_global_alignment=args.no_alignment,
83
+ disable_realignment=args.no_realignment,
84
+ )
85
+
86
+ result = api.measure(args.reference, args.degraded)
87
+
88
+ except (FileNotFoundError, ValueError, RuntimeError) as exc:
89
+ logger.error("%s", exc)
90
+ sys.exit(1)
91
+ except Exception as exc:
92
+ logger.error("Unexpected error: %s", exc)
93
+ sys.exit(2)
94
+
95
+ # Output results
96
+ print(f"MOS-LQO: {result.moslqo:.6f}")
97
+ print(f"VNSIM: {result.vnsim:.6f}")
98
+
99
+ if args.verbose:
100
+ logger.info("FVNSIM: %s", result.fvnsim)
101
+ logger.info("FVNSIM10: %s", result.fvnsim10)
102
+ logger.info("FSTDNSIM: %s", result.fstdnsim)
103
+ logger.info("FVDEGENERGY: %s", result.fvdegenergy)
104
+ logger.info("Patches: %d", len(result.patch_sims))
105
+ for i, p in enumerate(result.patch_sims):
106
+ logger.info(
107
+ " Patch %d: sim=%.4f ref=[%.3f-%.3f] deg=[%.3f-%.3f]",
108
+ i, p.similarity,
109
+ p.ref_patch_start_time, p.ref_patch_end_time,
110
+ p.deg_patch_start_time, p.deg_patch_end_time,
111
+ )
112
+
113
+
114
+ if __name__ == "__main__":
115
+ main()