visqol-python 3.3.6__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 (34) hide show
  1. {visqol_python-3.3.6 → visqol_python-3.4.0}/CHANGELOG.md +45 -0
  2. {visqol_python-3.3.6 → visqol_python-3.4.0}/PKG-INFO +4 -1
  3. {visqol_python-3.3.6 → visqol_python-3.4.0}/pyproject.toml +21 -2
  4. {visqol_python-3.3.6 → visqol_python-3.4.0}/tests/test_conformance.py +44 -23
  5. visqol_python-3.4.0/tests/test_parallel_correctness.py +96 -0
  6. {visqol_python-3.3.6 → visqol_python-3.4.0}/tests/test_quick.py +9 -9
  7. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/__init__.py +3 -3
  8. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/__main__.py +28 -12
  9. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/alignment.py +7 -11
  10. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/analysis_window.py +3 -6
  11. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/api.py +146 -31
  12. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/audio_utils.py +6 -11
  13. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/gammatone.py +142 -42
  14. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/nsim.py +13 -17
  15. visqol_python-3.4.0/visqol/numba_accel.py +619 -0
  16. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/patch_creator.py +25 -28
  17. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/patch_selector.py +224 -61
  18. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/quality_mapper.py +13 -14
  19. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/signal_utils.py +3 -3
  20. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/visqol_core.py +38 -31
  21. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/visqol_manager.py +16 -14
  22. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol_python.egg-info/PKG-INFO +4 -1
  23. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol_python.egg-info/SOURCES.txt +2 -0
  24. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol_python.egg-info/requires.txt +4 -0
  25. {visqol_python-3.3.6 → visqol_python-3.4.0}/CONTRIBUTING.md +0 -0
  26. {visqol_python-3.3.6 → visqol_python-3.4.0}/LICENSE +0 -0
  27. {visqol_python-3.3.6 → visqol_python-3.4.0}/MANIFEST.in +0 -0
  28. {visqol_python-3.3.6 → visqol_python-3.4.0}/README.md +0 -0
  29. {visqol_python-3.3.6 → visqol_python-3.4.0}/setup.cfg +0 -0
  30. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
  31. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol/py.typed +0 -0
  32. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol_python.egg-info/dependency_links.txt +0 -0
  33. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol_python.egg-info/entry_points.txt +0 -0
  34. {visqol_python-3.3.6 → visqol_python-3.4.0}/visqol_python.egg-info/top_level.txt +0 -0
