From 613ae18e87cdebf82932fb4a2968cdbebca280cb Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:51:30 -0500 Subject: [PATCH] cover logo: fix the two ways artists silently lost their wordmark Both regressions were invisible because the pipeline's failure mode is a shrug: no logo, no error, fall back to plain text. 1. The dominance guard was resolution-dependent. It bins the crop into a coarse 16-cubed RGB histogram and rejects when the dominant colour holds under half the pixels ("text over a photo"). A BIGGER source resolves more grain and JPEG texture, which spreads pixels across bins and shrinks that share -- so the identical crop of Alvvays' "Blue Rev" measures dominance 0.746 at 1000px and 0.226 at 3000px. Once the upgrade path started supplying hi-res artwork, covers that had segmented cleanly for months began failing a guard that had not changed; its input grew. The histogram and guard now read a copy normalised to the 1000px they were calibrated at, while the alpha ramp still runs at full resolution -- which is the whole reason for fetching the larger cover. 2. Only the top 14 covers were ever offered. The rank puts albums first and penalises remix/mix/edit/live titles, but a prolific artist's wordmark is often on exactly those: Poolside has 74 releases and SEVEN covers that OCR reads as "POOLSIDE", every one of them outside a 14-cover window. The cap goes to 40 on both sides (the frontend list must not undercut the backend's, since whatever it trims the extractor can never see). This is free for artists that hit early -- the locate loop stops at COVER_LOGO_MAX_HITS -- and is paid once, then cached, by artists with no wordmark at all. Recovered: Alvvays (nothing -> 871x173), Poolside (nothing -> 528x225), Men I Trust, and Motor City Drum Ensemble now extracts too. Verified no regression across the rest of the research roster: Camping in Alaska, Crazy P, Folamour, Hot Toddy, Mildlife, Session Victim, Turnover, Knocked Loose, Snail Mail, Spiritbox and Todd Terje all still extract at their previous sizes. --- src-tauri/src/commands.rs | 11 ++++++++++- src-tauri/src/cover_logo.rs | 35 ++++++++++++++++++++++++++++++++--- src/lib/state.svelte.js | 7 ++++++- 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 5b200ab..0403b54 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1545,7 +1545,16 @@ pub async fn catalog_artist_socials( // v4: the alpha ramp now scales to the measured ink distance, so every cached // mask predates the anti-aliasing this pipeline relies on and must be redrawn. const COVER_LOGO_RECIPE: &str = "cover-logo-v4-ink-scaled-alpha"; -const COVER_LOGO_MAX_COVERS: usize = 14; +// A prolific artist's wordmark is often nowhere near the top of this list. The +// rank puts albums first and penalises `remix|mix|edit|live` titles — but for +// Poolside (74 releases) every one of the SEVEN covers that OCR reads as +// "POOLSIDE" is a remix single, so all seven sat outside a 14-cover window and +// the artist silently lost its logo. Raising the cap costs nothing for artists +// that hit early: the locate loop stops as soon as COVER_LOGO_MAX_HITS is +// reached, so the extra covers are only ever fetched when the early ones miss. +// The price is paid by artists with no wordmark anywhere, who now OCR more +// covers before giving up — once, then it is cached. +const COVER_LOGO_MAX_COVERS: usize = 40; const COVER_LOGO_MAX_HITS: usize = 5; const COVER_LOGO_LOCATE_WINDOW: usize = 4; const COVER_LOGO_UPGRADE_WINDOW: usize = 4; diff --git a/src-tauri/src/cover_logo.rs b/src-tauri/src/cover_logo.rs index 8b63fc2..2c06727 100644 --- a/src-tauri/src/cover_logo.rs +++ b/src-tauri/src/cover_logo.rs @@ -45,6 +45,10 @@ fn normalize(s: &str) -> String { // Segmentation tuning (RGB distance space, 0..~441). const BG_NEAR_DIST: f32 = 46.0; // a pixel this close to the dominant colour counts as background const BG_MIN_DOMINANCE: f32 = 0.5; // background must fill at least this share of the crop, else it's a photo → reject +/// Cover long edge the background histogram and dominance guard are calibrated +/// at. Larger sources are normalised down for that test only — see the note at +/// the guard for why its verdict is otherwise resolution-dependent. +const DOMINANCE_REF_COVER: f32 = 1000.0; const ALPHA_LO: f32 = 46.0; // distance-from-bg below which a pixel is background (transparent) const ALPHA_HI: f32 = 120.0; // floor for the opaque end of the ramp (low-contrast marks) /// Share of the measured ink-vs-background distance the alpha ramp spans. The @@ -123,11 +127,36 @@ pub(crate) fn extract_wordmark( let crop = image.crop_imm(x0, y0, x1 - x0, y1 - y0).to_rgba8(); let (w, h) = crop.dimensions(); + // Both the histogram and the dominance guard read a copy normalised to the + // cover resolution they were tuned at. + // + // They are scale-sensitive in opposite directions to intuition: a coarse 16³ + // histogram over a BIGGER source resolves more film grain and JPEG texture, + // which spreads pixels across bins and shrinks the dominant colour's share. + // Alvvays' "Blue Rev" measures dominance 0.746 at 1000px and 0.226 at 3000px + // for the identical crop — so once the extractor started receiving hi-res + // artwork, covers that had segmented cleanly for months began failing the + // 0.5 guard as "text over a photo". The guard did not change; its input grew. + // + // Normalising here keeps the verdict identical whatever resolution the + // upgrade path supplies, while the alpha ramp below still runs at FULL + // resolution, which is the whole point of fetching the larger cover. + let guard_scale = DOMINANCE_REF_COVER / (iw.max(ih) as f32); + let guard_copy = (guard_scale < 1.0).then(|| { + image::imageops::resize( + &crop, + ((w as f32 * guard_scale).round() as u32).max(1), + ((h as f32 * guard_scale).round() as u32).max(1), + image::imageops::FilterType::Triangle, + ) + }); + let guard = guard_copy.as_ref().unwrap_or(&crop); + // Background = the dominant colour, via a coarse 16³ RGB histogram over the // whole crop. Robust to sparse texture (faint line-art on black) in a way a // ring mean is not — the text is a minority, so the mode is the background. let mut hist = [0u32; 16 * 16 * 16]; - for p in crop.pixels() { + for p in guard.pixels() { let (r, g, b) = (p.0[0] >> 4, p.0[1] >> 4, p.0[2] >> 4); hist[(r as usize) * 256 + (g as usize) * 16 + b as usize] += 1; } @@ -145,8 +174,8 @@ pub(crate) fn extract_wordmark( // Dominance guard: what share of the crop is near that background colour? // A clean wordmark has a dominant background (text is the minority); a photo // has no single dominant colour, so this share is low → reject. - let total = (w * h) as f32; - let near = crop + let total = (guard.width() * guard.height()) as f32; + let near = guard .pixels() .filter(|p| dist([p.0[0] as f32, p.0[1] as f32, p.0[2] as f32], bg) < BG_NEAR_DIST) .count() as f32; diff --git a/src/lib/state.svelte.js b/src/lib/state.svelte.js index 6621b74..950fb9b 100644 --- a/src/lib/state.svelte.js +++ b/src/lib/state.svelte.js @@ -1285,12 +1285,17 @@ function albumLogoRank(a) { return typeRank + noisy; } +// Must not undercut the backend's own cap (COVER_LOGO_MAX_COVERS): whatever is +// trimmed here the extractor can never see. A prolific artist's wordmark is +// frequently on a low-ranked remix single — all seven of Poolside's readable +// covers sat past position 14 — and the backend stops early once it has enough +// hits, so a longer list costs nothing for artists whose top covers work. function coverLogoCandidates(page) { return [...(page.albums ?? [])] .sort((a, b) => albumLogoRank(a) - albumLogoRank(b)) .map((a) => ({ title: a.title ?? "", url: a.cover })) .filter((c) => c.url) - .slice(0, 14); + .slice(0, 40); } function coverLogoInputKey(artistId, page, covers) {