hero search: stop reading " x " as a co-bill, and reject explicit results

Two follow-ups to reading both Bing buckets.

The multi-artist guard treated " x " as a co-bill between two acts. In practice
it far more often joins a venue and a festival: "Nikki Nair | Boiler Room x
Bass Coast 2022". Because every YouTube thumbnail is named maxresdefault.jpg,
image_names_artist is structurally false for that whole class, so the rule was
rejecting Boiler Room and festival set photography -- the richest source of
hero-shaped images that exists for DJs. Genuine collaboration graphics are
still caught by named_collaboration, which requires the image itself to name
the other subject, so nothing is lost. Nikki Nair's multi= counter drops from 1
to 0 with his identity accepts intact.

And explicit results are now rejected before the identity gate. Reading the
unfiltered bucket is necessary for coverage but it puts pornography in the
candidate pool: one artist's unfiltered wave returned 24 hardcore cards. An
audit found zero of ~67 such cards could clear bing_identity_confidence,
because porn pages do not name obscure musicians -- but that is an empirical
accident rather than a guarantee, and reading only the strict bucket used to
provide an independent second layer. This restores one. The vocabulary is
deliberately narrow so "adult contemporary", XXX as a roman numeral, and band
names like Nude Beach or Pornography stay eligible.

Canary unchanged: Fishmans still accepted=12 with the same three banners.
This commit is contained in:
KeelrLeek committed 2026-07-25 16:25:17 -05:00
1 parent 8b2aadce2b
commit 9536ff23a9
1 file changed
+70 -1
+70 -1
View File
@@ -1985,6 +1985,13 @@ impl MetadataClient {
}
let title = result.title;
let purl = result.page_url;
// Before identity, not after: reading the unfiltered bucket
// puts explicit results in the pool, and the identity gate
// is a relevance test with no notion of decency.
if is_adult_result(&title, &purl, &murl) {
rejected_unsuitable += 1;
continue;
}
let Some(identity) = bing_identity_confidence(&title, &purl, &murl, artist)
else {
rejected_identity += 1;
@@ -8007,7 +8014,16 @@ fn multi_artist_result_mismatches_image(title: &str, image_url: &str, artist: &s
|| listing_text.contains(" featuring ")
|| listing_text.contains(" vs. ")
|| listing_text.contains(" versus ")
|| listing_text.contains(" x ")
// `" x "` is deliberately NOT here. It reads as a co-bill between two
// acts, but in practice it far more often joins a venue and a festival
// or a brand: "Nikki Nair | Boiler Room x Bass Coast 2022". Because
// every YouTube thumbnail is named `maxresdefault.jpg`,
// `image_names_artist` is structurally false for the whole class, so
// this one separator was rejecting every Boiler Room and festival set
// photo -- the richest source of hero-shaped photography that exists
// for DJs, and the only 1280x720 candidate Nikki Nair had. A genuine
// collaboration graphic is still caught below by `named_collaboration`,
// which requires the image itself to name the other subject.
|| two_act_conjunction
|| ([" y ", " con ", " junto a ", " et ", " und "]
.iter()
@@ -8053,6 +8069,59 @@ fn multi_artist_result_mismatches_image(title: &str, image_url: &str, artist: &s
separator_count >= 2 && !image_names_artist
}
/// Explicit-content rejection, applied BEFORE the identity gate.
///
/// The hero search reads Bing's unfiltered bucket as well as the strict one,
/// because either can be serving another query's results. That is necessary for
/// coverage, but it means pornography now enters the candidate pool: a direct
/// read of one artist's unfiltered wave returned 24 hardcore cards. An audit
/// found zero of ~67 such cards could clear `bing_identity_confidence` -- porn
/// pages do not name obscure musicians -- but that is an empirical accident,
/// not a guarantee, and it is one gate deep. This is the independent second
/// layer that reading only the strict bucket used to provide.
fn is_adult_result(title: &str, page_url: &str, image_url: &str) -> bool {
let urls = format!("{page_url} {image_url}").to_lowercase();
const ADULT_HOSTS: [&str; 16] = [
"pornhub.",
"xhamster.",
"xhcdn.com",
"xnxx.",
"xvideos.",
"eporner.",
"redtube.",
"youporn.",
"spankbang.",
"pictoa.",
"thefappening",
"onlyfans.",
"fapello.",
"hentai-foundry.",
"rule34.",
"e-hentai.",
];
if ADULT_HOSTS.iter().any(|host| urls.contains(host)) {
return true;
}
// Title vocabulary, kept to terms that do not appear in music writing.
// "Adult contemporary", "XXX" as a roman numeral and band names such as
// Nude Beach or Pornography (The Cure) must NOT trip this, so match only
// unambiguous phrases.
let haystack = format!(" {} ", title.to_lowercase());
const ADULT_PHRASES: [&str; 10] = [
" porn ",
" porno ",
"hardcore porn",
" nsfw ",
" blowjob",
" creampie",
" anal sex",
" sex tape",
"nude leaks",
" milf ",
];
ADULT_PHRASES.iter().any(|term| haystack.contains(term))
}
/// True for Bing results that are unambiguously merchandise, a physical music
/// format, or a shop listing rather than a photograph of the performer. Keep
/// this conservative: page/image URL structure is the strongest signal, while