@@ -4,6 +4,50 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [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
+
7
51
  ## [3.3.5] - 2026-03-23
8
52
 
9
53
  ### Added
@@ -49,6 +93,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
49
93
  - Bundled SVR model (`libsvm_nu_svr_model.txt`)
50
94
  - GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
51
95
 
96
+ [3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
52
97
  [3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
53
98
  [3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
54
99
  [3.3.4]: https://github.com/talker93/visqol-python/compare/v3.3.3...v3.3.4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: visqol-python
3
- Version: 3.3.6
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,12 +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"
33
35
  Provides-Extra: dev
34
36
  Requires-Dist: pytest>=7.0; extra == "dev"
35
37
  Requires-Dist: ruff>=0.4; extra == "dev"
36
38
  Requires-Dist: mypy>=1.8; extra == "dev"
39
+ Requires-Dist: numba>=0.57; extra == "dev"
37
40
  Dynamic: license-file
38
41
 
39
42
  # ViSQOL (Python)
@@ -37,11 +37,13 @@ dependencies = [
37
37
  ]
38
38
 
39
39
  [project.optional-dependencies]
40
+ accel = ["numba>=0.57"]
40
41
  test = ["pytest>=7.0"]
41
42
  dev = [
42
43
  "pytest>=7.0",
43
44
  "ruff>=0.4",
44
45
  "mypy>=1.8",
46
+ "numba>=0.57",
45
47
  ]
46
48
 
47
49
  [project.urls]
@@ -81,7 +83,16 @@ select = [
81
83
  "SIM", # flake8-simplify
82
84
  "RUF", # ruff-specific rules
83
85
  ]
84
- ignore = ["E501"] # line length handled by formatter
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"]
85
96
 
86
97
  [tool.ruff.lint.isort]
87
98
  known-first-party = ["visqol"]
@@ -92,5 +103,13 @@ warn_return_any = true
92
103
  warn_unused_configs = true
93
104
 
94
105
  [[tool.mypy.overrides]]
95
- module = ["svmutil.*", "libsvm.*", "soundfile.*"]
106
+ module = ["svmutil.*", "libsvm.*", "soundfile.*", "numba.*"]
96
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)
@@ -10,12 +10,10 @@ import pytest
10
10
  from visqol import (
11
11
  AudioSignal,
12
12
  PatchSimilarityResult,
13
- ProgressCallback,
14
13
  SimilarityResult,
15
14
  VisqolApi,
16
15
  )
17
16
 
18
-
19
17
  # ── API creation ──
20
18
 
21
19
 
@@ -126,9 +124,7 @@ class TestMeasureFromArrays:
126
124
  rng = np.random.default_rng(42)
127
125
  deg = ref + 0.3 * rng.standard_normal(len(ref))
128
126
  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
- )
127
+ assert 1.0 <= result.moslqo <= 5.0, f"MOS should be in [1, 5], got {result.moslqo:.4f}"
132
128
 
133
129
 
134
130
  # ── Result fields ──
@@ -209,10 +205,12 @@ class TestMeasureBatch:
209
205
  def test_batch_nonexistent_files_returns_exceptions(self):
210
206
  api = VisqolApi()
211
207
  api.create(mode="speech")
212
- results = api.measure_batch([
213
- ("/nonexistent/a.wav", "/nonexistent/b.wav"),
214
- ("/nonexistent/c.wav", "/nonexistent/d.wav"),
215
- ])
208
+ results = api.measure_batch(
209
+ [
210
+ ("/nonexistent/a.wav", "/nonexistent/b.wav"),
211
+ ("/nonexistent/c.wav", "/nonexistent/d.wav"),
212
+ ]
213
+ )
216
214
  assert len(results) == 2
217
215
  assert all(isinstance(r, Exception) for r in results)
218
216
 
@@ -246,6 +244,7 @@ class TestVersion:
246
244
 
247
245
  def test_version_string(self):
248
246
  import visqol
247
+
249
248
  assert hasattr(visqol, "__version__")
250
249
  assert isinstance(visqol.__version__, str)
251
250
  parts = visqol.__version__.split(".")
@@ -254,6 +253,7 @@ class TestVersion:
254
253
  def test_public_exports(self):
255
254
  """Package should export key classes."""
256
255
  import visqol
256
+
257
257
  assert hasattr(visqol, "VisqolApi")
258
258
  assert hasattr(visqol, "SimilarityResult")
259
259
  assert hasattr(visqol, "AudioSignal")
@@ -13,7 +13,7 @@ Usage:
13
13
  print(f"MOS-LQO: {result.moslqo}")
14
14
  """
15
15
 
16
- __version__: str = "3.3.6"
16
+ __version__: str = "3.4.0"
17
17
 
18
18
  from visqol.api import ProgressCallback, VisqolApi
19
19
  from visqol.audio_utils import AudioSignal
@@ -21,9 +21,9 @@ from visqol.nsim import PatchSimilarityResult
21
21
  from visqol.visqol_core import SimilarityResult
22
22
 
23
23
  __all__: list[str] = [
24
- "VisqolApi",
25
- "SimilarityResult",
26
24
  "AudioSignal",
27
25
  "PatchSimilarityResult",
28
26
  "ProgressCallback",
27
+ "SimilarityResult",
28
+ "VisqolApi",
29
29
  ]
@@ -23,39 +23,52 @@ def main() -> None:
23
23
  description="ViSQOL - Virtual Speech Quality Objective Listener (Python)",
24
24
  )
25
25
  parser.add_argument(
26
- "--reference", "-r", required=True,
26
+ "--reference",
27
+ "-r",
28
+ required=True,
27
29
  help="Path to reference audio file (WAV)",
28
30
  )
29
31
  parser.add_argument(
30
- "--degraded", "-d", required=True,
32
+ "--degraded",
33
+ "-d",
34
+ required=True,
31
35
  help="Path to degraded audio file (WAV)",
32
36
  )
33
37
  parser.add_argument(
34
- "--speech_mode", action="store_true",
38
+ "--speech_mode",
39
+ action="store_true",
35
40
  help="Use speech mode (16 kHz, exponential mapping)",
36
41
  )
37
42
  parser.add_argument(
38
- "--model", default=None,
43
+ "--model",
44
+ default=None,
39
45
  help="Path to SVR model file (Audio mode only)",
40
46
  )
41
47
  parser.add_argument(
42
- "--search_window", type=int, default=60,
48
+ "--search_window",
49
+ type=int,
50
+ default=60,
43
51
  help="Search window radius (default: 60)",
44
52
  )
45
53
  parser.add_argument(
46
- "--unscaled_speech", action="store_true",
54
+ "--unscaled_speech",
55
+ action="store_true",
47
56
  help="Don't scale speech MOS to max 5.0",
48
57
  )
49
58
  parser.add_argument(
50
- "--no_alignment", action="store_true",
59
+ "--no_alignment",
60
+ action="store_true",
51
61
  help="Disable global alignment",
52
62
  )
53
63
  parser.add_argument(
54
- "--no_realignment", action="store_true",
64
+ "--no_realignment",
65
+ action="store_true",
55
66
  help="Disable fine realignment",
56
67
  )
57
68
  parser.add_argument(
58
- "--verbose", "-v", action="store_true",
69
+ "--verbose",
70
+ "-v",
71
+ action="store_true",
59
72
  help="Enable verbose output",
60
73
  )
61
74
 
@@ -105,9 +118,12 @@ def main() -> None:
105
118
  for i, p in enumerate(result.patch_sims):
106
119
  logger.info(
107
120
  " 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,
121
+ i,
122
+ p.similarity,
123
+ p.ref_patch_start_time,
124
+ p.ref_patch_end_time,
125
+ p.deg_patch_start_time,
126
+ p.deg_patch_end_time,
111
127
  )
112
128
 
113
129
 
@@ -6,17 +6,13 @@ Corresponds to C++ file: alignment.cc
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from typing import Tuple
10
-
11
9
  import numpy as np
12
10
 
13
11
  from visqol.audio_utils import AudioSignal
14
- from visqol.signal_utils import upper_envelope, find_best_lag
12
+ from visqol.signal_utils import find_best_lag, upper_envelope
15
13
 
16
14
 
17
- def globally_align(
18
- reference: AudioSignal, degraded: AudioSignal
19
- ) -> Tuple[AudioSignal, float]:
15
+ def globally_align(reference: AudioSignal, degraded: AudioSignal) -> tuple[AudioSignal, float]:
20
16
  """
21
17
  Globally align degraded signal to reference signal.
22
18
 
@@ -43,7 +39,7 @@ def globally_align(
43
39
 
44
40
  if best_lag < 0:
45
41
  # Degraded comes before reference: truncate front of degraded
46
- new_data = degraded.data[abs(best_lag):]
42
+ new_data = degraded.data[abs(best_lag) :]
47
43
  else:
48
44
  # Reference comes before degraded: prepend zeros to degraded
49
45
  new_data = np.concatenate([np.zeros(best_lag), degraded.data])
@@ -55,7 +51,7 @@ def globally_align(
55
51
 
56
52
  def align_and_truncate(
57
53
  reference: AudioSignal, degraded: AudioSignal
58
- ) -> Tuple[AudioSignal, AudioSignal, float]:
54
+ ) -> tuple[AudioSignal, AudioSignal, float]:
59
55
  """
60
56
  Align and truncate signals to the same length.
61
57
 
@@ -70,15 +66,15 @@ def align_and_truncate(
70
66
  deg_data = aligned_deg.data
71
67
 
72
68
  if len(ref_data) > len(deg_data):
73
- ref_data = ref_data[:len(deg_data)]
69
+ ref_data = ref_data[: len(deg_data)]
74
70
  elif len(ref_data) < len(deg_data):
75
71
  # For positive lag, the beginning of ref aligns with zeros
76
72
  lag_samples = int(lag_seconds * reference.sample_rate)
77
73
  if lag_samples > 0:
78
74
  ref_data = ref_data[lag_samples:]
79
- deg_data = deg_data[lag_samples:lag_samples + len(ref_data)]
75
+ deg_data = deg_data[lag_samples : lag_samples + len(ref_data)]
80
76
  else:
81
- deg_data = deg_data[:len(ref_data)]
77
+ deg_data = deg_data[: len(ref_data)]
82
78
 
83
79
  # Ensure same length
84
80
  min_len = min(len(ref_data), len(deg_data))
@@ -43,7 +43,7 @@ class AnalysisWindow:
43
43
 
44
44
  self.window_duration: float = window_duration
45
45
  self.overlap: float = overlap
46
- self.size: int = int(round(sample_rate * window_duration))
46
+ self.size: int = round(sample_rate * window_duration)
47
47
  self._hann_window: NDArray[np.float64] | None = None
48
48
 
49
49
  @property
@@ -57,9 +57,7 @@ class AnalysisWindow:
57
57
  if self._hann_window is None:
58
58
  # Match C++ exactly: 0.5 − 0.5 * cos(2π i / (size − 1))
59
59
  n = self.size
60
- self._hann_window = 0.5 - 0.5 * np.cos(
61
- 2.0 * np.pi * np.arange(n) / (n - 1)
62
- )
60
+ self._hann_window = 0.5 - 0.5 * np.cos(2.0 * np.pi * np.arange(n) / (n - 1))
63
61
  return self._hann_window
64
62
 
65
63
  def apply_hann_window(self, frame: NDArray[np.float64]) -> NDArray[np.float64]:
@@ -70,7 +68,6 @@ class AnalysisWindow:
70
68
  """
71
69
  if len(frame) != self.size:
72
70
  raise ValueError(
73
- f"Frame length ({len(frame)}) does not match "
74
- f"window size ({self.size})."
71
+ f"Frame length ({len(frame)}) does not match window size ({self.size})."
75
72
  )
76
73
  return frame * self.hann_window