visqol-python 3.3.3__tar.gz → 3.3.4__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 (33) hide show
  1. {visqol_python-3.3.3/visqol_python.egg-info → visqol_python-3.3.4}/PKG-INFO +26 -18
  2. {visqol_python-3.3.3 → visqol_python-3.3.4}/README.md +21 -12
  3. {visqol_python-3.3.3 → visqol_python-3.3.4}/pyproject.toml +12 -3
  4. visqol_python-3.3.4/tests/test_conformance.py +125 -0
  5. visqol_python-3.3.4/tests/test_quick.py +89 -0
  6. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/__init__.py +1 -1
  7. {visqol_python-3.3.3 → visqol_python-3.3.4/visqol_python.egg-info}/PKG-INFO +26 -18
  8. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol_python.egg-info/SOURCES.txt +0 -1
  9. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol_python.egg-info/requires.txt +3 -0
  10. visqol_python-3.3.3/setup.py +0 -58
  11. visqol_python-3.3.3/tests/test_conformance.py +0 -173
  12. visqol_python-3.3.3/tests/test_quick.py +0 -81
  13. {visqol_python-3.3.3 → visqol_python-3.3.4}/LICENSE +0 -0
  14. {visqol_python-3.3.3 → visqol_python-3.3.4}/MANIFEST.in +0 -0
  15. {visqol_python-3.3.3 → visqol_python-3.3.4}/requirements.txt +0 -0
  16. {visqol_python-3.3.3 → visqol_python-3.3.4}/setup.cfg +0 -0
  17. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/__main__.py +0 -0
  18. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/alignment.py +0 -0
  19. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/analysis_window.py +0 -0
  20. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/api.py +0 -0
  21. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/audio_utils.py +0 -0
  22. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/gammatone.py +0 -0
  23. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  24. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/nsim.py +0 -0
  25. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/patch_creator.py +0 -0
  26. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/patch_selector.py +0 -0
  27. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/quality_mapper.py +0 -0
  28. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/signal_utils.py +0 -0
  29. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/visqol_core.py +0 -0
  30. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol/visqol_manager.py +0 -0
  31. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol_python.egg-info/dependency_links.txt +0 -0
  32. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol_python.egg-info/entry_points.txt +0 -0
  33. {visqol_python-3.3.3 → visqol_python-3.3.4}/visqol_python.egg-info/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.3.3
3
+ Version: 3.3.4
4
4
  Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
5
- Home-page: https://github.com/talker93/visqol-python
6
5
  Author: Shan Jiang
7
6
  License-Expression: Apache-2.0
8
7
  Project-URL: Homepage, https://github.com/talker93/visqol-python
8
+ Project-URL: Changelog, https://github.com/talker93/visqol-python/blob/main/CHANGELOG.md
9
9
  Project-URL: Bug Reports, https://github.com/talker93/visqol-python/issues
10
10
  Project-URL: Source, https://github.com/talker93/visqol-python
11
11
  Project-URL: Original C++, https://github.com/google/visqol
@@ -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.8
18
17
  Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
@@ -22,19 +21,24 @@ Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
24
23
  Classifier: Topic :: Scientific/Engineering
25
- Requires-Python: >=3.8
24
+ Requires-Python: >=3.9
26
25
  Description-Content-Type: text/markdown
27
26
  License-File: LICENSE
28
27
  Requires-Dist: numpy>=1.20
29
28
  Requires-Dist: scipy>=1.7
30
29
  Requires-Dist: soundfile>=0.10
31
30
  Requires-Dist: libsvm-official>=3.25
32
- Dynamic: home-page
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7.0; extra == "test"
33
33
  Dynamic: license-file
34
- Dynamic: requires-python
35
34
 
36
35
  # ViSQOL (Python)
37
36
 
