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

8.6 KiB

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_targetmove_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:

# 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:

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.pyrank_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.pymatches_target bitrate-as-minimum, FLAC matched on bit_depth/sample_rate not bitrate, v2→v3 migration.
  • tests/imports/test_quality_guard.pycheck_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).