Metadata-Version: 2.4
Name: speequal
Version: 3.3.3.post1
Summary: Speech/audio quality metrics as prebuilt wheels: Google's ViSQOL v3 (speequal.visqol).
Home-page: https://github.com/danjacobellis/speequal
License: Apache-2.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: protobuf
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# speequal

Speech/audio quality metrics as prebuilt Python wheels — `pip install speequal`. One metric today:

**`speequal.visqol`** — [ViSQOL v3](https://github.com/google/visqol), Google's objective, full-reference perceptual audio/speech quality metric (MOS-LQO), plus a small numpy API. This is a packaging of Google's code, not a reimplementation: the C++ library, the pybind11 bindings, the SVR and TFLite lattice models and all default mappings are the ones in `google/visqol` at the commit recorded in [`UPSTREAM.md`](UPSTREAM.md), built with Bazel and redistributed under the same Apache-2.0 license (see `LICENSE`, `NOTICE`). Upstream is not on PyPI (the name is prohibited there) and needs Bazel plus a TensorFlow Lite source build to install; this wheel needs neither.

```
pip install speequal
```

Wheels: Linux x86_64, CPython 3.12 and 3.14, tagged `manylinux_2_39` (built on Ubuntu 24.04 with GCC 13; they need glibc ≥ 2.39 and a GCC-13-era `libstdc++`). Other platforms/interpreters fall back to the sdist, which needs Bazel (see below). Runtime dependencies: `numpy`, `protobuf` (≥ 4.21 — the bindings use `message_factory.GetMessageClass`; tested with 6.33 and 7.36).

## API

```python
from speequal import visqol

visqol.speech(ref, deg, fs=16000)          # MOS-LQO, speech mode (wideband, 16 kHz)
visqol.audio(ref, deg, fs=48000)           # MOS-LQO, audio mode (SVR model, 48 kHz)
visqol.measure(ref, deg, mode="speech", fs=16000)  # upstream's raw SimilarityResultMsg
visqol.selftest()                          # the install gate (see below); speequal.selftest() runs every metric's
```

`ref` / `deg` are 1-d float arrays (numpy or torch, any float dtype, values in `[-1, 1]`) or paths to 16-bit PCM WAV files. `speech()` raises unless `fs == 16000` and `audio()` unless `fs == 48000` — nothing is resampled for you; get to the right rate first. Lengths may differ (ViSQOL aligns and handles the mismatch itself; nothing is padded or cropped here). Model files are located inside the package, never relative to the working directory. `speequal.__version__` is the wheel version (`<upstream visqol version>.postN`), `speequal.visqol.UPSTREAM_COMMIT` the vendored git sha.

Options, all mirroring `VisqolConfig` fields: `speech(..., lattice=True)` maps NSIM→MOS with the TFLite deep-lattice model (upstream's binary default; its ceiling for an identical pair is ≈ 4.51), `lattice=False` uses the scaled polynomial mapping (identical pair → 5.0); `unscaled_mos_mapping=True` (requires `lattice=False`, as upstream ignores it otherwise) leaves a perfect score at ≈ 4.0. `measure()` additionally exposes `allow_unsupported_sample_rates` and `search_window_radius`, and returns `moslqo`, `vnsim`, `fvnsim`, `fvnsim10`, `fstdnsim`, `fvdegenergy`, `center_freq_bands`, `patch_sims`, `alignment_lag_s`.

Upstream's own modules are untouched and still importable, one level down: `from speequal.visqol import visqol_lib_py`, `from speequal.visqol.pb2 import visqol_config_pb2, similarity_result_pb2`.

## CLI

```
python -m speequal.visqol ref.wav deg.wav            # speech mode, prints the MOS-LQO
python -m speequal.visqol ref.wav deg.wav --audio    # audio mode
python -m speequal.visqol ref.wav deg.wav --verbose  # + VNSIM, per-band NSIM, patch count
```

The sample rate is read from the files (any rate in speech mode, 48 kHz in audio mode, like upstream's binary). `--no-lattice`, `--unscaled`, `--allow-unsupported-sample-rates` as above.

## Self-test

```
python -c "import speequal; speequal.selftest()"
```

For ViSQOL: (a) speech mode at 16 kHz: a clip vs itself hits the mapper ceiling (≈ 4.5 lattice / 5.0 polynomial) and a −20 dB-noise copy scores lower; (b) audio mode at 48 kHz: a clip vs itself ≥ 4.7; (c) **conformance**: the 20 reference/degraded/flag cases of upstream's `tests/conformance_test.cc` with the expected MOS-LQO values of `src/include/conformance.h` (copied into `speequal/visqol/conformance.json`) reproduce to 1e-3. The two clean-speech clips ship in the wheel; the other conformance WAVs (~55 MB) are downloaded once from `google/visqol` at the pinned commit into `~/.cache/visqol/` (`VISQOL_TESTDATA=/path/to/upstream/testdata` uses a local copy). `visqol.selftest(conformance=False)` skips the download.

## Building from source

The sdist on PyPI is upstream's tree plus the wrapper; `pip install` from it runs upstream's `setup.py`, which needs **Bazel 5.3.2** (bazelisk picks it up from `.bazelversion`), a C++17 compiler, and fetches TensorFlow 2.11 sources — so on an unsupported platform expect a long first build. From this repository:

```
git clone https://github.com/danjacobellis/speequal && cd speequal
pip install numpy setuptools wheel auditwheel patchelf build   # in the target interpreter
./build.sh                       # installs bazelisk into ~/.local/bin if absent; wheel lands in dist/
./build.sh --python=/path/to/other/python   # e.g. a 3.14
./install_for_debugging.sh       # build + install into ~/g + selftest
```

`build.sh` copies `upstream/` to `build/upstream/`, applies `patches/`, runs Bazel there, builds the wheel with upstream's `setup.py`, then `auditwheel repair`s it to the most specific manylinux policy the build host allows. Bazel's cache makes rebuilds fast; `--clean` expunges it.

## What changed vs upstream

Nothing in the metric. Every deviation from `google/visqol` is a file in [`patches/`](patches/):

- `0001-bazelrc-gcc13-cstdint.patch` — `build --cxxopt=-include --cxxopt=cstdint`: TF 2.11's `tensorflow/lite/kernels/internal/spectrogram.cc` uses `uint32_t` without including `<cstdint>`, which GCC 13 no longer tolerates.
- `0002-setup-py-version-wrapper-metadata.patch` — `setup.py`: distribution `speequal`, version `3.3.3.postN`, Google's `visqol` package mapped to `speequal.visqol` (`.model`, `.pb2` beneath it), PyPI metadata (readme, license, `python_requires`, `install_requires = numpy, protobuf`), a `Distribution.has_ext_modules` override so the wheel is tagged `cpXY-<platform>` instead of `py3-none-any`, and copying the wrapper (`python_wrapper/`) plus the two clean-speech WAVs into the package.
- `0003-workspace-pybind11_protobuf-getmessageclass.patch` — `WORKSPACE` applies a one-hunk patch to the pinned `pybind11_protobuf` (2022): protobuf ≥ 5 removed `MessageFactory.GetPrototype`, which made every proto conversion silently fail; use `message_factory.GetMessageClass` when it exists.
- `0004-manifest-in-sdist.patch` — `MANIFEST.in` so the sdist carries the whole Bazel tree (`src/`, `python/`, `model/`, `WORKSPACE`, `BUILD`, `.bazelrc`, `.bazelversion`, the patches) and never a previous build's `bazel-bin`.

Added on top (not patches): the `speequal` package from `src/speequal/` (`__init__.py`, `visqol/{__init__,api,__main__,selftest}.py`, `visqol/conformance.json`).
