visqol-python 3.4.0__tar.gz → 3.5.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 → visqol_python-3.5.0}/CHANGELOG.md +63 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/PKG-INFO +78 -18
- {visqol_python-3.4.0 → visqol_python-3.5.0}/README.md +69 -16
- {visqol_python-3.4.0 → visqol_python-3.5.0}/pyproject.toml +16 -2
- {visqol_python-3.4.0 → visqol_python-3.5.0}/tests/test_conformance.py +50 -7
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/__init__.py +1 -1
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/__main__.py +21 -2
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/api.py +21 -3
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/gammatone.py +2 -2
- visqol_python-3.5.0/visqol/model/lattice_tcditugenmeetpackhref_ls2_nl60_lr12_bs2048_learn.005_ep2400_train1_7_raw.tflite +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/nsim.py +4 -1
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/numba_accel.py +9 -3
- visqol_python-3.5.0/visqol/quality_mapper.py +228 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/signal_utils.py +8 -5
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/visqol_manager.py +49 -4
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/PKG-INFO +78 -18
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/SOURCES.txt +1 -0
- visqol_python-3.5.0/visqol_python.egg-info/requires.txt +30 -0
- visqol_python-3.4.0/visqol/quality_mapper.py +0 -124
- visqol_python-3.4.0/visqol_python.egg-info/requires.txt +0 -16
- {visqol_python-3.4.0 → visqol_python-3.5.0}/CONTRIBUTING.md +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/LICENSE +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/MANIFEST.in +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/setup.cfg +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/tests/test_parallel_correctness.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/tests/test_quick.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/alignment.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/analysis_window.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/audio_utils.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/model/libsvm_nu_svr_model.txt +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/patch_creator.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/patch_selector.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/py.typed +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol/visqol_core.py +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/dependency_links.txt +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/entry_points.txt +0 -0
- {visqol_python-3.4.0 → visqol_python-3.5.0}/visqol_python.egg-info/top_level.txt +0 -0
|
@@ -4,6 +4,68 @@ 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.5.0] - 2026-05-26
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Deep-lattice TFLite speech quality mapper** (`pip install visqol-python[lattice]`):
|
|
11
|
+
- `TFLiteSpeechQualityMapper` loads the same `.tflite` lattice network used by
|
|
12
|
+
C++ ViSQOL's default `--use_lattice_model=true` and runs inference through
|
|
13
|
+
the upstream Google TFLite C++ runtime via `ai-edge-litert`
|
|
14
|
+
- New `use_lattice_model` parameter on `VisqolApi.create()` (default `None`
|
|
15
|
+
auto-enables lattice when the runtime is installed)
|
|
16
|
+
- New `lattice_model_path` parameter to override the bundled model
|
|
17
|
+
- New CLI flags `--no_lattice_model` and `--lattice_model PATH`
|
|
18
|
+
- New `[lattice]` and `[all]` extras in `pyproject.toml`
|
|
19
|
+
- Bundled `lattice_*.tflite` (2.1 MB) into the wheel as package data
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- **GH issue #1**: Speech-mode MOS scores were systematically 1–2 points higher
|
|
23
|
+
than C++ ViSQOL's default. Root cause: the Python port only implemented the
|
|
24
|
+
legacy polynomial mapper (`SpeechSimilarityToQualityMapper`, equivalent to
|
|
25
|
+
C++ `--use_lattice_model=false`), while the C++ default routes through the
|
|
26
|
+
TFLite lattice network. Installing `visqol-python[lattice]` now matches C++
|
|
27
|
+
default scoring (CA01 conformance: diff 0.027 vs 1–2 MOS before).
|
|
28
|
+
- **`signal_utils.normalize()` parity bug**: the previous implementation did
|
|
29
|
+
min–max scaling to ``[0, 1]`` (shifting the signal positive and adding a DC
|
|
30
|
+
offset), while C++ ``MiscMath::Normalize`` only divides by the peak. This
|
|
31
|
+
inflated the RMS values fed to the speech-mode VAD, causing Python to keep
|
|
32
|
+
every patch as voice-active and adding spurious patches the C++ binary
|
|
33
|
+
would have discarded. Fixing this brought polynomial speech parity from
|
|
34
|
+
diff 0.007 → 0.001 and was a prerequisite for lattice parity. Only the
|
|
35
|
+
speech-mode VAD path used this function; audio mode is unaffected.
|
|
36
|
+
- **`nsim` stddev estimator mismatch**: both ``nsim.measure_patch_similarity``
|
|
37
|
+
(``np.std(..., ddof=0)``) and the Numba ``_measure_patch_similarity_numba``
|
|
38
|
+
kernel (``sqrt(ss / cols)``) used the population estimator (divide by N).
|
|
39
|
+
C++ uses Armadillo's ``stddev(..., 0)`` which is the *unbiased* sample
|
|
40
|
+
estimator (divide by N-1, despite the misleading ``0`` flag). The
|
|
41
|
+
per-band ``freq_band_stddevs`` was therefore systematically smaller by
|
|
42
|
+
``sqrt((N-1)/N) ≈ 0.974``, which fed into the pooled ``fstdnsim`` and
|
|
43
|
+
perturbed every lattice prediction.
|
|
44
|
+
- **`numba_accel.fastmath=True` on the Gammatone spectrogram kernel**: the
|
|
45
|
+
compounded LLVM-level FP reassociation across the 4-stage cascaded IIR ×
|
|
46
|
+
thousands of samples × hundreds of frames pushed lattice MOS off by
|
|
47
|
+
another ~0.02 vs strict IEEE-754. ``fastmath`` has been removed from the
|
|
48
|
+
spectrogram kernel; ``parallel=True`` is kept (each frame's IIR state is
|
|
49
|
+
independent so the reduction is safe).
|
|
50
|
+
|
|
51
|
+
### Speech-mode parity numbers (CA01 conformance)
|
|
52
|
+
|
|
53
|
+
| Mode | Before all fixes | After all fixes | C++ baseline |
|
|
54
|
+
|------|------------------|-----------------|--------------|
|
|
55
|
+
| Polynomial | diff 0.0067 | **diff 0.0011** | 3.3745 |
|
|
56
|
+
| Lattice | diff 0.0856 (≈1–2 MOS on Nils's TCD-VOIP samples) | **diff 0.0023** | 3.3130 |
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- Speech mode `create(mode="speech")` now auto-uses lattice when available; when
|
|
60
|
+
`ai-edge-litert` is missing, it logs a one-time warning and falls back to
|
|
61
|
+
polynomial (existing scores reproduce exactly).
|
|
62
|
+
- `tests/test_conformance.py` split the single speech case into
|
|
63
|
+
`test_speech_polynomial_conformance` (existing C++ polynomial baseline 3.3745)
|
|
64
|
+
and `test_speech_lattice_conformance` (regression baseline captured from this
|
|
65
|
+
implementation).
|
|
66
|
+
- README: documented the polynomial-vs-lattice distinction, new install matrix,
|
|
67
|
+
and parity caveats.
|
|
68
|
+
|
|
7
69
|
## [3.4.0] - 2026-03-23
|
|
8
70
|
|
|
9
71
|
### Added
|
|
@@ -93,6 +155,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
|
|
|
93
155
|
- Bundled SVR model (`libsvm_nu_svr_model.txt`)
|
|
94
156
|
- GitHub Actions workflow for auto-publish to PyPI via Trusted Publisher
|
|
95
157
|
|
|
158
|
+
[3.5.0]: https://github.com/talker93/visqol-python/compare/v3.4.0...v3.5.0
|
|
96
159
|
[3.4.0]: https://github.com/talker93/visqol-python/compare/v3.3.6...v3.4.0
|
|
97
160
|
[3.3.6]: https://github.com/talker93/visqol-python/compare/v3.3.5...v3.3.6
|
|
98
161
|
[3.3.5]: https://github.com/talker93/visqol-python/compare/v3.3.4...v3.3.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: visqol-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: ViSQOL - Virtual Speech Quality Objective Listener (Pure Python)
|
|
5
5
|
Author: Shan Jiang
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -9,7 +9,7 @@ Project-URL: Changelog, https://github.com/talker93/visqol-python/blob/main/CHAN
|
|
|
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
|
|
12
|
-
Keywords: audio-quality,speech-quality,MOS,PESQ,POLQA,visqol,objective-metric,perceptual-quality
|
|
12
|
+
Keywords: audio-quality,speech-quality,MOS,PESQ,POLQA,visqol,objective-metric,perceptual-quality,numba,batch-processing
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
15
15
|
Classifier: Intended Audience :: Science/Research
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
22
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
23
23
|
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Typing :: Typed
|
|
24
25
|
Requires-Python: >=3.9
|
|
25
26
|
Description-Content-Type: text/markdown
|
|
26
27
|
License-File: LICENSE
|
|
@@ -30,6 +31,11 @@ Requires-Dist: soundfile>=0.10
|
|
|
30
31
|
Requires-Dist: libsvm-official>=3.25
|
|
31
32
|
Provides-Extra: accel
|
|
32
33
|
Requires-Dist: numba>=0.57; extra == "accel"
|
|
34
|
+
Provides-Extra: lattice
|
|
35
|
+
Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "lattice"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: numba>=0.57; extra == "all"
|
|
38
|
+
Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "all"
|
|
33
39
|
Provides-Extra: test
|
|
34
40
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
35
41
|
Provides-Extra: dev
|
|
@@ -37,6 +43,7 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
37
43
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
38
44
|
Requires-Dist: mypy>=1.8; extra == "dev"
|
|
39
45
|
Requires-Dist: numba>=0.57; extra == "dev"
|
|
46
|
+
Requires-Dist: ai-edge-litert>=2.1.5; python_version >= "3.10" and extra == "dev"
|
|
40
47
|
Dynamic: license-file
|
|
41
48
|
|
|
42
49
|
# ViSQOL (Python)
|
|
@@ -46,19 +53,25 @@ Dynamic: license-file
|
|
|
46
53
|
[](https://pypi.org/project/visqol-python/)
|
|
47
54
|
[](LICENSE)
|
|
48
55
|
|
|
49
|
-
A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener)
|
|
56
|
+
A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) for objective audio/speech quality assessment.
|
|
50
57
|
|
|
51
58
|
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
59
|
|
|
53
60
|
## Features
|
|
54
61
|
|
|
55
62
|
- **Two modes**: Audio mode (music/general audio at 48 kHz) and Speech mode (speech at 16 kHz)
|
|
56
|
-
- **High accuracy**:
|
|
63
|
+
- **High accuracy**: 12/12 conformance tests pass against the official C++ implementation
|
|
57
64
|
- Audio mode: 9/10 tests produce **identical** MOS scores (diff = 0.000000), 1 test diff = 0.000117
|
|
58
|
-
- Speech mode: diff = 0.
|
|
59
|
-
-
|
|
60
|
-
- **
|
|
61
|
-
- **
|
|
65
|
+
- Speech mode (polynomial): diff = 0.001057
|
|
66
|
+
- Speech mode (lattice TFLite): diff = 0.002341
|
|
67
|
+
- **Two speech quality mappers** matching C++ ViSQOL:
|
|
68
|
+
- **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
|
|
69
|
+
- **Polynomial (fallback)** — legacy exponential fit (`--use_lattice_model=false` in C++)
|
|
70
|
+
- **Pure Python**: no C/C++ compilation required (the optional `[lattice]` extra adds the Google `ai-edge-litert` TFLite runtime as a binary wheel)
|
|
71
|
+
- **Minimal dependencies**: 4 core pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
|
|
72
|
+
- **Optional Numba acceleration**: `pip install visqol-python[accel]` for JIT-compiled Gammatone filterbank (parallel + fastmath) and DP patch matching — **9× overall speedup**, RTF 0.064 (surpasses C++ estimates)
|
|
73
|
+
- **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
|
|
74
|
+
- **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
|
|
62
75
|
|
|
63
76
|
## Installation
|
|
64
77
|
|
|
@@ -66,14 +79,34 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
|
|
|
66
79
|
pip install visqol-python
|
|
67
80
|
```
|
|
68
81
|
|
|
82
|
+
For **C++-default-equivalent speech mode** (deep-lattice TFLite mapper):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pip install visqol-python[lattice] # requires Python ≥ 3.10
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For **Numba-accelerated** Gammatone filtering and DP matching (~9× faster):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install visqol-python[accel]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Install everything (lattice + numba):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
pip install visqol-python[all]
|
|
98
|
+
```
|
|
99
|
+
|
|
69
100
|
Or install from source:
|
|
70
101
|
|
|
71
102
|
```bash
|
|
72
103
|
git clone https://github.com/talker93/visqol-python.git
|
|
73
104
|
cd visqol-python
|
|
74
|
-
pip install -e .
|
|
105
|
+
pip install -e ".[dev]"
|
|
75
106
|
```
|
|
76
107
|
|
|
108
|
+
> **Note on speech mode parity**: Without the `[lattice]` extra, speech mode falls back to the polynomial mapping (equivalent to running C++ ViSQOL with `--use_lattice_model=false`). The polynomial can over-predict MOS by 1–2 points on degraded speech vs the C++ default. Install `[lattice]` whenever you need numbers that line up with the C++ default behaviour (see [issue #1](https://github.com/talker93/visqol-python/issues/1)).
|
|
109
|
+
|
|
77
110
|
## Quick Start
|
|
78
111
|
|
|
79
112
|
### Python API
|
|
@@ -124,12 +157,15 @@ file_pairs = [
|
|
|
124
157
|
("ref3.wav", "deg3.wav"),
|
|
125
158
|
]
|
|
126
159
|
|
|
127
|
-
#
|
|
160
|
+
# Sequential with progress callback
|
|
128
161
|
results = api.measure_batch(
|
|
129
162
|
file_pairs,
|
|
130
163
|
progress_callback=lambda done, total: print(f"{done}/{total}"),
|
|
131
164
|
)
|
|
132
165
|
|
|
166
|
+
# Multi-process parallel (uses all CPU cores)
|
|
167
|
+
results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
|
|
168
|
+
|
|
133
169
|
for pair, result in zip(file_pairs, results):
|
|
134
170
|
if isinstance(result, Exception):
|
|
135
171
|
print(f"{pair}: FAILED — {result}")
|
|
@@ -156,7 +192,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
|
|
|
156
192
|
|------|-------------|
|
|
157
193
|
| `-r`, `--reference` | Path to reference WAV file (required) |
|
|
158
194
|
| `-d`, `--degraded` | Path to degraded WAV file (required) |
|
|
159
|
-
| `--speech_mode` | Use speech mode (16 kHz
|
|
195
|
+
| `--speech_mode` | Use speech mode (16 kHz) |
|
|
196
|
+
| `--no_lattice_model` | Speech mode: disable lattice TFLite mapper, use polynomial fallback |
|
|
197
|
+
| `--lattice_model` | Custom path to lattice `.tflite` model (speech mode) |
|
|
198
|
+
| `--unscaled_speech` | Don't scale polynomial speech MOS to 5.0 (polynomial only) |
|
|
160
199
|
| `--model` | Custom SVR model file path (audio mode only) |
|
|
161
200
|
| `--search_window` | Search window radius (default: 60) |
|
|
162
201
|
| `--verbose`, `-v` | Show detailed per-patch results |
|
|
@@ -184,21 +223,34 @@ The `measure()` method returns a `SimilarityResult` object with:
|
|
|
184
223
|
|
|
185
224
|
### Speech Mode
|
|
186
225
|
- Target sample rate: **16 kHz**
|
|
187
|
-
-
|
|
188
|
-
- Quality mapping: exponential polynomial fit
|
|
226
|
+
- 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
|
|
189
227
|
- VAD (Voice Activity Detection) based patch selection
|
|
228
|
+
- Quality mapping (choose one):
|
|
229
|
+
- **Deep-lattice TFLite (default)** — same mapper as C++ ViSQOL's default `--use_lattice_model=true`; requires `pip install visqol-python[lattice]`
|
|
230
|
+
- **Exponential polynomial (fallback)** — same as C++ `--use_lattice_model=false`; used automatically when the lattice runtime is not installed
|
|
231
|
+
- Toggle from Python: `api.create(mode="speech", use_lattice_model=False)`
|
|
232
|
+
- Toggle from CLI: `--no_lattice_model`
|
|
190
233
|
- Best for: speech, VoIP, telephony
|
|
191
234
|
|
|
192
235
|
## Performance
|
|
193
236
|
|
|
194
237
|
Measured on Apple M-series, Python 3.13:
|
|
195
238
|
|
|
239
|
+
### Without Numba (pure Python + NumPy/SciPy)
|
|
240
|
+
|
|
196
241
|
| Mode | Avg RTF | Typical Time |
|
|
197
242
|
|------|---------|-------------|
|
|
198
|
-
| Audio (48 kHz) | **0.
|
|
243
|
+
| Audio (48 kHz) | **0.18x** | ~2.2 s per file pair |
|
|
199
244
|
| Speech (16 kHz) | **0.38x** | ~1 s per file pair |
|
|
200
245
|
|
|
246
|
+
### With Numba (`pip install visqol-python[accel]`)
|
|
247
|
+
|
|
248
|
+
| Mode | Avg RTF | Typical Time | Speedup |
|
|
249
|
+
|------|---------|-------------|---------|
|
|
250
|
+
| Audio (48 kHz) | **0.064x** | ~0.8 s per file pair | **9×** |
|
|
251
|
+
|
|
201
252
|
> RTF (Real-Time Factor) < 1.0 means faster than real-time.
|
|
253
|
+
> With Numba acceleration, the Python implementation **surpasses C++ estimated performance** (RTF ≈ 0.093).
|
|
202
254
|
|
|
203
255
|
## Project Structure
|
|
204
256
|
|
|
@@ -218,18 +270,23 @@ visqol-python/
|
|
|
218
270
|
│ ├── alignment.py # Global alignment via cross-correlation
|
|
219
271
|
│ ├── nsim.py # NSIM similarity metric
|
|
220
272
|
│ ├── quality_mapper.py # SVR & exponential quality mapping
|
|
273
|
+
│ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
|
|
221
274
|
│ ├── __main__.py # CLI entry point
|
|
275
|
+
│ ├── py.typed # PEP 561 type marker
|
|
222
276
|
│ └── model/ # Bundled SVR model
|
|
223
277
|
│ └── libsvm_nu_svr_model.txt
|
|
224
|
-
├── tests/ # Tests (pytest)
|
|
278
|
+
├── tests/ # Tests & benchmarks (pytest)
|
|
225
279
|
│ ├── conftest.py # Shared fixtures & CLI options
|
|
226
280
|
│ ├── test_quick.py # Smoke tests (no external data needed)
|
|
227
|
-
│
|
|
281
|
+
│ ├── test_conformance.py # Full conformance tests (needs testdata)
|
|
282
|
+
│ ├── test_parallel_correctness.py # Numba parallel correctness tests
|
|
283
|
+
│ └── bench_*.py # Performance benchmarks
|
|
228
284
|
├── .github/workflows/
|
|
229
|
-
│ ├── ci.yml # CI: test
|
|
285
|
+
│ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
|
|
230
286
|
│ └── publish.yml # Auto-publish to PyPI on tag push
|
|
231
287
|
├── pyproject.toml # Package metadata & build config
|
|
232
288
|
├── CHANGELOG.md
|
|
289
|
+
├── CONTRIBUTING.md
|
|
233
290
|
├── LICENSE
|
|
234
291
|
└── README.md
|
|
235
292
|
```
|
|
@@ -250,7 +307,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
|
|
|
250
307
|
| glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
|
|
251
308
|
| contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
|
|
252
309
|
| castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
|
|
253
|
-
| speech_CA01 | Speech | 3.3745 | 3.
|
|
310
|
+
| speech_CA01 (polynomial) | Speech | 3.3745 | 3.3756 | 0.001057 |
|
|
311
|
+
| speech_CA01 (lattice) | Speech | 3.3130 | 3.3153 | 0.002341 |
|
|
312
|
+
|
|
313
|
+
Both speech values come from running the C++ ViSQOL binary directly with the corresponding `--use_lattice_model` flag, so they represent ground-truth parity targets.
|
|
254
314
|
|
|
255
315
|
## References
|
|
256
316
|
|
|
@@ -5,19 +5,25 @@
|
|
|
5
5
|
[](https://pypi.org/project/visqol-python/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener)
|
|
8
|
+
A pure Python implementation of [Google's ViSQOL](https://github.com/google/visqol) (Virtual Speech Quality Objective Listener) for objective audio/speech quality assessment.
|
|
9
9
|
|
|
10
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**.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- **Two modes**: Audio mode (music/general audio at 48 kHz) and Speech mode (speech at 16 kHz)
|
|
15
|
-
- **High accuracy**:
|
|
15
|
+
- **High accuracy**: 12/12 conformance tests pass against the official C++ implementation
|
|
16
16
|
- Audio mode: 9/10 tests produce **identical** MOS scores (diff = 0.000000), 1 test diff = 0.000117
|
|
17
|
-
- Speech mode: diff = 0.
|
|
18
|
-
-
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
17
|
+
- Speech mode (polynomial): diff = 0.001057
|
|
18
|
+
- Speech mode (lattice TFLite): diff = 0.002341
|
|
19
|
+
- **Two speech quality mappers** matching C++ ViSQOL:
|
|
20
|
+
- **Lattice (default)** — deep-lattice TFLite network (`--use_lattice_model=true` in C++); requires the optional `[lattice]` extra
|
|
21
|
+
- **Polynomial (fallback)** — legacy exponential fit (`--use_lattice_model=false` in C++)
|
|
22
|
+
- **Pure Python**: no C/C++ compilation required (the optional `[lattice]` extra adds the Google `ai-edge-litert` TFLite runtime as a binary wheel)
|
|
23
|
+
- **Minimal dependencies**: 4 core pip packages (`numpy`, `scipy`, `soundfile`, `libsvm-official`)
|
|
24
|
+
- **Optional Numba acceleration**: `pip install visqol-python[accel]` for JIT-compiled Gammatone filterbank (parallel + fastmath) and DP patch matching — **9× overall speedup**, RTF 0.064 (surpasses C++ estimates)
|
|
25
|
+
- **Batch & parallel evaluation**: `measure_batch(parallel=True)` for multi-process execution across CPU cores
|
|
26
|
+
- **Fully typed**: PEP 561 `py.typed`, strict mypy, ruff-enforced code style
|
|
21
27
|
|
|
22
28
|
## Installation
|
|
23
29
|
|
|
@@ -25,14 +31,34 @@ ViSQOL compares a reference audio signal with a degraded version and outputs a *
|
|
|
25
31
|
pip install visqol-python
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
For **C++-default-equivalent speech mode** (deep-lattice TFLite mapper):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install visqol-python[lattice] # requires Python ≥ 3.10
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For **Numba-accelerated** Gammatone filtering and DP matching (~9× faster):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install visqol-python[accel]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Install everything (lattice + numba):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install visqol-python[all]
|
|
50
|
+
```
|
|
51
|
+
|
|
28
52
|
Or install from source:
|
|
29
53
|
|
|
30
54
|
```bash
|
|
31
55
|
git clone https://github.com/talker93/visqol-python.git
|
|
32
56
|
cd visqol-python
|
|
33
|
-
pip install -e .
|
|
57
|
+
pip install -e ".[dev]"
|
|
34
58
|
```
|
|
35
59
|
|
|
60
|
+
> **Note on speech mode parity**: Without the `[lattice]` extra, speech mode falls back to the polynomial mapping (equivalent to running C++ ViSQOL with `--use_lattice_model=false`). The polynomial can over-predict MOS by 1–2 points on degraded speech vs the C++ default. Install `[lattice]` whenever you need numbers that line up with the C++ default behaviour (see [issue #1](https://github.com/talker93/visqol-python/issues/1)).
|
|
61
|
+
|
|
36
62
|
## Quick Start
|
|
37
63
|
|
|
38
64
|
### Python API
|
|
@@ -83,12 +109,15 @@ file_pairs = [
|
|
|
83
109
|
("ref3.wav", "deg3.wav"),
|
|
84
110
|
]
|
|
85
111
|
|
|
86
|
-
#
|
|
112
|
+
# Sequential with progress callback
|
|
87
113
|
results = api.measure_batch(
|
|
88
114
|
file_pairs,
|
|
89
115
|
progress_callback=lambda done, total: print(f"{done}/{total}"),
|
|
90
116
|
)
|
|
91
117
|
|
|
118
|
+
# Multi-process parallel (uses all CPU cores)
|
|
119
|
+
results = api.measure_batch(file_pairs, parallel=True, max_workers=4)
|
|
120
|
+
|
|
92
121
|
for pair, result in zip(file_pairs, results):
|
|
93
122
|
if isinstance(result, Exception):
|
|
94
123
|
print(f"{pair}: FAILED — {result}")
|
|
@@ -115,7 +144,10 @@ python -m visqol -r reference.wav -d degraded.wav -v
|
|
|
115
144
|
|------|-------------|
|
|
116
145
|
| `-r`, `--reference` | Path to reference WAV file (required) |
|
|
117
146
|
| `-d`, `--degraded` | Path to degraded WAV file (required) |
|
|
118
|
-
| `--speech_mode` | Use speech mode (16 kHz
|
|
147
|
+
| `--speech_mode` | Use speech mode (16 kHz) |
|
|
148
|
+
| `--no_lattice_model` | Speech mode: disable lattice TFLite mapper, use polynomial fallback |
|
|
149
|
+
| `--lattice_model` | Custom path to lattice `.tflite` model (speech mode) |
|
|
150
|
+
| `--unscaled_speech` | Don't scale polynomial speech MOS to 5.0 (polynomial only) |
|
|
119
151
|
| `--model` | Custom SVR model file path (audio mode only) |
|
|
120
152
|
| `--search_window` | Search window radius (default: 60) |
|
|
121
153
|
| `--verbose`, `-v` | Show detailed per-patch results |
|
|
@@ -143,21 +175,34 @@ The `measure()` method returns a `SimilarityResult` object with:
|
|
|
143
175
|
|
|
144
176
|
### Speech Mode
|
|
145
177
|
- Target sample rate: **16 kHz**
|
|
146
|
-
-
|
|
147
|
-
- Quality mapping: exponential polynomial fit
|
|
178
|
+
- 21 Gammatone frequency bands (50 Hz – 8 000 Hz)
|
|
148
179
|
- VAD (Voice Activity Detection) based patch selection
|
|
180
|
+
- Quality mapping (choose one):
|
|
181
|
+
- **Deep-lattice TFLite (default)** — same mapper as C++ ViSQOL's default `--use_lattice_model=true`; requires `pip install visqol-python[lattice]`
|
|
182
|
+
- **Exponential polynomial (fallback)** — same as C++ `--use_lattice_model=false`; used automatically when the lattice runtime is not installed
|
|
183
|
+
- Toggle from Python: `api.create(mode="speech", use_lattice_model=False)`
|
|
184
|
+
- Toggle from CLI: `--no_lattice_model`
|
|
149
185
|
- Best for: speech, VoIP, telephony
|
|
150
186
|
|
|
151
187
|
## Performance
|
|
152
188
|
|
|
153
189
|
Measured on Apple M-series, Python 3.13:
|
|
154
190
|
|
|
191
|
+
### Without Numba (pure Python + NumPy/SciPy)
|
|
192
|
+
|
|
155
193
|
| Mode | Avg RTF | Typical Time |
|
|
156
194
|
|------|---------|-------------|
|
|
157
|
-
| Audio (48 kHz) | **0.
|
|
195
|
+
| Audio (48 kHz) | **0.18x** | ~2.2 s per file pair |
|
|
158
196
|
| Speech (16 kHz) | **0.38x** | ~1 s per file pair |
|
|
159
197
|
|
|
198
|
+
### With Numba (`pip install visqol-python[accel]`)
|
|
199
|
+
|
|
200
|
+
| Mode | Avg RTF | Typical Time | Speedup |
|
|
201
|
+
|------|---------|-------------|---------|
|
|
202
|
+
| Audio (48 kHz) | **0.064x** | ~0.8 s per file pair | **9×** |
|
|
203
|
+
|
|
160
204
|
> RTF (Real-Time Factor) < 1.0 means faster than real-time.
|
|
205
|
+
> With Numba acceleration, the Python implementation **surpasses C++ estimated performance** (RTF ≈ 0.093).
|
|
161
206
|
|
|
162
207
|
## Project Structure
|
|
163
208
|
|
|
@@ -177,18 +222,23 @@ visqol-python/
|
|
|
177
222
|
│ ├── alignment.py # Global alignment via cross-correlation
|
|
178
223
|
│ ├── nsim.py # NSIM similarity metric
|
|
179
224
|
│ ├── quality_mapper.py # SVR & exponential quality mapping
|
|
225
|
+
│ ├── numba_accel.py # Optional Numba JIT kernels (DP, NSIM, Gammatone)
|
|
180
226
|
│ ├── __main__.py # CLI entry point
|
|
227
|
+
│ ├── py.typed # PEP 561 type marker
|
|
181
228
|
│ └── model/ # Bundled SVR model
|
|
182
229
|
│ └── libsvm_nu_svr_model.txt
|
|
183
|
-
├── tests/ # Tests (pytest)
|
|
230
|
+
├── tests/ # Tests & benchmarks (pytest)
|
|
184
231
|
│ ├── conftest.py # Shared fixtures & CLI options
|
|
185
232
|
│ ├── test_quick.py # Smoke tests (no external data needed)
|
|
186
|
-
│
|
|
233
|
+
│ ├── test_conformance.py # Full conformance tests (needs testdata)
|
|
234
|
+
│ ├── test_parallel_correctness.py # Numba parallel correctness tests
|
|
235
|
+
│ └── bench_*.py # Performance benchmarks
|
|
187
236
|
├── .github/workflows/
|
|
188
|
-
│ ├── ci.yml # CI: test
|
|
237
|
+
│ ├── ci.yml # CI: lint + type-check + matrix test (Python × NumPy)
|
|
189
238
|
│ └── publish.yml # Auto-publish to PyPI on tag push
|
|
190
239
|
├── pyproject.toml # Package metadata & build config
|
|
191
240
|
├── CHANGELOG.md
|
|
241
|
+
├── CONTRIBUTING.md
|
|
192
242
|
├── LICENSE
|
|
193
243
|
└── README.md
|
|
194
244
|
```
|
|
@@ -209,7 +259,10 @@ Tested against the [official C++ ViSQOL v3.3.3](https://github.com/google/visqol
|
|
|
209
259
|
| glock_48aac | Audio | 4.3325 | 4.3325 | 0.000000 |
|
|
210
260
|
| contrabassoon_24aac | Audio | 2.3469 | 2.3468 | 0.000117 |
|
|
211
261
|
| castanets_identity | Audio | 4.7321 | 4.7321 | 0.000000 |
|
|
212
|
-
| speech_CA01 | Speech | 3.3745 | 3.
|
|
262
|
+
| speech_CA01 (polynomial) | Speech | 3.3745 | 3.3756 | 0.001057 |
|
|
263
|
+
| speech_CA01 (lattice) | Speech | 3.3130 | 3.3153 | 0.002341 |
|
|
264
|
+
|
|
265
|
+
Both speech values come from running the C++ ViSQOL binary directly with the corresponding `--use_lattice_model` flag, so they represent ground-truth parity targets.
|
|
213
266
|
|
|
214
267
|
## References
|
|
215
268
|
|
|
@@ -15,6 +15,7 @@ authors = [
|
|
|
15
15
|
keywords = [
|
|
16
16
|
"audio-quality", "speech-quality", "MOS", "PESQ", "POLQA",
|
|
17
17
|
"visqol", "objective-metric", "perceptual-quality",
|
|
18
|
+
"numba", "batch-processing",
|
|
18
19
|
]
|
|
19
20
|
classifiers = [
|
|
20
21
|
"Development Status :: 4 - Beta",
|
|
@@ -28,6 +29,7 @@ classifiers = [
|
|
|
28
29
|
"Programming Language :: Python :: 3.13",
|
|
29
30
|
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
30
31
|
"Topic :: Scientific/Engineering",
|
|
32
|
+
"Typing :: Typed",
|
|
31
33
|
]
|
|
32
34
|
dependencies = [
|
|
33
35
|
"numpy>=1.20",
|
|
@@ -38,12 +40,18 @@ dependencies = [
|
|
|
38
40
|
|
|
39
41
|
[project.optional-dependencies]
|
|
40
42
|
accel = ["numba>=0.57"]
|
|
43
|
+
lattice = ["ai-edge-litert>=2.1.5; python_version >= '3.10'"]
|
|
44
|
+
all = [
|
|
45
|
+
"numba>=0.57",
|
|
46
|
+
"ai-edge-litert>=2.1.5; python_version >= '3.10'",
|
|
47
|
+
]
|
|
41
48
|
test = ["pytest>=7.0"]
|
|
42
49
|
dev = [
|
|
43
50
|
"pytest>=7.0",
|
|
44
51
|
"ruff>=0.4",
|
|
45
52
|
"mypy>=1.8",
|
|
46
53
|
"numba>=0.57",
|
|
54
|
+
"ai-edge-litert>=2.1.5; python_version >= '3.10'",
|
|
47
55
|
]
|
|
48
56
|
|
|
49
57
|
[project.urls]
|
|
@@ -63,7 +71,7 @@ version = {attr = "visqol.__version__"}
|
|
|
63
71
|
exclude = ["tests*"]
|
|
64
72
|
|
|
65
73
|
[tool.setuptools.package-data]
|
|
66
|
-
visqol = ["model/*.txt", "py.typed"]
|
|
74
|
+
visqol = ["model/*.txt", "model/*.tflite", "py.typed"]
|
|
67
75
|
|
|
68
76
|
[tool.pytest.ini_options]
|
|
69
77
|
testpaths = ["tests"]
|
|
@@ -103,7 +111,13 @@ warn_return_any = true
|
|
|
103
111
|
warn_unused_configs = true
|
|
104
112
|
|
|
105
113
|
[[tool.mypy.overrides]]
|
|
106
|
-
module = [
|
|
114
|
+
module = [
|
|
115
|
+
"svmutil.*",
|
|
116
|
+
"libsvm.*",
|
|
117
|
+
"soundfile.*",
|
|
118
|
+
"numba.*",
|
|
119
|
+
"ai_edge_litert.*",
|
|
120
|
+
]
|
|
107
121
|
ignore_missing_imports = true
|
|
108
122
|
|
|
109
123
|
[[tool.mypy.overrides]]
|
|
@@ -52,9 +52,22 @@ def audio_api():
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
@pytest.fixture(scope="session")
|
|
55
|
-
def
|
|
55
|
+
def speech_polynomial_api():
|
|
56
|
+
"""Speech mode with the legacy polynomial mapper (C++ --use_lattice_model=false)."""
|
|
56
57
|
api = VisqolApi()
|
|
57
|
-
api.create(mode="speech")
|
|
58
|
+
api.create(mode="speech", use_lattice_model=False)
|
|
59
|
+
return api
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.fixture(scope="session")
|
|
63
|
+
def speech_lattice_api():
|
|
64
|
+
"""Speech mode with the deep-lattice TFLite mapper (C++ default)."""
|
|
65
|
+
pytest.importorskip(
|
|
66
|
+
"ai_edge_litert",
|
|
67
|
+
reason="ai-edge-litert not installed (pip install visqol-python[lattice]).",
|
|
68
|
+
)
|
|
69
|
+
api = VisqolApi()
|
|
70
|
+
api.create(mode="speech", use_lattice_model=True)
|
|
58
71
|
return api
|
|
59
72
|
|
|
60
73
|
|
|
@@ -105,10 +118,21 @@ AUDIO_CASES = [
|
|
|
105
118
|
),
|
|
106
119
|
]
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
# Speech conformance with the polynomial mapper (== C++ --use_lattice_model=false).
|
|
122
|
+
# Expected values come from running C++ ViSQOL v3.3.3 with that flag.
|
|
123
|
+
SPEECH_POLYNOMIAL_CASES = [
|
|
109
124
|
("CA01_01.wav", "transcoded_CA01_01.wav", 3.374505555111911, "CA01_transcoded"),
|
|
110
125
|
]
|
|
111
126
|
|
|
127
|
+
# Speech conformance with the deep-lattice TFLite mapper (C++ default).
|
|
128
|
+
# Expected values come from running the C++ ViSQOL binary with default flags
|
|
129
|
+
# (--use_lattice_model=true). Residual divergence from this Python port is
|
|
130
|
+
# tracked by the TOLERANCE constant above; lattice tends to amplify upstream
|
|
131
|
+
# numerical noise more than the polynomial mapper.
|
|
132
|
+
SPEECH_LATTICE_CASES = [
|
|
133
|
+
("CA01_01.wav", "transcoded_CA01_01.wav", 3.3129589557647705, "CA01_transcoded_lattice"),
|
|
134
|
+
]
|
|
135
|
+
|
|
112
136
|
|
|
113
137
|
# ── Audio mode tests ──
|
|
114
138
|
|
|
@@ -133,13 +157,32 @@ def test_audio_conformance(audio_api, conf_dir, ref_name, deg_name, expected_mos
|
|
|
133
157
|
|
|
134
158
|
@pytest.mark.parametrize(
|
|
135
159
|
"ref_name, deg_name, expected_mos, test_id",
|
|
136
|
-
|
|
137
|
-
ids=[c[3] for c in
|
|
160
|
+
SPEECH_POLYNOMIAL_CASES,
|
|
161
|
+
ids=[c[3] for c in SPEECH_POLYNOMIAL_CASES],
|
|
162
|
+
)
|
|
163
|
+
def test_speech_polynomial_conformance(
|
|
164
|
+
speech_polynomial_api, speech_dir, ref_name, deg_name, expected_mos, test_id
|
|
165
|
+
):
|
|
166
|
+
ref_path = os.path.join(speech_dir, ref_name)
|
|
167
|
+
deg_path = os.path.join(speech_dir, deg_name)
|
|
168
|
+
result = speech_polynomial_api.measure(ref_path, deg_path)
|
|
169
|
+
diff = abs(result.moslqo - expected_mos)
|
|
170
|
+
assert diff < TOLERANCE, (
|
|
171
|
+
f"[{test_id}] MOS={result.moslqo:.6f}, expected={expected_mos:.6f}, diff={diff:.6f}"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.parametrize(
|
|
176
|
+
"ref_name, deg_name, expected_mos, test_id",
|
|
177
|
+
SPEECH_LATTICE_CASES,
|
|
178
|
+
ids=[c[3] for c in SPEECH_LATTICE_CASES],
|
|
138
179
|
)
|
|
139
|
-
def
|
|
180
|
+
def test_speech_lattice_conformance(
|
|
181
|
+
speech_lattice_api, speech_dir, ref_name, deg_name, expected_mos, test_id
|
|
182
|
+
):
|
|
140
183
|
ref_path = os.path.join(speech_dir, ref_name)
|
|
141
184
|
deg_path = os.path.join(speech_dir, deg_name)
|
|
142
|
-
result =
|
|
185
|
+
result = speech_lattice_api.measure(ref_path, deg_path)
|
|
143
186
|
diff = abs(result.moslqo - expected_mos)
|
|
144
187
|
assert diff < TOLERANCE, (
|
|
145
188
|
f"[{test_id}] MOS={result.moslqo:.6f}, expected={expected_mos:.6f}, diff={diff:.6f}"
|
|
@@ -53,7 +53,21 @@ def main() -> None:
|
|
|
53
53
|
parser.add_argument(
|
|
54
54
|
"--unscaled_speech",
|
|
55
55
|
action="store_true",
|
|
56
|
-
help="Don't scale speech MOS to max 5.0",
|
|
56
|
+
help="Don't scale speech MOS to max 5.0 (polynomial mapping only)",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--no_lattice_model",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help=(
|
|
62
|
+
"Speech mode: disable the deep-lattice TFLite mapper and use the "
|
|
63
|
+
"polynomial mapping instead. By default lattice is auto-enabled "
|
|
64
|
+
"when `ai-edge-litert` is installed (matches C++ default)."
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--lattice_model",
|
|
69
|
+
default=None,
|
|
70
|
+
help="Path to lattice .tflite model (speech mode, optional override).",
|
|
57
71
|
)
|
|
58
72
|
parser.add_argument(
|
|
59
73
|
"--no_alignment",
|
|
@@ -86,19 +100,24 @@ def main() -> None:
|
|
|
86
100
|
mode: str = "speech" if args.speech_mode else "audio"
|
|
87
101
|
|
|
88
102
|
try:
|
|
103
|
+
# Lattice toggle: pass False if --no_lattice_model, else None (auto-detect).
|
|
104
|
+
use_lattice = False if args.no_lattice_model else None
|
|
105
|
+
|
|
89
106
|
api = VisqolApi()
|
|
90
107
|
api.create(
|
|
91
108
|
mode=mode,
|
|
92
109
|
model_path=args.model,
|
|
93
110
|
search_window=args.search_window,
|
|
94
111
|
use_unscaled_speech=args.unscaled_speech,
|
|
112
|
+
use_lattice_model=use_lattice,
|
|
113
|
+
lattice_model_path=args.lattice_model,
|
|
95
114
|
disable_global_alignment=args.no_alignment,
|
|
96
115
|
disable_realignment=args.no_realignment,
|
|
97
116
|
)
|
|
98
117
|
|
|
99
118
|
result = api.measure(args.reference, args.degraded)
|
|
100
119
|
|
|
101
|
-
except (FileNotFoundError, ValueError, RuntimeError) as exc:
|
|
120
|
+
except (FileNotFoundError, ValueError, RuntimeError, ImportError) as exc:
|
|
102
121
|
logger.error("%s", exc)
|
|
103
122
|
sys.exit(1)
|
|
104
123
|
except Exception as exc:
|