0.108.11: Ring X correction included (built, not installed)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-05 16:17:07 +00:00
co-authored by Claude Opus 5.5
parent 4f4bfd3138
commit 80a37131ab
10 changed files with 6944 additions and 6 deletions
@@ -64,7 +64,8 @@ internal val Briefs: List<Brief> = listOf(
Brief(309, "BritX", setOf("Movie"), setOf("Crime", "Comedy", "Drama", "Thriller"),
themes = w("gangster", "gang", "criminal", "crime", "heist", "drug", "hooligan", "council estate", "thug", "con", "robbery", "dealer"),
against = w("documentary"), era = 1965 to 2026, uk = true, current = true, needTheme = true),
Brief(401, "Ring X", setOf("Movie", "Series"), setOf("Sport", "Action", "Documentary"), themes = w("wrestl", "WWE", "AEW", "TNA", "UFC", "MMA", "mixed martial", "title match", "pay-per-view", "bout"),
Brief(401, "Ring X", setOf("Movie", "Series"), setOf("Sport", "Action", "Documentary"), themes = w("wrestl", "WWE", "AEW", "TNA", "NJPW", "ROH", "Impact Wrestling", "NXT", "SmackDown", "WrestleMania", "Royal Rumble", "SummerSlam",
"Survivor Series", "Money in the Bank", "Bound for Glory", "WrestleDream", "title match", "pay-per-view"),
against = w("romance"), era = 1980 to 2026, current = true, needTheme = true),
Brief(601, "Vortex", setOf("Series", "Movie"), setOf("Animation", "Action", "Anime"), setOf("Animation", "Anime"),
w("anime", "fight", "power", "warrior", "battle", "hero", "martial", "saiyan", "ninja", "mecha", "demon"), w("preschool", "sitcom"), 1980 to 2026, needTheme = true),
@@ -91,6 +92,9 @@ private val EnglishCountries = Regex("United States|United Kingdom|Canada|Austra
private val ForeignBoard = Regex("^(SG|BR|PL|KR|JP|DE|FR|IT|ES|NL|TR|IN|MX|CN|HK|TW|TH|PH|MY|ID|RU|SE|NO|DK|FI|AR|CL|CO|PT|CZ|HU|RO|GR|IL|EG|SA|AE|VN)-", RegexOption.IGNORE_CASE)
/** Mainstream audience ratings: a horror-comedy rated these (or for children) keeps its usual channels. */
private val MainstreamRatings = setOf("PG-13", "12", "12A", "TV-14", "GB-12", "GB-12A", "GB-15", "15")
/** Paul, 5 November 2026: Ring X is professional wrestling. A verified promotion, by studio, counts for it; MMA events go on no channel. */
private val WrestlingPromotion = Regex("\\b(WWE|World Wrestling|All Elite Wrestling|AEW|TNA|Total Nonstop|Impact Wrestling|New Japan|NJPW|Ring of Honor|ROH|NWA|WCW|ECW|Lucha|Stardom|Game Changer Wrestling)\\b", RegexOption.IGNORE_CASE)
private val MmaEvent = Regex("\\b(UFC|MMA|mixed martial|Bellator|PFL|Ultimate Fighting|cage (fight|bout)|octagon)", RegexOption.IGNORE_CASE)
private val HorrorComedy = Regex("\\b(body horror|body-horror|gross-out|splatter|horror-comedy|horror comedy)", RegexOption.IGNORE_CASE)
private val LiveSpecial = Regex("\\b(stand-up|standup|comedian|comedy special|unruly set|in this set|live from|in concert|tour film|takes aim at)\\b", RegexOption.IGNORE_CASE)
private val EventFilmWords = Regex("superhero|epic|franchise|saga|blockbuster", RegexOption.IGNORE_CASE)
@@ -179,6 +183,8 @@ internal fun channelScore(e: TitleEvidence, b: Brief, text: ChannelText, links:
val ev = mutableListOf<String>(); val g = e.genreSet; val ov = e.overview.orEmpty(); val rating = e.rating.orEmpty(); val ch = b.number
val all = ov + " " + e.name + " " + e.taglines.orEmpty().joinToString(" ")
if (e.type !in b.kinds) return 0.0 to listOf("wrong kind")
val withStudios = listOf(e.name, ov, e.studios.orEmpty().joinToString(" ")).joinToString(" ")
if (MmaEvent.containsMatchIn(withStudios) && !WrestlingPromotion.containsMatchIn(withStudios)) return 0.0 to listOf("an MMA event: Ring X is professional wrestling, and no channel takes MMA events")
if (b.kids && rating in Adult) return 0.0 to listOf("rated $rating")
var s = 0.0
val animated = "Animation" in g; val doc = "Documentary" in g
@@ -223,6 +229,7 @@ internal fun channelScore(e: TitleEvidence, b: Brief, text: ChannelText, links:
}
if (b.kids && rating.isNotEmpty() && rating !in KidsOk) { s *= .5; ev += "rated $rating" }
if (b.kids && rating.isEmpty()) { s *= .8; ev += "no rating to confirm it suits children" }
if (ch == 401 && WrestlingPromotion.containsMatchIn(e.studios.orEmpty().joinToString(" "))) { s += .15; ev += "a verified wrestling promotion" }
if ("Horror" in g && (ch == 304 || ch == 305)) { s += .12; ev += "a horror film" }
if ("Horror" in g && ch in setOf(308, 307, 612, 613)) { s *= .5; ev += "horror is not for this channel" }
if (horrorComedy && ch == 305) { s += .15; ev += "a horror-comedy, After Dark's cult territory" }
@@ -31,6 +31,19 @@ class ChairLearningTest {
assertNull(lessons.holdBackFor(401, wrestling, setOf(lesson.key)))
}
@Test fun ringXIsWrestlingAndMmaEventsHaveNoChannel() {
// Paul's correction, 5 November 2026.
val text = ChannelText(emptyMap(), mapOf("u" to ufc))
assertTrue(Briefs.all { channelScore(ufc, it, text).first == 0.0 })
assertTrue(channelScore(ufc, Briefs.first { it.number == 401 }, text).second.single().contains("MMA event"))
// A strawweight bout isn't WWE: the promotion is matched as a whole word.
val straw = ev("s", "UFC Fight Night 289", listOf("Action"), "A women's strawweight final, a mixed martial arts event.")
assertEquals(0.0, channelScore(straw, Briefs.first { it.number == 401 }, text).first, 0.0)
val wwe = ev("w", "Survivor Series: WarGames", listOf("Action", "Sport"), "Survivor Series: the WarGames pay-per-view.", listOf("WWE"))
val ringX = channelScore(wwe, Briefs.first { it.number == 401 }, ChannelText(emptyMap(), mapOf("w" to wwe)))
assertTrue(ringX.first > .45); assertTrue(ringX.second.any { "verified wrestling promotion" in it })
}
@Test fun aBroadGenreNeedsThreeAndASpecificReasonWins() {
val e = mapOf("1" to ev("1", "Wedding One", listOf("Romance", "Musical")), "2" to ev("2", "Wedding Two", listOf("Romance", "Musical")), "3" to ev("3", "Wedding Three", listOf("Romance", "Musical")))
assertTrue(chairLessons(listOf(dropped("1", "Wedding One", 307), dropped("2", "Wedding Two", 307)), e).lessons.isEmpty()) // two by genre: not enough
@@ -44,11 +57,11 @@ class ChairLearningTest {
val line = { id: String, name: String, ov: String -> JSONObject().put("id", id).put("type", "Movie").put("name", name).put("year", 2026)
.put("overview", ov).put("genres", JSONArray(listOf("Action", "Sport"))).put("studios", JSONObject.NULL)
.put("rating", JSONObject.NULL).put("countries", JSONArray(listOf("United States"))).put("taglines", JSONObject.NULL) }
val evidence = File.createTempFile("evidence", ".jsonl").apply { deleteOnExit(); writeText(listOf(line("1", "UFC 330", "A UFC mixed martial arts pay-per-view event with a title bout."),
line("2", "UFC 331", "A UFC mixed martial arts pay-per-view event with a title bout."), line("9", "UFC 334", "A UFC mixed martial arts pay-per-view event with a title bout.")).joinToString("") { "$it\n" }) }
val evidence = File.createTempFile("evidence", ".jsonl").apply { deleteOnExit(); writeText(listOf(line("1", "Fight Night 1", "A boxing pay-per-view title match between two heavyweight champions."),
line("2", "Fight Night 2", "A boxing pay-per-view title match between two heavyweight champions."), line("9", "Fight Night 9", "A boxing pay-per-view title match between two heavyweight champions.")).joinToString("") { "$it\n" }) }
val sets = File.createTempFile("collections", ".jsonl").apply { delete(); deleteOnExit() }
val history = listOf(dropped("1", "UFC 330", 401), dropped("2", "UFC 331", 401))
fun report(state: LearningState) = buildDeskReport(evidence, sets, emptyMap(), listOf(Arrival("9", "Movie", "UFC 334", 2026, "2026-10-30", "tmdb:9", 0)),
val history = listOf(dropped("1", "Fight Night 1", 401), dropped("2", "Fight Night 2", 401))
fun report(state: LearningState) = buildDeskReport(evidence, sets, emptyMap(), listOf(Arrival("9", "Movie", "Fight Night 9", 2026, "2026-10-30", "tmdb:9", 0)),
emptySet(), emptyMap(), 1, true, DirectorMode.Balanced, 1_793_800_000_000, decided = setOf("1", "2"), history = history, learning = state)
fun shown(r: DeskReport) = (r.picks.map { it.id } + r.asks.map { it.id }).contains("9")
val proposed = report(LearningState(on = true))
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"8009312": [], "8078448": ["Dark Files"], "7998858": [], "8002849": [], "7982220": ["Orbit Romance"], "8060672": [], "8005900": [], "7981213": ["Dark Files"], "8063723": ["Orbit Horror", "After Dark"], "8036706": [], "8063799": ["Orbit Romance", "BritX"], "8065673": [], "8070609": [], "8056325": [], "8065631": ["", "Orbit Premiere"], "8054668": [], "7993465": [], "8002696": [], "7999478": ["", "PixelZ"], "7998882": [], "8014652": ["", "Orbit 2"], "7991575": ["Orbit Romance"], "8057254": [], "8002507": [], "8008372": [], "8067827": [], "8068916": [], "8064652": [], "8000477": ["Orbit Action"], "8003324": [], "7978360": [], "7988199": ["Evidence"], "8059718": [], "8068177": [], "8068803": ["", "Orbit Action"], "8001802": [], "8067524": [], "8020593": ["Orbit Horror"], "8001326": ["", "Orbit 1"], "7980720": [], "8065517": [], "8055642": ["BritX"], "7998439": ["Orbit Horror", "After Dark"], "8006374": [], "8064561": [], "7991863": [], "8019916": [], "8059814": ["Orbit Romance"], "7998500": [], "8059092": ["", "Orbit Three", "Orbit 1"], "8068802": [], "7980716": ["Ring X"], "8069094": [], "7986681": [], "7993234": [], "8007968": ["Vortex"], "8006205": ["", "Vortex"], "8015874": [], "7999723": [], "8068581": ["Dark Files"]}
@@ -0,0 +1 @@
{"8061719": [], "7990654": [], "8014514": ["Vortex", ""], "8018748": [], "8054652": ["", "Orbit Premiere"], "8014763": ["Dark Files"], "7998267": [], "8076383": ["Dark Files"], "8002269": [], "8000996": ["", "Orbit Action", "Orbit Premiere"], "8064403": ["", "Orbit 2", "Orbit Three"], "8058519": ["", "Orbit Action"], "8014850": [], "8015663": [""], "8018840": ["Vortex"], "8002646": ["Evidence"], "8068419": [], "7985411": ["Vortex"], "8020592": ["Orbit Horror", "After Dark"], "8008678": [], "7993183": [], "8014260": ["Orbit Horror", "After Dark"], "8005458": ["", "Vortex"], "8062525": [], "7985729": ["Riot"], "8017065": ["Orbit Horror", "After Dark"], "7983711": ["Dark Files"], "7999018": ["", "Orbit Action"], "7980339": [], "8001615": [], "7988590": ["", "Riot", "Vortex"], "8020625": ["Paradox", ""], "8014564": [], "8005767": ["Orbit Three", "Paradox", ""], "8016391": [], "7986603": [], "8068826": ["After Dark", "Orbit Horror", ""], "7998219": [], "8059189": [], "8059883": [], "8001772": [], "8070303": [], "8057986": ["Paradox"], "8005522": ["", "Riot", "Vortex"], "8066994": ["Orbit Horror", "After Dark"], "8000537": ["Evidence"], "8059669": ["", "Rewind", "Vortex"], "8059757": [], "8070341": ["Terra"], "7996078": [], "7986970": ["", "Odyssey"], "8059396": [], "8017361": [], "8008734": ["Evidence"], "7995268": ["Orbit Action"], "8059121": [], "8001610": [], "7989166": [], "8019148": ["Paradox"], "8018213": ["Orbit Romance", "Orbit Premiere", ""]}
@@ -0,0 +1 @@
{"7997934": [], "7990343": [], "7997920": [], "8004097": [], "8006222": ["", "Vortex"], "8059706": [], "7992286": ["", "Orbit 2", "Orbit Wild"], "8063802": [], "8059688": [], "8070401": [], "8078255": ["Orbit Action"], "8060602": [], "7984233": ["Orbit Action"], "8069326": [], "8004110": [], "8057270": ["", "Orbit Three", "Paradox"], "8003572": [], "7992309": [], "7993894": ["", "Orbit 1", "Orbit Three"], "8015209": [], "8020676": ["Orbit Horror", "After Dark"], "8001473": [], "8003473": ["", "Paradox", "Orbit Junior"], "7997175": [], "7983690": [], "8064662": ["", "Orbit 1"], "8018565": ["", "Ring X", "Dark Files"], "8053190": [], "8057209": [], "7988409": ["", "Orbit Action"], "8063800": ["Orbit Horror", "Orbit Premiere", "Orbit Sci-Fi", "After Dark"], "7998091": ["", "Evidence"], "8053589": [], "8019919": [], "7984422": [], "8001782": ["Evidence"], "8064648": [], "7918893": [], "8036553": [], "8014136": ["", "Vortex"], "8069389": [], "8014328": ["", "Orbit Premiere", "Orbit Action", "Detour"], "8016310": [], "7997957": [], "8005689": ["", "Vortex"], "7985721": [], "8014863": ["After Dark", "Orbit Horror"], "8000540": [], "7999979": [], "8014370": [], "8057745": [], "8000875": [], "7895642": [], "8059925": ["Evidence"], "7986731": ["", "Evidence"], "8054463": [], "7997680": [], "8055180": [], "8063988": [], "8036794": []}
@@ -0,0 +1 @@
{"8019496": [], "8020674": [], "8032453": ["", "Orbit Action"], "8014886": ["", "Orbit Action"], "8005758": ["Dark Files"], "8070429": [], "8060115": ["", "Odyssey"], "7997994": ["Evidence"], "8061381": [], "8063798": [], "8070559": [], "7997791": [], "8060401": ["Wonder", "PixelZ"], "8005448": ["Vortex"], "8014088": ["Vortex"], "8020594": ["Orbit Horror", "After Dark"], "7985943": ["", "Orbit Wild"], "8014195": [], "8068447": ["Paradox"], "8019422": [], "8006592": [], "7996047": ["Orbit Horror", "After Dark"], "8014004": ["Dark Files"], "7998951": ["Orbit Action"], "8056349": [], "8014275": [], "8062599": [], "8008975": ["Paradox"], "7993266": [], "8001036": ["", "Vortex", "Rewind"], "7998036": [], "8006415": [], "8001551": [], "8005690": [], "7991705": [], "8061386": [], "7997825": ["Orbit 1"], "8070470": [], "8058485": [], "8060107": [], "8019013": ["Ring X"], "7986905": [], "8014336": ["", "Orbit Three", "Orbit 1", "Paradox"], "8017315": ["Terra"], "7994924": ["", "Orbit Sci-Fi"], "8055366": [], "8004050": [], "8026256": [], "8003283": [], "8018832": [], "7981186": [], "8018809": ["Ring X"], "8014525": [], "8019603": ["", "After Dark", "Orbit Horror"], "8016061": ["Evidence"], "8019883": ["", "Paradox"], "8066243": [], "8057252": [], "7985161": [], "8020570": []}
@@ -0,0 +1 @@
{"7992616": [], "8055740": ["Vortex", ""], "8017129": ["Dark Files"], "7994839": [], "7810527": ["", "Orbit Romance"], "8001330": [], "8005695": [], "8054732": ["Orbit Action", "Orbit Premiere"], "8068393": [], "8014272": [], "8062422": [], "8015848": [], "8002168": ["", "After Dark", "Orbit Horror"], "7994621": [], "8020569": [], "7994727": [], "8063747": ["", "Detour"], "8006397": ["", "Vortex"], "8055413": ["Ring X"], "8057405": [], "7996086": ["Orbit Romance"], "8020476": ["", "Orbit 1"], "8068548": [], "7989454": [], "8065773": ["", "Orbit Romance"], "8000853": [], "8016802": ["After Dark", "Orbit Horror"], "8018860": [], "8055247": [], "7990898": ["Orbit Action"], "8003286": [], "8036571": [], "8006051": [], "8014593": [], "7996098": ["", "Orbit 2"], "8007859": ["", "Dark Files"], "8058146": [], "8001871": [], "8008064": [], "8067816": [], "8068818": [], "8000824": [], "8068195": ["", "Evidence"], "7990789": [], "7991287": [], "8067869": [], "8060483": ["Dark Files", ""], "8003369": ["", "Orbit 2"], "8014831": [], "8058811": [], "8065674": ["", "After Dark", "Orbit Horror", "Orbit Sci-Fi"], "8067819": [], "8069941": [], "8058439": [], "8019411": ["Orbit Romance"], "7993206": ["Evidence"], "8014542": ["", "Orbit Action"], "8000114": ["Ring X"], "8059745": ["Dark Files"], "8064062": []}
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
import json,re,math,collections,sys,glob,sqlite3
S,R=sys.argv[1],sys.argv[2]
ev={}
for l in open(f'{S}/evidence-0.jsonl'):
r=json.loads(l); ev[r['id']]=r
c=sqlite3.connect(f'file:{S}/cat-now/catalogue.db?mode=ro',uri=True)
UK=re.compile(r'United Kingdom|England|Scotland|Wales|\bUK\b')
UKSTUDIO=re.compile(r'\bBBC\b|\bITV\b|Channel 4|Film4|Sky\b|Working Title|Ealing|Hammer|Aardman',re.I)
def W(*words): return re.compile(r'\b('+'|'.join(words)+r')',re.I)
# Each channel's brief from docs/channels and Paul's 4 November definitions: what it carries, themes for and against, era, ratings, origin.
B={
'101':dict(name='Orbit 1',kind={'Series'},genres_any={'Comedy'},themes=W('sitcom','family','household','neighbou?r','roommate','parents','dad','mum','mom','sisters','brothers','college','workplace'),against=W('murder','killer','documentary','wedding of','engagement'),era=(1970,2026),kids=False,need_theme=True),
'102':dict(name='Orbit 2',kind={'Series'},genres_any={'Game Show','Reality','Comedy','Talk'},themes=W('quiz','game show','contestants','panel','challenge','teams compete'),against=W('murder','documentary'),era=(1960,2015),kids=False,uk=True),
'103':dict(name='Orbit Three',kind={'Series'},genres_any={'Comedy'},themes=W('sketch','cult comedy','surreal','dark comedy','scottish'),against=W('documentary'),era=(1990,2026),kids=False,uk=True),
'104':dict(name='Orbit Gold',kind={'Series'},genres_any={'Comedy'},themes=W('sitcom','classic comedy'),against=W('documentary'),era=(1960,1999),kids=False,uk=True),
'105':dict(name='Riot',kind={'Series'},genres_any={'Animation','Comedy'},need_any={'Animation'},themes=W('adult','irreverent','raunchy','satir','dysfunctional'),against=W('preschool','children learn'),era=(1988,2026),kids=False),
'106':dict(name='Paradox',kind={'Series'},genres_any={'Science Fiction','Fantasy','Mystery','Horror'},themes=W('supernatural','paranormal','alien','time travel','parallel','ghost','vampire','demon','monster','mysterious','strange','unexplained','dimension','future','space','dystopia','cult','prophecy','witch'),against=W('documentary','reality','romance','cooking'),era=(1955,2026),kids=False),
'107':dict(name='Evidence',kind={'Series'},genres_any={'Crime','Mystery'},themes=W('detective','inspector','police','forensic','pathologist','homicide','murder','sergeant','DCI','sleuth','constable','cop','FBI','NCIS','agent','crime scene'),against=W('documentary','true story of','real-life','archive footage','interviews','lawyer','law firm','solicitor','attorney','barrister','courtroom','divorce','legal'),need_theme=True,era=(1965,2026),kids=False,scripted=True),
'201':dict(name='Terra',kind={'Series'},genres_any={'Documentary'},themes=W('wildlife','animal','ocean','sea','reef','planet','nature','ecosystem','species','forest','jungle','predator','habitat','natural world','migration'),against=W('murder','killer','crime','war','music','celebrity','royal','politic','AI','robot','superhero'),era=(1975,2026),kids=False,need_theme=True),
'202':dict(name='Odyssey',kind={'Series'},genres_any={'Documentary','History'},themes=W('space','universe','astronom','science','engineer','scientist','history of','historian','ancient','archaeolog','exploration','explorer','technolog','invention','journey across','civilisation','civilization','planet earth'),against=W('murder','killer','crime','celebrit','reality','athlete','boxer','comedian','sitcom','band','singer','fame','star of'),era=(1950,2026),kids=False,need_theme=True),
'203':dict(name='Orbit Wild',kind={'Series'},genres_any={'Reality','Documentary','Adventure'},themes=W('surviv','wilderness','expedition','endurance','remote','frontier','alone in','hunt','fishing','mountain','arctic','jungle','desert'),against=W('murder','dating','cooking','makeover','celebrit','villa','luxury','cash prize','contestants compete'),era=(1985,2026),kids=False,need_theme=True),
'204':dict(name='Dark Files',kind={'Series'},genres_any={'Documentary','Crime','Mystery'},need_any={'Documentary'},themes=W('murder','killer','disappear','missing','true crime','unsolved','investigat','case','paranormal','haunt','unexplained','cult','conspiracy','serial','detective','victim','trial'),against=W('wildlife','cooking'),era=(1975,2026),kids=False,need_theme=True),
'301':dict(name='Orbit Premiere',kind={'Movie'},genres_any={'Action','Adventure','Science Fiction','Fantasy'},themes=W('epic','hero','battle','world','mission','superhero','franchise'),against=W('documentary','concert'),era=(1975,2026),kids=False,current=True,need_theme=True),
'302':dict(name='Orbit Action',kind={'Movie'},genres_any={'Action'},themes=W('cop','agent','assassin','heist','fight','mercenar','revenge','martial','spy','chase','soldier'),against=W('documentary','concert','UFC','WWE'),era=(1960,2026),kids=False,current=True),
'303':dict(name='Orbit Sci-Fi',kind={'Movie'},genres_any={'Science Fiction'},need_any={'Science Fiction'},themes=W('alien','space','future','robot','planet','time','dystopia','android','galaxy'),against=W('documentary'),era=(1950,2026),kids=False,current=True),
'304':dict(name='Orbit Horror',kind={'Movie'},genres_any={'Horror'},need_any={'Horror'},themes=W('haunt','demon','possess','killer','evil','terror','ghost','curse','slasher','nightmare'),against=W('documentary'),era=(1960,2026),kids=False,current=True),
'305':dict(name='After Dark',kind={'Movie'},genres_any={'Horror','Thriller','Science Fiction'},themes=W('cult','bizarre','strange','exploitation','b-movie','grindhouse','creature','mutant','zombie','cannibal','obscure','surreal','underground','gore'),against=W('documentary','romcom','family'),era=(1955,2018),kids=False,intl_bonus=True),
'306':dict(name='Orbit VHS',kind={'Movie'},genres_any={'Comedy','Adventure','Fantasy','Family','Science Fiction'},themes=W('kids','teen','adventure','summer','gang','treasure'),against=W('documentary'),era=(1980,1995),kids=False),
'307':dict(name='Orbit Romance',kind={'Movie'},genres_any={'Romance'},need_any={'Romance'},themes=W('love','romance','romantic','falls for','fall in love','wedding','sweetheart','kiss','date'),against=W('documentary','murder','horror','outlaw','gunfight','sheriff','war'),need_theme=True,era=(1930,2026),kids=False,current=True),
'308':dict(name='Detour',kind={'Movie'},genres_any={'Comedy'},need_any={'Comedy'},themes=W('road trip','party','college','bachelor','wild night','spring break','frat','hangover','vegas'),against=W('documentary','animated'),era=(1990,2026),kids=False,current=True,need_theme=True),
'309':dict(name='BritX',kind={'Movie'},genres_any={'Crime','Comedy','Drama','Thriller'},themes=W('gangster','gang','criminal','crime','heist','drug','hooligan','council estate','thug','con','robbery','dealer'),need_theme=True,against=W('documentary'),era=(1965,2026),kids=False,uk=True,current=True),
'401':dict(name='Ring X',kind={'Movie','Series'},genres_any={'Sport','Action','Documentary'},themes=W('wrestl','WWE','AEW','TNA','NJPW','ROH','Impact Wrestling','NXT','SmackDown','WrestleMania','Royal Rumble','SummerSlam','Survivor Series','Money in the Bank','Bound for Glory','WrestleDream','title match','pay-per-view'),against=W('romance'),era=(1980,2026),kids=False,current=True,need_theme=True),
'601':dict(name='Vortex',kind={'Series','Movie'},genres_any={'Animation','Action','Anime'},need_any={'Animation','Anime'},themes=W('anime','fight','power','warrior','battle','hero','martial','saiyan','ninja','mecha','demon'),against=W('preschool','sitcom'),era=(1980,2026),kids=False,need_theme=True),
'602':dict(name='Heroes',kind={'Series'},genres_any={'Animation','Action'},need_any={'Animation'},themes=W('superhero','batman','spider','x-men','justice','villain','powers'),against=W('preschool'),era=(1980,2015),kids=True),
'603':dict(name='Orbit X',kind={'Series'},genres_any={'Animation','Action','Adventure'},need_any={'Animation','Children','Family'},themes=W('rangers','robot','team','evil','power','adventure','toy'),against=W('preschool','adult'),era=(1980,2002),kids=True),
'604':dict(name='Rewind',kind={'Series'},genres_any={'Animation','Comedy'},need_any={'Animation'},themes=W('cartoon','kids','adventures','friends'),against=W('adult','preschool'),era=(1990,2015),kids=True),
'605':dict(name='ToonZ',kind={'Series'},genres_any={'Animation','Comedy'},need_any={'Animation'},themes=W('cartoon','classic'),against=W('adult'),era=(1928,2005),kids=True),
'611':dict(name='Orbit Junior',kind={'Series'},genres_any={'Children','Family','Animation','Comedy'},need_any={'Children','Kids','Family'},themes=W('children','kids','school','friends','adventure'),against=W('adult','murder'),era=(1955,2026),kids=True,uk=True),
'612':dict(name='Wonder',kind={'Movie'},genres_any={'Animation','Family','Fantasy'},need_any={'Animation'},themes=W('princess','magic','fairy','kingdom','adventure','tale'),against=W('adult','horror','preschool','junior','Disney Jr','Spidey'),era=(1935,2026),kids=True),
'613':dict(name='PixelZ',kind={'Movie'},genres_any={'Animation','Family','Comedy'},need_any={'Animation'},themes=W('adventure','friends','family','toy','journey'),against=W('adult','horror'),era=(1995,2026),kids=True,current=True),
'614':dict(name='Tots',kind={'Series'},genres_any={'Children','Animation','Family'},need_any={'Children','Kids'},themes=W('preschool','little','learn','friends','gentle','toddler','young children'),against=W('adult','violence','fight','battle'),era=(1980,2026),kids=True),
}
KIDS_OK={'G','PG','TV-Y','TV-Y7','TV-G','TV-PG','U','TV-Y7-FV','E','0+','3+','6+','7+'}
ADULT={'R','TV-MA','NC-17','18','18+','X','NR18','SG-R21','SG-M18','GB-18'}
SYN={'Anime':'Animation','Martial Arts':'Action','Sci-Fi & Fantasy':'Science Fiction','Action & Adventure':'Action','Kids':'Children','Suspense':'Thriller','Sports':'Sport'}
def gen(r): return {SYN.get(g,g) for g in (r.get('genres') or [])}
# Channel text profiles: TF-IDF of their titles' descriptions, so a candidate is also compared with what the channel actually shows.
STOP=set('a an the and or of to in on for with his her their its is are was be by as at from this that he she they it into who when after one two new life series film show story world'.split())
def toks(t): return [w for w in re.findall(r"[a-z][a-z'-]{2,}",(t or '').lower()) if w not in STOP]
pools={}
for f in glob.glob(f'{R}/episodes-*.json'):
ch=f.split('-')[-1][:-5]
if ch in B: pools[ch]=[s['id'] for s in json.load(open(f))['series']]
docs={ch:[ev[i] for i in ids if i in ev and ev[i].get('overview')] for ch,ids in pools.items()}
df=collections.Counter(w for ch in docs for d in docs[ch] for w in set(toks(d['overview'])))
N=sum(len(v) for v in docs.values())
def vec(text):
tf=collections.Counter(toks(text)); return {w:(1+math.log(n))*math.log((N+1)/(df.get(w,0)+1)) for w,n in tf.items()}
def norm(v):
s=math.sqrt(sum(x*x for x in v.values())) or 1; return {k:x/s for k,x in v.items()}
cent={}
for ch,ds in docs.items():
acc=collections.Counter()
for d in ds:
for k,x in norm(vec(d['overview'])).items(): acc[k]+=x
cent[ch]=norm(dict(acc))
def cos(a,b): return sum(x*b.get(k,0) for k,x in a.items())
ENGLISH=re.compile(r'United States|United Kingdom|Canada|Australia|Ireland|New Zealand|England|Scotland|Wales')
def original(i):
row=c.execute('select original_title,title from items where id=?',(i,)).fetchone()
return row or ('','')
BOARD=re.compile(r'^(SG|BR|PL|KR|JP|DE|FR|IT|ES|NL|TR|IN|MX|CN|HK|TW|TH|PH|MY|ID|RU|SE|NO|DK|FI|AR|CL|CO|PT|CZ|HU|RO|GR|IL|EG|SA|AE|VN)-',re.I)
def foreign(r):
if BOARD.search(r.get('rating') or ''): return True,'rated by a non-English-language board ('+r['rating']+')'
o,t=original(r['id'])
nonlatin=bool(re.search(r'[^\x00-\u024f\s\d\W]',o or ''))
countries=' '.join(r.get('countries') or [])
if nonlatin: return True,'original title not in English'
if countries and not ENGLISH.search(countries): return True,'made in '+countries.split()[0]
if o and t and o.strip().lower()!=t.strip().lower() and not re.search(r'[A-Za-z]{3}',o): return True,'original title differs'
return False,''
INTL={'305','601','401'}
ANIMATED_CH={'105','601','602','603','604','605','611','612','613','614'}
DOC_CH={'201','202','203','204','401'}
FICTION_CH={'101','103','104','105','106','107','301','302','303','304','305','306','307','308','309','601','602','603','604','605','612','613'}
MAINSTREAM={'PG-13','12','12A','TV-14','GB-12','GB-12A','GB-15','15'}
HORRORCOMEDY=re.compile(r'\b(body horror|body-horror|gross-out|splatter|horror-comedy|horror comedy)',re.I)
# Paul, 5 November 2026: Ring X is professional wrestling. A verified promotion (by studio) counts for it; MMA events go on no channel.
PROMO=re.compile(r"\b(WWE|World Wrestling|All Elite Wrestling|AEW|TNA|Total Nonstop|Impact Wrestling|New Japan|NJPW|Ring of Honor|ROH|NWA|WCW|ECW|Lucha|Stardom|Game Changer Wrestling)\b",re.I)
MMA=re.compile(r"\b(UFC|MMA|mixed martial|Bellator|PFL|Ultimate Fighting|cage (fight|bout)|octagon)",re.I)
LIVE=re.compile(r'\b(stand-up|standup|comedian|comedy special|unruly set|in this set|live from|in concert|tour film|takes aim at)\b',re.I)
def score(r,ch):
b=B[ch]; e=[]; typ=r.get('type'); g=gen(r); ov=r.get('overview') or ''; year=r.get('year'); rating=r.get('rating') or ''
text=ov+' '+(r.get('name') or '')+' '+' '.join(r.get('taglines') or [])
if typ not in b['kind']: return 0,['wrong kind']
allt=' '.join([r.get('name') or '',ov,' '.join(r.get('studios') or [])])
if MMA.search(allt) and not PROMO.search(allt): return 0,['an MMA event: Ring X is professional wrestling, and no channel takes MMA events']
if b.get('kids') and rating in ADULT: return 0,[f'rated {rating}']
s=0.
# Format, as a hard limit: animation, documentary and fiction each have their own channels; live specials have none but Ring X's events.
animated='Animation' in g
doc=bool(g & {'Documentary'})
if animated and ch not in ANIMATED_CH: return 0,['animation, not for this channel']
if not animated and ch in ANIMATED_CH and g and ch not in ('611','601'): return 0,['live action, not for this channel']
if doc and ch in FICTION_CH: return 0,['a documentary, not fiction']
if g and not doc and ch in DOC_CH and ch!='401' and not (g & {'Reality'} and ch in ('203','204')): return 0,['fiction, not factual']
if ch=='107' and (doc or g & {'Reality'}): return 0,['factual, and Evidence is scripted only']
if LIVE.search(ov) and ch!='401': return 0,['a live or stand-up special']
# Genres: only as one part of the evidence; missing genres are not held against it.
if g:
if b.get('need_any') and not (g & b['need_any']): return 0,[f'not {"/".join(sorted(b["need_any"]))}']
gh=len(g & b['genres_any'])
s+=.25*min(1,gh/1.5); e.append(f'genres {", ".join(sorted(g & b["genres_any"])) or "none of its"}')
else: s+=.1; e.append('no genres given (not held against it)')
# Themes from the description, for and against.
pos=sorted(set(m.group(0).lower() for m in b['themes'].finditer(text)))
neg=sorted(set(m.group(0).lower() for m in b['against'].finditer(text)))
if ov:
s+=.35*min(1,len(pos)/2); e.append(('themes: '+', '.join(pos[:4])) if pos else 'no brief themes in its description')
if neg: s-=.25*min(1,len(neg)/2); e.append('against: '+', '.join(neg[:3]))
sim=cos(norm(vec(ov)),cent.get(ch,{})) if ch in cent else 0
short=len(ov)<200
# A short description says little: lean on the defining genre and its themes instead of word similarity.
if short and g and (g & (b.get('need_any') or b['genres_any'])) and (pos or b.get('need_any')): s+=.4*max(min(1,sim/.25),.6); e.append(f'short description, matched on genre ({sim:.2f})')
else: s+=.4*min(1,sim/.25); e.append(f'description like its titles ({sim:.2f})')
else: s+=.15; e.append('no description (not held against it)')
# Paul, 5 November 2026: context over category. Horror played for laughs (a horror-comedy, body horror, gross-out) is After
# Dark's cult territory, whatever its year, rather than mainstream Horror or Sci-Fi.
# Paul, the same day: not every horror-comedy. A family-friendly or mainstream-rated one (PG-13, 12A and under) keeps its usual
# channels; tone and audience still count. An unknown rating isn't held against it.
hc='Horror' in g and ('Comedy' in g or bool(HORRORCOMEDY.search(text))) and 'Family' not in g and rating not in KIDS_OK|MAINSTREAM
if year and not (hc and ch=='305'):
lo,hi=b['era']
if year<lo-3 or year>hi+3: s*=.3 if not b.get('current') else 1; e.append(f'{year} outside its era {lo}-{hi}' if not b.get('current') else '')
if ch=='101' and g & {'Game Show','Reality','Talk'}: return 0,['a game, reality or talk show, not a sitcom']
if ch=='309':
verified=bool(UK.search(' '.join(r.get('countries') or []))) or bool(UKSTUDIO.search(' '.join(r.get('studios') or [])))
if not verified: return 0,['British origin not verified' if not (r.get('countries') or r.get('studios')) else 'not British by its countries or studios']
if b.get('uk'):
uk=bool(UK.search(' '.join(r.get('countries') or []))) or bool(UKSTUDIO.search(' '.join(r.get('studios') or [])))
if uk: s+=.1; e.append('British')
elif r.get('countries') or r.get('studios'): s*=.6; e.append('not British by its studios or countries')
if b.get('kids') and rating and rating not in KIDS_OK: s*=.5; e.append(f'rated {rating}')
if b.get('kids') and not rating: s*=.8; e.append('no rating to confirm it suits children')
if ch=='401' and PROMO.search(' '.join(r.get('studios') or [])): s+=.15; e.append('a verified wrestling promotion')
if 'Horror' in g and ch in ('304','305') : s+=.12; e.append('a horror film')
if 'Horror' in g and ch in ('308','307','612','613'): s*=.5; e.append('horror is not for this channel')
if hc and ch=='305': s+=.15; e.append("a horror-comedy, After Dark's cult territory")
if hc and ch in ('303','304'): s*=.7; e.append('a horror-comedy, not mainstream horror or sci-fi')
if b.get('need_any') and g & b['need_any'] and len(ov)<160: s+=.1; e.append('short description, its defining genre carries more')
if b.get('need_theme') and not pos: s*=.5; e.append('none of its themes')
# Paul, 4 November 2026: made-for-TV romances (the Hallmark kind) aren't Orbit material.
if 'TV Movie' in g: s*=.4; e.append('a made-for-TV film')
if ch=='301' and 'Action' in g and not re.search(r'superhero|epic|franchise|saga|blockbuster',text,re.I): s*=.8; e.append('action, but not an event film')
f,why=foreign(r)
if f and ch not in INTL: s*=.5; e.append(why+' (this channel is English-language)')
return max(0,s),[x for x in e if x]
def place(r):
sc=sorted(((score(r,ch),ch) for ch in B),key=lambda x:-x[0][0])
(s1,e1),c1=sc[0]; (s2,e2),c2=sc[1]
if s1<.45: v='no clear home'
elif s2>=s1*.85: v='ambiguous'
else: v='suggested'
tier='routine' if v=='suggested' and s1>=.7 and s2<s1*.7 else ('editorial' if v=='suggested' else ('needs evidence' if v=='ambiguous' else 'no action'))
return dict(id=r['id'],title=r.get('name'),year=r.get('year'),type=r.get('type'),genres=sorted(gen(r)),verdict=v,tier=tier,top=B[c1]['name'],s1=round(s1,2),second=B[c2]['name'],s2=round(s2,2),evidence='; '.join(e1))
if __name__=='__main__':
ids=[row[0] for row in c.execute("select id from items where premiere between '2026-09-05' and '2026-11-04' and provider_ids like '%Imdb%'")]
seen=set(); out=[]
carried={i for v in pools.values() for i in v}
for i in ids:
if i not in ev or i in carried: continue
r=ev[i]; key=(r.get('name'),r.get('year'))
if key in seen: continue
seen.add(key); out.append(place(r))
json.dump(out,open(f'{S}/staged8.json','w'),indent=0,ensure_ascii=False)
print(len(out),collections.Counter(o['verdict'] for o in out),collections.Counter(o['tier'] for o in out))
print(collections.Counter(o['top'] for o in out if o['verdict']=='suggested').most_common())