37
+ [![PyPI version](https://img.shields.io/pypi/v/visqol-python)](https://pypi.org/project/visqol-python/)
38
+ [![CI](https://github.com/talker93/visqol-python/actions/workflows/ci.yml/badge.svg)](https://github.com/talker93/visqol-python/actions/workflows/ci.yml)
39
+ [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
40
+ [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
41
+
38
42
  A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) v3.3.3 for objective audio/speech quality assessment.
39
43
 
40
44
  ViSQOL compares a reference audio signal with a degraded version and outputs a **MOS-LQO** (Mean Opinion Score - Listening Quality Objective) score on a scale of **1.0 – 5.0**.
@@ -52,10 +56,10 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
52
56
  ## Installation
53
57
 
54
58
  ```bash
55
- pip install numpy scipy soundfile libsvm-official
59
+ pip install visqol-python
56
60
  ```
57
61
 
58
- Or install as a package:
62
+ Or install from source:
59
63
 
60
64
  ```bash
61
65
  git clone https://github.com/talker93/visqol-python.git
@@ -167,8 +171,8 @@ Measured on Apple M-series, Python 3.13:
167
171
  ```
168
172
  visqol-python/
169
173
  ├── visqol/ # Main package
170
- │ ├── __init__.py # Package exports
171
- │ ├── api.py # Public API
174
+ │ ├── __init__.py # Package exports & version
175
+ │ ├── api.py # Public API (VisqolApi)
172
176
  │ ├── visqol_manager.py # Pipeline orchestrator
173
177
  │ ├── visqol_core.py # Core algorithm
174
178
  │ ├── audio_utils.py # Audio I/O & SPL normalization
@@ -180,14 +184,18 @@ visqol-python/
180
184
  │ ├── alignment.py # Global alignment via cross-correlation
181
185
  │ ├── nsim.py # NSIM similarity metric
182
186
  │ ├── quality_mapper.py # SVR & exponential quality mapping
183
- └── __main__.py # CLI entry point
184
- ├── model/ # Bundled SVR model
185
- └── libsvm_nu_svr_model.txt
186
- ├── tests/ # Conformance tests
187
- │ ├── test_conformance.py
188
- └── test_quick.py
189
- ├── setup.py
190
- ├── requirements.txt
187
+ ├── __main__.py # CLI entry point
188
+ │ └── model/ # Bundled SVR model
189
+ └── libsvm_nu_svr_model.txt
190
+ ├── tests/ # Tests (pytest)
191
+ │ ├── conftest.py # Shared fixtures & CLI options
192
+ ├── test_quick.py # Smoke tests (no external data needed)
193
+ │ └── test_conformance.py # Full conformance tests (needs testdata)
194
+ ├── .github/workflows/
195
+ │ ├── ci.yml # CI: test on Python 3.9–3.13
196
+ │ └── publish.yml # Auto-publish to PyPI on tag push
197
+ ├── pyproject.toml # Package metadata & build config
198
+ ├── CHANGELOG.md
191
199
  ├── LICENSE
192
200
  └── README.md
193
201
  ```
@@ -1,5 +1,10 @@
1
1
  # ViSQOL (Python)
2
2
 
3
+ [![PyPI version](https://img.shields.io/pypi/v/visqol-python)](https://pypi.org/project/visqol-python/)
4
+ [![CI](https://github.com/talker93/visqol-python/actions/workflows/ci.yml/badge.svg)](https://github.com/talker93/visqol-python/actions/workflows/ci.yml)
5
+ [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
6
+ [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
7
+
3
8
  A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) v3.3.3 for objective audio/speech quality assessment.
4
9
 
5
10
  ViSQOL compares a reference audio signal with a degraded version and outputs a **MOS-LQO** (Mean Opinion Score - Listening Quality Objective) score on a scale of **1.0 – 5.0**.
@@ -17,10 +22,10 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
17
22
  ## Installation
18
23
 
19
24
  ```bash
20
- pip install numpy scipy soundfile libsvm-official
25
+ pip install visqol-python
21
26
  ```
22
27
 
23
- Or install as a package:
28
+ Or install from source:
24
29
 
25
30
  ```bash
26
31
  git clone https://github.com/talker93/visqol-python.git
@@ -132,8 +137,8 @@ Measured on Apple M-series, Python 3.13:
132
137
  ```
133
138
  visqol-python/
134
139
  ├── visqol/ # Main package
135
- │ ├── __init__.py # Package exports
136
- │ ├── api.py # Public API
140
+ │ ├── __init__.py # Package exports & version
141
+ │ ├── api.py # Public API (VisqolApi)
137
142
  │ ├── visqol_manager.py # Pipeline orchestrator
138
143
  │ ├── visqol_core.py # Core algorithm
139
144
  │ ├── audio_utils.py # Audio I/O & SPL normalization
@@ -145,14 +150,18 @@ visqol-python/
145
150
  │ ├── alignment.py # Global alignment via cross-correlation
146
151
  │ ├── nsim.py # NSIM similarity metric
147
152
  │ ├── quality_mapper.py # SVR & exponential quality mapping
148
- └── __main__.py # CLI entry point
149
- ├── model/ # Bundled SVR model
150
- └── libsvm_nu_svr_model.txt
151
- ├── tests/ # Conformance tests
152
- │ ├── test_conformance.py
153
- └── test_quick.py
154
- ├── setup.py
155
- ├── requirements.txt
153
+ ├── __main__.py # CLI entry point
154
+ │ └── model/ # Bundled SVR model
155
+ └── libsvm_nu_svr_model.txt
156
+ ├── tests/ # Tests (pytest)
157
+ │ ├── conftest.py # Shared fixtures & CLI options
158
+ ├── test_quick.py # Smoke tests (no external data needed)
159
+ │ └── test_conformance.py # Full conformance tests (needs testdata)
160
+ ├── .github/workflows/
161
+ │ ├── ci.yml # CI: test on Python 3.9–3.13
162
+ │ └── publish.yml # Auto-publish to PyPI on tag push
163
+ ├── pyproject.toml # Package metadata & build config
164
+ ├── CHANGELOG.md
156
165
  ├── LICENSE
157
166
  └── README.md
158
167
  ```
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "visqol-python"
7
- version = "3.3.3"
7
+ dynamic = ["version"]
8
8
  description = "ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
11
- requires-python = ">=3.8"
11
+ requires-python = ">=3.9"
12
12
  authors = [
13
13
  {name = "Shan Jiang"},
14
14
  ]
@@ -21,7 +21,6 @@ classifiers = [
21
21
  "Intended Audience :: Developers",
22
22
  "Intended Audience :: Science/Research",
23
23
  "Programming Language :: Python :: 3",
24
- "Programming Language :: Python :: 3.8",
25
24
  "Programming Language :: Python :: 3.9",
26
25
  "Programming Language :: Python :: 3.10",
27
26
  "Programming Language :: Python :: 3.11",
@@ -37,8 +36,12 @@ dependencies = [
37
36
  "libsvm-official>=3.25",
38
37
  ]
39
38
 
39
+ [project.optional-dependencies]
40
+ test = ["pytest>=7.0"]
41
+
40
42
  [project.urls]
41
43
  Homepage = "https://github.com/talker93/visqol-python"
44
+ Changelog = "https://github.com/talker93/visqol-python/blob/main/CHANGELOG.md"
42
45
  "Bug Reports" = "https://github.com/talker93/visqol-python/issues"
43
46
  Source = "https://github.com/talker93/visqol-python"
44
47
  "Original C++" = "https://github.com/google/visqol"
@@ -46,8 +49,14 @@ Source = "https://github.com/talker93/visqol-python"
46
49
  [project.scripts]
47
50
  visqol = "visqol.__main__:main"
48
51
 
52
+ [tool.setuptools.dynamic]
53
+ version = {attr = "visqol.__version__"}
54
+
49
55
  [tool.setuptools.packages.find]
50
56
  exclude = ["tests*"]
51
57
 
52
58
  [tool.setuptools.package-data]
53
59
  visqol = ["model/*.txt"]
60
+
61
+ [tool.pytest.ini_options]
62
+ testpaths = ["tests"]
@@ -0,0 +1,125 @@
1
+ """
2
+ ViSQOL Python conformance tests.
3
+
4
+ Requires the official ViSQOL testdata directory. Provide it via:
5
+ pytest tests/test_conformance.py --testdata /path/to/visqol/testdata
6
+
7
+ The testdata directory should contain:
8
+ conformance_testdata_subset/ (audio test WAV files)
9
+ clean_speech/ (speech test WAV files)
10
+
11
+ You can obtain these from the official ViSQOL repository:
12
+ https://github.com/google/visqol
13
+ """
14
+
15
+ import os
16
+
17
+ import pytest
18
+
19
+ from visqol import VisqolApi
20
+
21
+
22
+ # ── Fixtures ──
23
+
24
+ @pytest.fixture(scope="session")
25
+ def testdata_dir(request):
26
+ """Resolve testdata directory from --testdata or auto-detect."""
27
+ td = request.config.getoption("--testdata")
28
+ if td and os.path.isdir(td):
29
+ return td
30
+ # Fallback: look relative to this file (when inside the original visqol repo)
31
+ candidate = os.path.join(os.path.dirname(__file__), "..", "..", "testdata")
32
+ if os.path.isdir(candidate):
33
+ return candidate
34
+ pytest.skip("testdata directory not found — use --testdata /path/to/visqol/testdata")
35
+
36
+
37
+ @pytest.fixture(scope="session")
38
+ def conf_dir(testdata_dir):
39
+ return os.path.join(testdata_dir, "conformance_testdata_subset")
40
+
41
+
42
+ @pytest.fixture(scope="session")
43
+ def speech_dir(testdata_dir):
44
+ return os.path.join(testdata_dir, "clean_speech")
45
+
46
+
47
+ @pytest.fixture(scope="session")
48
+ def audio_api():
49
+ api = VisqolApi()
50
+ api.create(mode="audio")
51
+ return api
52
+
53
+
54
+ @pytest.fixture(scope="session")
55
+ def speech_api():
56
+ api = VisqolApi()
57
+ api.create(mode="speech")
58
+ return api
59
+
60
+
61
+ # ── Test data ──
62
+
63
+ TOLERANCE = 0.05
64
+
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"),
86
+ ]
87
+
88
+ SPEECH_CASES = [
89
+ ("CA01_01.wav", "transcoded_CA01_01.wav",
90
+ 3.374505555111911, "CA01_transcoded"),
91
+ ]
92
+
93
+
94
+ # ── Audio mode tests ──
95
+
96
+ @pytest.mark.parametrize(
97
+ "ref_name, deg_name, expected_mos, test_id",
98
+ AUDIO_CASES,
99
+ ids=[c[3] for c in AUDIO_CASES],
100
+ )
101
+ def test_audio_conformance(audio_api, conf_dir, ref_name, deg_name, expected_mos, test_id):
102
+ ref_path = os.path.join(conf_dir, ref_name)
103
+ deg_path = os.path.join(conf_dir, deg_name)
104
+ result = audio_api.measure(ref_path, deg_path)
105
+ diff = abs(result.moslqo - expected_mos)
106
+ assert diff < TOLERANCE, (
107
+ f"[{test_id}] MOS={result.moslqo:.6f}, expected={expected_mos:.6f}, diff={diff:.6f}"
108
+ )
109
+
110
+
111
+ # ── Speech mode tests ──
112
+
113
+ @pytest.mark.parametrize(
114
+ "ref_name, deg_name, expected_mos, test_id",
115
+ SPEECH_CASES,
116
+ ids=[c[3] for c in SPEECH_CASES],
117
+ )
118
+ def test_speech_conformance(speech_api, speech_dir, ref_name, deg_name, expected_mos, test_id):
119
+ ref_path = os.path.join(speech_dir, ref_name)
120
+ deg_path = os.path.join(speech_dir, deg_name)
121
+ result = speech_api.measure(ref_path, deg_path)
122
+ diff = abs(result.moslqo - expected_mos)
123
+ assert diff < TOLERANCE, (
124
+ f"[{test_id}] MOS={result.moslqo:.6f}, expected={expected_mos:.6f}, diff={diff:.6f}"
125
+ )
@@ -0,0 +1,89 @@
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 VisqolApi
11
+
12
+
13
+ class TestApiCreation:
14
+ """Test that VisqolApi can be created in different modes."""
15
+
16
+ def test_create_audio_mode(self):
17
+ api = VisqolApi()
18
+ api.create(mode="audio")
19
+
20
+ def test_create_speech_mode(self):
21
+ api = VisqolApi()
22
+ api.create(mode="speech")
23
+
24
+ def test_create_default_mode(self):
25
+ """Default mode (no argument) should work as audio mode."""
26
+ api = VisqolApi()
27
+ api.create()
28
+
29
+
30
+ class TestMeasureFromArrays:
31
+ """Test measure_from_arrays with synthetic signals."""
32
+
33
+ def test_identical_signal_high_score(self):
34
+ """Identical signals should produce a high MOS score."""
35
+ api = VisqolApi()
36
+ api.create(mode="speech")
37
+ sr = 16000
38
+ duration = 3.0
39
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
40
+ signal = 0.5 * np.sin(2 * np.pi * 440 * t)
41
+ result = api.measure_from_arrays(signal, signal, sample_rate=sr)
42
+ assert result.moslqo >= 4.0, (
43
+ f"Identical signal should give MOS >= 4.0, got {result.moslqo:.4f}"
44
+ )
45
+
46
+ def test_degraded_signal_lower_score(self):
47
+ """Adding noise to a signal should produce a lower MOS score."""
48
+ api = VisqolApi()
49
+ api.create(mode="speech")
50
+ sr = 16000
51
+ duration = 3.0
52
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
53
+ ref = 0.5 * np.sin(2 * np.pi * 440 * t)
54
+ rng = np.random.default_rng(42)
55
+ deg = ref + 0.3 * rng.standard_normal(len(ref))
56
+ result = api.measure_from_arrays(ref, deg, sample_rate=sr)
57
+ assert 1.0 <= result.moslqo <= 5.0, (
58
+ f"MOS should be in [1, 5], got {result.moslqo:.4f}"
59
+ )
60
+
61
+
62
+ class TestResultFields:
63
+ """Test that SimilarityResult has all expected fields."""
64
+
65
+ def test_result_has_expected_fields(self):
66
+ api = VisqolApi()
67
+ api.create(mode="speech")
68
+ sr = 16000
69
+ duration = 3.0
70
+ t = np.linspace(0, duration, int(sr * duration), endpoint=False)
71
+ signal = 0.5 * np.sin(2 * np.pi * 440 * t)
72
+ result = api.measure_from_arrays(signal, signal, sample_rate=sr)
73
+ assert hasattr(result, "moslqo")
74
+ assert hasattr(result, "vnsim")
75
+ assert hasattr(result, "fvnsim")
76
+ assert hasattr(result, "fstdnsim")
77
+ assert hasattr(result, "fvdegenergy")
78
+ assert hasattr(result, "patch_sims")
79
+
80
+
81
+ class TestVersion:
82
+ """Test package version is accessible."""
83
+
84
+ def test_version_string(self):
85
+ import visqol
86
+ assert hasattr(visqol, "__version__")
87
+ assert isinstance(visqol.__version__, str)
88
+ parts = visqol.__version__.split(".")
89
+ assert len(parts) >= 2, "Version should have at least major.minor"
@@ -13,7 +13,7 @@ Usage:
13
13
  print(f"MOS-LQO: {result.moslqo}")
14
14
  """
15
15
 
16
- __version__ = "3.3.3"
16
+ __version__ = "3.3.4"
17
17
 
18
18
  from visqol.api import VisqolApi
19
19
 
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.3.3
3
+ Version: 3.3.4
4
4
  Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
5
- Home-page: https://github.com/talker93/visqol-python
6
5
  Author: Shan Jiang
7
6
  License-Expression: Apache-2.0
8
7
  Project-URL: Homepage, https://github.com/talker93/visqol-python
8
+ Project-URL: Changelog, https://github.com/talker93/visqol-python/blob/main/CHANGELOG.md
9
9
  Project-URL: Bug Reports, https://github.com/talker93/visqol-python/issues
10
10
  Project-URL: Source, https://github.com/talker93/visqol-python
11
11
  Project-URL: Original C++, https://github.com/google/visqol
@@ -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.8
18
17
  Classifier: Programming Language :: Python :: 3.9
19
18
  Classifier: Programming Language :: Python :: 3.10
20
19
  Classifier: Programming Language :: Python :: 3.11
@@ -22,19 +21,24 @@ Classifier: Programming Language :: Python :: 3.12
22
21
  Classifier: Programming Language :: Python :: 3.13
23
22
  Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
24
23
  Classifier: Topic :: Scientific/Engineering
25
- Requires-Python: >=3.8
24
+ Requires-Python: >=3.9
26
25
  Description-Content-Type: text/markdown
27
26
  License-File: LICENSE
28
27
  Requires-Dist: numpy>=1.20
29
28
  Requires-Dist: scipy>=1.7
30
29
  Requires-Dist: soundfile>=0.10
31
30
  Requires-Dist: libsvm-official>=3.25
32
- Dynamic: home-page
31
+ Provides-Extra: test
32
+ Requires-Dist: pytest>=7.0; extra == "test"
33
33
  Dynamic: license-file
34
- Dynamic: requires-python
35
34
 
36
35
  # ViSQOL (Python)
37
36
 
37
+ [![PyPI version](https://img.shields.io/pypi/v/visqol-python)](https://pypi.org/project/visqol-python/)
38
+ [![CI](https://github.com/talker93/visqol-python/actions/workflows/ci.yml/badge.svg)](https://github.com/talker93/visqol-python/actions/workflows/ci.yml)
39
+ [![Python](https://img.shields.io/pypi/pyversions/visqol-python)](https://pypi.org/project/visqol-python/)
40
+ [![License](https://img.shields.io/github/license/talker93/visqol-python)](LICENSE)
41
+
38
42
  A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) v3.3.3 for objective audio/speech quality assessment.
39
43
 
40
44
  ViSQOL compares a reference audio signal with a degraded version and outputs a **MOS-LQO** (Mean Opinion Score - Listening Quality Objective) score on a scale of **1.0 – 5.0**.
@@ -52,10 +56,10 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
52
56
  ## Installation
53
57
 
54
58
  ```bash
55
- pip install numpy scipy soundfile libsvm-official
59
+ pip install visqol-python
56
60
  ```
57
61
 
58
- Or install as a package:
62
+ Or install from source:
59
63
 
60
64
  ```bash
61
65
  git clone https://github.com/talker93/visqol-python.git
@@ -167,8 +171,8 @@ Measured on Apple M-series, Python 3.13:
167
171
  ```
168
172
  visqol-python/
169
173
  ├── visqol/ # Main package
170
- │ ├── __init__.py # Package exports
171
- │ ├── api.py # Public API
174
+ │ ├── __init__.py # Package exports & version
175
+ │ ├── api.py # Public API (VisqolApi)
172
176
  │ ├── visqol_manager.py # Pipeline orchestrator
173
177
  │ ├── visqol_core.py # Core algorithm
174
178
  │ ├── audio_utils.py # Audio I/O & SPL normalization
@@ -180,14 +184,18 @@ visqol-python/
180
184
  │ ├── alignment.py # Global alignment via cross-correlation
181
185
  │ ├── nsim.py # NSIM similarity metric
182
186
  │ ├── quality_mapper.py # SVR & exponential quality mapping
183
- └── __main__.py # CLI entry point
184
- ├── model/ # Bundled SVR model
185
- └── libsvm_nu_svr_model.txt
186
- ├── tests/ # Conformance tests
187
- │ ├── test_conformance.py
188
- └── test_quick.py
189
- ├── setup.py
190
- ├── requirements.txt
187
+ ├── __main__.py # CLI entry point
188
+ │ └── model/ # Bundled SVR model
189
+ └── libsvm_nu_svr_model.txt
190
+ ├── tests/ # Tests (pytest)
191
+ │ ├── conftest.py # Shared fixtures & CLI options
192
+ ├── test_quick.py # Smoke tests (no external data needed)
193
+ │ └── test_conformance.py # Full conformance tests (needs testdata)
194
+ ├── .github/workflows/
195
+ │ ├── ci.yml # CI: test on Python 3.9–3.13
196
+ │ └── publish.yml # Auto-publish to PyPI on tag push
197
+ ├── pyproject.toml # Package metadata & build config
198
+ ├── CHANGELOG.md
191
199
  ├── LICENSE
192
200
  └── README.md
193
201
  ```
@@ -3,7 +3,6 @@ MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
5
  requirements.txt
6
- setup.py
7
6
  tests/test_conformance.py
8
7
  tests/test_quick.py
9
8
  visqol/__init__.py
@@ -2,3 +2,6 @@ numpy>=1.20
2
2
  scipy>=1.7
3
3
  soundfile>=0.10
4
4
  libsvm-official>=3.25
5
+
6
+ [test]
7
+ pytest>=7.0
@@ -1,58 +0,0 @@
1
- from setuptools import setup, find_packages
2
- import os
3
-
4
- here = os.path.abspath(os.path.dirname(__file__))
5
-
6
- with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
7
- long_description = f.read()
8
-
9
- setup(
10
- name="visqol-python",
11
- version="3.3.3",
12
- description="ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)",
13
- long_description=long_description,
14
- long_description_content_type="text/markdown",
15
- url="https://github.com/talker93/visqol-python",
16
- author="Shan Jiang",
17
- license="Apache-2.0",
18
- packages=find_packages(exclude=["tests"]),
19
- python_requires=">=3.8",
20
- install_requires=[
21
- "numpy>=1.20",
22
- "scipy>=1.7",
23
- "soundfile>=0.10",
24
- "libsvm-official>=3.25",
25
- ],
26
- entry_points={
27
- "console_scripts": [
28
- "visqol=visqol.__main__:main",
29
- ],
30
- },
31
- package_data={
32
- "visqol": ["model/*.txt"],
33
- },
34
- include_package_data=True,
35
- keywords=[
36
- "audio-quality", "speech-quality", "MOS", "PESQ", "POLQA",
37
- "visqol", "objective-metric", "perceptual-quality",
38
- ],
39
- classifiers=[
40
- "Development Status :: 4 - Beta",
41
- "Intended Audience :: Developers",
42
- "Intended Audience :: Science/Research",
43
- "Programming Language :: Python :: 3",
44
- "Programming Language :: Python :: 3.8",
45
- "Programming Language :: Python :: 3.9",
46
- "Programming Language :: Python :: 3.10",
47
- "Programming Language :: Python :: 3.11",
48
- "Programming Language :: Python :: 3.12",
49
- "Programming Language :: Python :: 3.13",
50
- "Topic :: Multimedia :: Sound/Audio :: Analysis",
51
- "Topic :: Scientific/Engineering",
52
- ],
53
- project_urls={
54
- "Bug Reports": "https://github.com/talker93/visqol-python/issues",
55
- "Source": "https://github.com/talker93/visqol-python",
56
- "Original C++": "https://github.com/google/visqol",
57
- },
58
- )
@@ -1,173 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- ViSQOL Python conformance tests.
4
-
5
- Usage:
6
- python tests/test_conformance.py --testdata /path/to/visqol/testdata
7
-
8
- The testdata directory should contain:
9
- conformance_testdata_subset/ (audio test WAV files)
10
- clean_speech/ (speech test WAV files)
11
-
12
- You can obtain these from the official ViSQOL repository:
13
- https://github.com/google/visqol
14
- """
15
-
16
- import argparse
17
- import time
18
- import sys
19
- import os
20
-
21
- # Add project root for imports
22
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
23
-
24
- from visqol.api import VisqolApi
25
-
26
-
27
- def _get_testdata_dir():
28
- parser = argparse.ArgumentParser(add_help=False)
29
- parser.add_argument('--testdata', default=None)
30
- args, _ = parser.parse_known_args()
31
- if args.testdata:
32
- return args.testdata
33
- # Fallback: look relative to this file (when inside the original visqol repo)
34
- candidate = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata')
35
- if os.path.isdir(candidate):
36
- return candidate
37
- print("ERROR: testdata directory not found.")
38
- print("Usage: python tests/test_conformance.py --testdata /path/to/visqol/testdata")
39
- sys.exit(1)
40
-
41
-
42
- TD = _get_testdata_dir()
43
- CONF = os.path.join(TD, 'conformance_testdata_subset')
44
- SPEECH = os.path.join(TD, 'clean_speech')
45
-
46
- TOLERANCE = 0.05
47
-
48
- AUDIO_TESTS = [
49
- ('strauss48_stereo.wav', 'strauss48_stereo_lp35.wav',
50
- 1.3888791489130758, 'strauss_lp35'),
51
- ('steely48_stereo.wav', 'steely48_stereo_lp7.wav',
52
- 2.2501683734385183, 'steely_lp7'),
53
- ('sopr48_stereo.wav', 'sopr48_stereo_256kbps_aac.wav',
54
- 4.68228969737946, 'sopr_256aac'),
55
- ('ravel48_stereo.wav', 'ravel48_stereo_128kbps_opus.wav',
56
- 4.465141897255348, 'ravel_128opus'),
57
- ('moonlight48_stereo.wav', 'moonlight48_stereo_128kbps_aac.wav',
58
- 4.684292801646114, 'moonlight_128aac'),
59
- ('harpsichord48_stereo.wav', 'harpsichord48_stereo_96kbps_mp3.wav',
60
- 4.22374532766003, 'harpsichord_96mp3'),
61
- ('guitar48_stereo.wav', 'guitar48_stereo_64kbps_aac.wav',
62
- 4.349722308064298, 'guitar_64aac'),
63
- ('glock48_stereo.wav', 'glock48_stereo_48kbps_aac.wav',
64
- 4.332452943882108, 'glock_48aac'),
65
- ('contrabassoon48_stereo.wav', 'contrabassoon48_stereo_24kbps_aac.wav',
66
- 2.346868205375293, 'contrabassoon_24aac'),
67
- ('castanets48_stereo.wav', 'castanets48_stereo.wav',
68
- 4.732101253042348, 'castanets_identity'),
69
- ]
70
-
71
- SPEECH_TESTS = [
72
- ('CA01_01.wav', 'transcoded_CA01_01.wav',
73
- 3.374505555111911, 'CA01_transcoded_exp'),
74
- ]
75
-
76
-
77
- def run_audio_tests():
78
- print("=" * 70)
79
- print("AUDIO MODE CONFORMANCE TESTS")
80
- print("=" * 70)
81
-
82
- api = VisqolApi()
83
- api.create(mode="audio")
84
-
85
- pass_count = 0
86
- fail_count = 0
87
- total_time = 0
88
-
89
- for ref, deg, expected, name in AUDIO_TESTS:
90
- ref_path = os.path.join(CONF, ref)
91
- deg_path = os.path.join(CONF, deg)
92
-
93
- t0 = time.time()
94
- result = api.measure(ref_path, deg_path)
95
- elapsed = time.time() - t0
96
- total_time += elapsed
97
-
98
- diff = abs(result.moslqo - expected)
99
- passed = diff < TOLERANCE
100
- status = "PASS" if passed else "FAIL"
101
-
102
- if passed:
103
- pass_count += 1
104
- else:
105
- fail_count += 1
106
-
107
- print(f" {status} {name:30s} "
108
- f"MOS={result.moslqo:.4f} "
109
- f"exp={expected:.4f} "
110
- f"diff={diff:.6f} "
111
- f"({elapsed:.1f}s)")
112
-
113
- print(f"\nAudio: {pass_count}/{len(AUDIO_TESTS)} passed "
114
- f"(total: {total_time:.1f}s)")
115
- return fail_count
116
-
117
-
118
- def run_speech_tests():
119
- print("\n" + "=" * 70)
120
- print("SPEECH MODE CONFORMANCE TESTS (exponential mapping)")
121
- print("=" * 70)
122
-
123
- api = VisqolApi()
124
- api.create(mode="speech")
125
-
126
- pass_count = 0
127
- fail_count = 0
128
- total_time = 0
129
-
130
- for ref, deg, expected, name in SPEECH_TESTS:
131
- ref_path = os.path.join(SPEECH, ref)
132
- deg_path = os.path.join(SPEECH, deg)
133
-
134
- t0 = time.time()
135
- result = api.measure(ref_path, deg_path)
136
- elapsed = time.time() - t0
137
- total_time += elapsed
138
-
139
- diff = abs(result.moslqo - expected)
140
- passed = diff < TOLERANCE
141
- status = "PASS" if passed else "FAIL"
142
-
143
- if passed:
144
- pass_count += 1
145
- else:
146
- fail_count += 1
147
-
148
- print(f" {status} {name:30s} "
149
- f"MOS={result.moslqo:.4f} "
150
- f"exp={expected:.4f} "
151
- f"diff={diff:.6f} "
152
- f"({elapsed:.1f}s)")
153
-
154
- print(f"\nSpeech: {pass_count}/{len(SPEECH_TESTS)} passed "
155
- f"(total: {total_time:.1f}s)")
156
- return fail_count
157
-
158
-
159
- if __name__ == "__main__":
160
- audio_fails = run_audio_tests()
161
- speech_fails = run_speech_tests()
162
-
163
- total_tests = len(AUDIO_TESTS) + len(SPEECH_TESTS)
164
- total_fails = audio_fails + speech_fails
165
-
166
- print("\n" + "=" * 70)
167
- if total_fails == 0:
168
- print(f"ALL {total_tests} CONFORMANCE TESTS PASSED!")
169
- else:
170
- print(f"FAILED: {total_fails}/{total_tests} tests")
171
- print("=" * 70)
172
-
173
- sys.exit(total_fails)
@@ -1,81 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Quick ViSQOL conformance tests (subset of 3 audio + 1 speech).
4
-
5
- Usage:
6
- python tests/test_quick.py --testdata /path/to/visqol/testdata
7
- """
8
- import argparse
9
- import time
10
- import sys
11
- import os
12
-
13
- # Add project root for imports
14
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
15
-
16
- from visqol.api import VisqolApi
17
-
18
-
19
- def _get_testdata_dir():
20
- parser = argparse.ArgumentParser(add_help=False)
21
- parser.add_argument('--testdata', default=None)
22
- args, _ = parser.parse_known_args()
23
- if args.testdata:
24
- return args.testdata
25
- candidate = os.path.join(os.path.dirname(__file__), '..', '..', 'testdata')
26
- if os.path.isdir(candidate):
27
- return candidate
28
- print("ERROR: testdata directory not found.")
29
- print("Usage: python tests/test_quick.py --testdata /path/to/visqol/testdata")
30
- sys.exit(1)
31
-
32
-
33
- TD = _get_testdata_dir()
34
- CONF = os.path.join(TD, 'conformance_testdata_subset')
35
- SPEECH = os.path.join(TD, 'clean_speech')
36
-
37
- # Test 3 audio + 1 speech
38
- api_audio = VisqolApi()
39
- api_audio.create(mode="audio")
40
-
41
- tests = [
42
- (CONF, 'guitar48_stereo.wav', 'guitar48_stereo_64kbps_aac.wav',
43
- 4.349722, 'guitar_64aac'),
44
- (CONF, 'strauss48_stereo.wav', 'strauss48_stereo_lp35.wav',
45
- 1.388879, 'strauss_lp35'),
46
- (CONF, 'castanets48_stereo.wav', 'castanets48_stereo.wav',
47
- 4.732101, 'castanets_id'),
48
- ]
49
-
50
- all_pass = True
51
- for td, ref, deg, expected, name in tests:
52
- t0 = time.time()
53
- r = api_audio.measure(os.path.join(td, ref), os.path.join(td, deg))
54
- dt = time.time() - t0
55
- d = abs(r.moslqo - expected)
56
- ok = d < 0.05
57
- if not ok:
58
- all_pass = False
59
- print(f"{'PASS' if ok else 'FAIL'} {name:20s} "
60
- f"MOS={r.moslqo:.4f} exp={expected:.4f} "
61
- f"diff={d:.6f} ({dt:.1f}s)")
62
- sys.stdout.flush()
63
-
64
- # Speech test
65
- api_speech = VisqolApi()
66
- api_speech.create(mode="speech")
67
- t0 = time.time()
68
- r = api_speech.measure(
69
- os.path.join(SPEECH, 'CA01_01.wav'),
70
- os.path.join(SPEECH, 'transcoded_CA01_01.wav'))
71
- dt = time.time() - t0
72
- d = abs(r.moslqo - 3.374506)
73
- ok = d < 0.05
74
- if not ok:
75
- all_pass = False
76
- print(f"{'PASS' if ok else 'FAIL'} {'speech_CA01':20s} "
77
- f"MOS={r.moslqo:.4f} exp=3.3745 "
78
- f"diff={d:.6f} ({dt:.1f}s)")
79
-
80
- print(f"\n{'ALL PASS' if all_pass else 'SOME FAILED'}")
81
- sys.exit(0 if all_pass else 1)
File without changes
File without changes
File without changes