soulsync/docs/superpowers/specs/2026-06-14-global-quality-system-design.md
dev 97242cbd8d docs: global quality system design spec + Monochrome 30s bug note
Design for source-binding + quality-aware fall-through ranking
(per-source population, source-priority-king), ranked-targets UI,
quarantine-reason surfacing, and tests. Locks the constraints that
quality quarantine reuses the trigger='quality' retry path and never
sets force_imported (reserved for AcoustID mismatches).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 12:15:56 +02:00

180 lines
8.6 KiB
Markdown

# Global Quality System — Source Binding, Ranking & UI
**Branch:** `feature/global-quality-system`
**Date:** 2026-06-14
**Status:** Approved design — ready for implementation plan
## Problem
The quality model (`core/quality/model.py`) is complete and Soulseek already
uses it, but the system has no cross-cutting quality behaviour:
1. **Streaming sources don't populate real quality.** `SearchResult.audio_quality`
is a derived property over `format/bitrate/sample_rate/bit_depth`, but Tidal,
HiFi, Deezer, Qobuz, Amazon, YouTube never fill `sample_rate`/`bit_depth` (and
sometimes not even the right `format`). Their `audio_quality` therefore falls
back to crude kbps heuristics.
2. **Ranking is inconsistent.** Only the Soulseek path runs
`filter_results_by_quality_preference`. Streaming results are ordered by
match-confidence only — quality is ignored.
3. **No quality-aware source fall-through.** `search_with_fallback` is
"first non-empty source wins": it never escalates to the next source when the
current source can't deliver the wanted quality.
4. **No UI for the v3 ranked-target list.** The profile model is v3 (ordered
target list) but there is no editor for it.
## Decisions (locked)
- **Per-Source Population, not cross-source pooling.** Source priority (the
hybrid chain order) stays king. Each source populates an accurate
`audio_quality`; the chain fall-through becomes quality-aware so a source that
cannot meet *any* target is skipped in favour of the next source.
- **Full scope:** source mappers (A) + ranking wiring + streaming-path unify,
UI ranked-target editor (B), quarantine-reason surfacing (C), tests (D).
- **Bitrate is a settable minimum threshold (a range "≥ X"), never an exact
match.** Lossless (FLAC/WAV) is matched on `bit_depth`/`sample_rate`; bitrate is
only a fallback heuristic when those are absent. This is already how
`AudioQuality.matches_target` behaves — the work is to expose it correctly in
the UI and add a small VBR tolerance for lossy presets.
## Hard constraints (must not regress)
These are already correct in the codebase and the new work must preserve them:
- **Retry harmony.** A quality reject already flows through the same retry path as
AcoustID: `check_quality_target``move_to_quarantine(trigger='quality')`
`requeue_quarantined_task_for_retry(..., 'quality')` (pipeline.py:584-620). The
worker walks the next-best candidate using the per-source retry budget. New code
must keep `check_quality_target` returning a reason string that feeds this path.
- **force_import isolation.** `force_imported` status is set ONLY by the AcoustID
version-mismatch fallback (`core/imports/version_mismatch_fallback.py`). The
quality guard sets NO verification status — it quarantines with
`trigger='quality'` and the bypass flag `_skip_quarantine_check='quality'`. A
quality mismatch must NEVER become `force_imported`. force_import stays reserved
for AcoustID mismatches.
## A — Source mappers
New module **`core/quality/source_map.py`** centralises each source's tier
knowledge (kept out of `model.py` to avoid bloat). Each download client populates
the four quality fields (`quality`, `bitrate`, `sample_rate`, `bit_depth`) of its
`TrackResult` via these helpers; `audio_quality` then derives automatically.
Each tier value is a **claim**, verified post-download by `check_quality_target`
reading the real file. Ranking uses the claim to pick; the guard catches lies.
| Source | Source of values | Mapping |
|--------|------------------|---------|
| **Soulseek** | slskd attrs type 4 (sample_rate) / 5 (bit_depth) — real | already done (`AudioQuality.from_slskd_file`) |
| **Qobuz** | API `maximum_sampling_rate` (kHz) + `maximum_bit_depth` — real | `sample_rate = rate*1000`, `bit_depth`, `format='flac'` |
| **Deezer** | config code `flac`/`mp3_320`/`mp3_128` | flac→16-bit/44.1kHz · mp3_320→320kbps · mp3_128→128kbps |
| **Tidal** | track `audioQuality` tier | HI_RES_LOSSLESS/HI_RES→flac 24/96 · LOSSLESS→flac 16/44.1 · HIGH→aac 320 · LOW→aac 96 |
| **HiFi / Monochrome** | Tidal-backed; config quality key | same map as Tidal |
| **Amazon** | real `sampleRate` when present, else tier | UHD→24/96 · HD→16/44.1; prefer real sampleRate |
| **YouTube / SoundCloud** | yt-dlp / stream `format` + `abr` | lossy: format + bitrate, no bit_depth |
Helper shape:
```python
# core/quality/source_map.py
TIDAL_TIER_MAP: dict[str, AudioQuality]
AMAZON_TIER_MAP: dict[str, AudioQuality]
def quality_from_tidal_tier(tier: str) -> AudioQuality: ...
def quality_from_qobuz(sampling_rate_khz: float, bit_depth: int) -> AudioQuality: ...
def quality_from_deezer(code: str) -> AudioQuality: ...
def quality_from_amazon(stream_info: dict) -> AudioQuality: ...
```
Clients copy the result onto the `TrackResult` fields (small helper
`TrackResult.set_quality(aq)` to avoid four-line repetition at each call site).
## Wiring — quality-aware fall-through
New helper **`core/quality/selection.py`**:
```python
def rank_for_profile(candidates) -> tuple[list, bool]:
"""Return (ranked_candidates, satisfied_a_target).
satisfied = filter_and_rank(fallback_enabled=False) produced anything.
"""
```
`search_with_fallback` (`core/download_engine/engine.py`) changes from
"first non-empty wins" to:
```
best_fallback = []
for source in chain:
tracks, albums = source.search(query)
if not tracks: continue
ranked, satisfied = rank_for_profile(tracks)
if satisfied:
return ranked, albums # this source meets a target → done
best_fallback = best_fallback or (ranked, albums)
# chain exhausted, nothing satisfied a target
return best_fallback if fallback_enabled else ([], [])
```
**Key behavioural note:** with source-priority-king plus a long target list
(down to MP3 192), fall-through only triggers when a source meets *no* target at
all (below the floor) — e.g. user wants only FLAC and a source has only MP3.
Otherwise the first source that returns acceptable results wins, exactly as today
but now quality-ranked within.
## Streaming-path unification
In `download_orchestrator.search_and_download_best`, both paths converge on:
**match-filter first (right track), then quality-rank (best version).** The
streaming branch keeps its confidence filter (≥0.55) and then applies
`rank_for_profile` to the survivors; the Soulseek branch keeps its quality ranking
and is unchanged in spirit. No path is left quality-blind.
## B — Ranked-targets UI
A draggable, ordered list in the quality settings panel:
- Each row: drag handle, label, format, the relevant constraint
(bit_depth + min_sample_rate for lossless; **min_bitrate as a settable "≥ X
kbps" field** for lossy), delete button.
- "Add target" control with format/bit_depth/sample_rate/bitrate inputs.
- `fallback_enabled` toggle.
- Persists via the existing `GET/POST /api/quality-profile` (already v3-shaped).
Bitrate field is explicitly a **minimum threshold**, defaulting to small VBR
headroom for the common presets (e.g. a "320" preset stores `min_bitrate≈315` so
VBR/mono files near 320 still match). Lossless rows hide the bitrate field since
they match on bit_depth/sample_rate.
## C — Quarantine reason
Mostly wiring — the UI already carries `quarantineReason` dataset and an approve
flow (`webui/static/downloads.js`), and `check_quality_target` already returns a
"file is X, wanted Y" string.
- Ensure the quality rejection reason is threaded into the quarantine record's
reason field and rendered in the track-detail modal (what was wanted vs what the
file actually is).
- "Approve anyway" sets `_skip_quarantine_check='quality'` (the bypass already
honoured at pipeline.py:584-585).
## D — Tests
- `tests/quality/test_source_map.py` — each mapper produces the expected
`AudioQuality` (Tidal tiers, Qobuz kHz→Hz, Deezer codes, Amazon real-vs-tier,
lossy no bit_depth).
- `tests/quality/test_selection.py``rank_for_profile` satisfied/unsatisfied;
fall-through: source A below floor → engine escalates to source B; nothing
matches with `fallback_enabled` on (best returned) vs off (empty).
- Extend `tests/quality/test_model.py``matches_target` bitrate-as-minimum,
FLAC matched on bit_depth/sample_rate not bitrate, v2→v3 migration.
- `tests/imports/test_quality_guard.py``check_quality_target` quarantine
reason scenarios **plus a regression test asserting a quality mismatch uses
`trigger='quality'` and never sets `force_imported`.**
## Out of scope
- Cross-source candidate pooling (rejected in favour of per-source population).
- The Monochrome/HiFi 30-second-silence bug (tracked separately in `PLAN.md`;
needs ffmpeg `silencedetect`, not tier mapping).