-
threshold_for — Return the per-budget filter threshold in
[0, 1]. -
max_results_for — Return the per-budget cap on volumes/series to expand into issue queries.
-
should_keep_volume_name — Decide whether a discovered volume is worth a per-volume issue lookup.
comicbox.formats.base.online.series_filter¶
source module comicbox.formats.base.online.series_filter
Pre-call series-name fuzzy filter for online sources.
Each source’s volume/series discovery step returns up to N hits for a free-text series query. CV’s full-text search will return “Lois Lane: A Celebration” and “Lois Lane and Friends” alongside the canonical “Lois Lane (2019)” — and the matcher’s s_series signal would score the non-matches near zero anyway. Issuing the per-volume list_issues API call for those non-matches is wasted budget under CV’s 200/hr cap.
This module provides the pre-call filter both sources consult before deciding to spend an API call on a discovered volume. The same normalize_series from signals.py is reused so a candidate that passes the filter scores predictably under s_series downstream.
Why fuzz.ratio and not fuzz.WRatio (which s_series uses): WRatio applies a partial-match boost that lifts any substring overlap to 90+, including “Lois Lane” vs “Adventures of Lois Lane and Friends” (both score 90). That’s fine for scoring — the matcher’s downstream signals can still rank candidates — but useless for filtering, where we need to drop substring-overlapping-but-different volumes before issuing an API call for them. Plain ratio (Levenshtein) gives:
Lois Lane vs Lois Lane (1986) → 78
Lois Lane vs Watchmen (1986) → variant year tail OK
Lois Lane vs Watchmen Annotated → 62
Lois Lane vs Adventures of Lois Lane ... → 41
A natural threshold around 0.70 separates “same series, possibly with a year/volume suffix” (keep) from “different series that happens to share words” (drop). The pre-filter is intentionally STRICTER than the matcher’s s_series; the matcher gets to be permissive because it sees the full candidate. The pre-filter has to decide before any API call goes out.
Driven by the Effort resolved per-source. See tasks/online-tagging/06-api-budget-spec.md for the threshold rationale (Phase B picks the real numbers; Phase A ships with the lever dormant).
Functions
source threshold_for(budget: Effort) → float
Return the per-budget filter threshold in [0, 1].
source max_results_for(budget: Effort, *, default: int) → int
Return the per-budget cap on volumes/series to expand into issue queries.
default is the source’s class-level production cap (20 today). Returns the per-budget override when set; otherwise the default.
source should_keep_volume_name(profile_series: str | None, volume_name: str | None, threshold: float) → bool
Decide whether a discovered volume is worth a per-volume issue lookup.
Returns True (keep, spend the API call) when either:
threshold <= 0— the filter is configured as no-op for the resolved API budget. This isBALANCEDuntil Phase B says otherwise.profile_seriesis missing — we have no comparison anchor; never drop on missing data (would risk false-negatives for tagged-without-series fixtures).volume_nameis missing — likewise, can’t compare; keep.- The rapidfuzz
ratiosimilarity between the two normalized names is>= threshold * 100. Same normalization ass_series(so naming conventions agree), different primitive (so we filter substring noise the matcher would still score high). See module docstring for rationale.
Returns False (skip, save the call) otherwise. The caller is expected to log a debug-level reason when this happens so calibration runs can audit pre-filter false-negatives.