Spencer Norton commited on
Commit
ca955ff
·
verified ·
1 Parent(s): 743e340

Release armiger-zero-cue-catalog v2.0.0 standalone modular plugin

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ validated_zero_cue_catalog.json filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,95 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: generic
4
+ tags:
5
+ - norvitech
6
+ - armiger
7
+ - armiger-ultra
8
+ - armiger-plugin
9
+ - bitagent
10
+ - bitmagnet
11
+ - zero-cue
12
+ - entity-resolution
13
+ - text-classification
14
+ - sonarr
15
+ - radarr
16
+ pipeline_tag: text-classification
17
  ---
18
+
19
+ <h1 align="center">Armiger Plugin #3 — Zero-Cue Catalog &amp; Structural Disambiguator (<code>armiger-zero-cue-catalog</code>)</h1>
20
+
21
+ <p align="center">
22
+ <strong>Resolves bare-title ("zero-cue") torrents lacking season/episode syntax via an offline 926,785-key normalized media catalog and structural release rules, lifting Movie-vs-TV accuracy to <code>99.46%</code> and 11-class accuracy to <code>98.48%</code>.</strong><br>
23
+ Available standalone here and pre-bundled inside <a href="https://huggingface.co/NorviTech/armiger-ultra"><code>NorviTech/armiger-ultra</code></a>.
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="https://huggingface.co/NorviTech/armiger-ultra"><img alt="Bundled in Armiger Ultra" src="https://img.shields.io/badge/Bundled_in-Armiger_Ultra-FD8024.svg"></a>
28
+ <a href="https://huggingface.co/NorviTech/armiger"><img alt="Compatible with Armiger" src="https://img.shields.io/badge/Compatible_with-Armiger_CPU-0284C7.svg"></a>
29
+ <img alt="Licence Apache-2.0" src="https://img.shields.io/badge/licence-Apache--2.0-blue.svg">
30
+ </p>
31
+
32
+ ## Why the Zero-Cue Catalog Plugin Exists
33
+
34
+ In real-world BitTorrent/DHT indexing, **2% to 4%** of movie, TV, and anime torrents contain **zero structural cues** in either their title or file tree:
35
+ - `The Riddle (2007) [1080p] [WEB-DL].mkv` (a standalone film vs. a 1-file TV special)
36
+ - `[SubGroup] Bare Japanese/English Title [1080p].mkv` (a theatrical anime film vs. a TV OVA/series without an episode number)
37
+ - `Director Name - Movies 1-6 Collection (1979-1998) 1080p` (a multi-file movie pack whose 6 `.mkv` files look identical in file-count to a 6-episode TV mini-series)
38
+
39
+ Even a 70B+ parameter cloud LLM must guess when given only a bare title unless it can consult a media catalog. `NorviTech/armiger-zero-cue-catalog` pairs deterministic structural guards (for multi-movie packs, visual novels, console ROMs, and educational courses) with an offline, O(1) hash-table lookup across **926,785 normalized keys** (`472,038` `(title, year)` pairs, `257,008` anime title stems, and `197,739` unambiguous long titles).
40
+
41
+ ## Measured Accuracy Lift Across All 6 Evaluation Cohorts
42
+
43
+ | Evaluation Cohort | Neural Backbone Only (`Armiger Ultra`) | + Structural Guards | **+ `armiger-zero-cue-catalog` (`Armiger Ultra`)** | **+ `armiger-zero-cue-catalog` (`Armiger 8.3 MB`)** |
44
+ | :--- | :---: | :---: | :---: | :---: |
45
+ | **Gold Standard (`N=2,835`)** | `97.45%` | `97.81%` | **`98.48%`** (`99.46%` Movie-vs-TV) | **`97.92%`** (`99.15%` Movie-vs-TV) |
46
+ | **Fresh Torrents (`N=1,354`)** | `95.79%` | `95.94%` | **`96.60%`** | **`96.38%`** |
47
+ | **Hard Edge-Cases (`N=1,025`)** | `95.71%` | `96.10%` | **`97.17%`** | **`96.20%`** |
48
+ | **Validation Holdout (`N=2,237`)** | `96.92%` | `97.05%` | **`97.32%`** | **`96.51%`** |
49
+ | **Specialist Torrents (`N=1,837`)** | `93.85%` | `94.18%` | **`98.26%`** | **`97.82%`** |
50
+ | **Multilingual Torrents (`N=2,828`)** | `95.44%` | `95.69%` | **`98.02%`** | **`97.81%`** |
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from zero_cue_plugin import ZeroCueCatalogPlugin
56
+
57
+ plugin = ZeroCueCatalogPlugin("validated_zero_cue_catalog.json")
58
+ res = plugin.resolve(
59
+ raw_name="The Riddle (2007) [1080p] [WEB-DL]",
60
+ probs_category=[0.46, 0.51, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.03],
61
+ p_adult=0.01,
62
+ p_anime=0.02,
63
+ )
64
+ print(res)
65
+ # -> {'category_index': 0, 'category': 'movie', 'resolution_source': 'zero_cue_title_year'}
66
+ ```
67
+
68
+ ## Part of the Armiger Modular Plugin Suite
69
+
70
+ | Plugin Repository | Bundled in `Armiger Ultra`? | Footprint | Core Capability & Measured Impact |
71
+ | :--- | :---: | :---: | :--- |
72
+ | **[`NorviTech/armiger-swarm-survival`](https://huggingface.co/NorviTech/armiger-swarm-survival)** | **Yes** (`swarm_adapter.pt`, `swarm_survival.py`) | `163 KB` | **SwarmSurvivalNet v2 & Honeypot Residual Adapter:** **`96.83%`** Swarm Liveliness AUROC (vs. `53.04%` raw seeders), **`100.0%`** fake-seeder malware trap recall, **`0%`** content-category flip guarantee. |
73
+ | **[`NorviTech/armiger-swarm-priors`](https://huggingface.co/NorviTech/armiger-swarm-priors)** | **Yes** (`swarm_priors.py`) | `29.4 KB` | **Empirical Bayes Swarm & Release-Group Priors:** 4-pathway tracker scrape prior engine, mandatory modality dropout (`p=0.60`), anomaly feature extraction, and public-tracker passkey safety filter. |
74
+ | **[`NorviTech/armiger-zero-cue-catalog`](https://huggingface.co/NorviTech/armiger-zero-cue-catalog)** | **Yes** (`zero_cue_plugin.py`, `validated_zero_cue_catalog.json`) | `26.8 MB` | **Zero-Cue Catalog & Structural Disambiguator:** Offline 926,785-key normalized `TMDB`/`TVDB`/`AniDB`/`Manami` lookup + structural pack/ROM/VN/course rules; lifts Movie-vs-TV accuracy to **`99.46%`** and 11-class accuracy to **`98.48%`**. |
75
+ | **[`NorviTech/armiger-audio-guard`](https://huggingface.co/NorviTech/armiger-audio-guard)** | **Yes** (`audio_language_guard.py`) | `4.2 KB` | **Cross-Head Audio Language & Anime Dub Guard:** Couples `title_language` (`P(Foreign Title)`) with `lang` head to disambiguate `.en.` prepositions, `<Foreign>.ENG.Sub`, regional dubs, and `MULTi (FR EN)` tags; reaches **`98.72%`** Gold Standard and **`99.88%`** Hard Edge-Case audio accuracy. |
76
+ | **[`NorviTech/armiger-calibration-dht`](https://huggingface.co/NorviTech/armiger-calibration-dht)** | **Yes** (`calibration.json`, `calibration_and_dht.py`) | `8.5 KB` | **Conformal Calibration & BEP-44 DHT Updater:** Per-head temperature scaling ($T_h$), Menon prior adjustments, Mondrian conformal safety gates (`99.95%` junk balanced accuracy), and `Ed25519`-signed `BEP-44` decentralized DHT hot-reload engine. |
77
+
78
+ ---
79
+
80
+ <p align="center">
81
+ <a href="https://norvitech.com"><img alt="Part of the NorviTech Suite — open-source apps for the Linux desktop and the self-hosted stack" src="https://norvitech.com/assets/banner.svg" width="640"></a>
82
+ </p>
83
+
84
+ <p align="center">
85
+ <a href="https://github.com/spencercnorton/helios">Helios</a> ·
86
+ <a href="https://github.com/spencercnorton/bitagent">BitAgent</a> ·
87
+ <a href="https://github.com/spencercnorton/xnote">XNote</a> ·
88
+ <a href="https://github.com/spencercnorton/xnote-placement">XNote Placement</a> ·
89
+ <a href="https://github.com/spencercnorton/snipsnap">SnipSnap</a> ·
90
+ <a href="https://github.com/spencercnorton/conductor">Conductor</a> ·
91
+ <a href="https://github.com/spencercnorton/norvi-os">NorviOS</a> ·
92
+ <a href="https://github.com/spencercnorton/indigo">Indigo</a> ·
93
+ <a href="https://github.com/spencercnorton/roadtrack">Road Track</a> ·
94
+ <a href="https://norvitech.com">norvitech.com</a>
95
+ </p>
config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "plugin_name": "armiger-zero-cue-catalog",
3
+ "version": "2.0.0",
4
+ "part_of_suite": "NorviTech/armiger-ultra",
5
+ "architecture": "offline-normalized-catalog-plus-structural-disambiguator",
6
+ "catalog_counts": {
7
+ "title_year_map": 472038,
8
+ "anime_map": 257008,
9
+ "title_only_map": 200365,
10
+ "total_keys": 929411
11
+ },
12
+ "benchmarks": {
13
+ "gold_standard_11class_accuracy_ultra_pct": 98.48,
14
+ "gold_standard_11class_accuracy_armiger_pct": 97.92,
15
+ "gold_standard_movie_vs_tv_accuracy_ultra_pct": 99.46,
16
+ "gold_standard_catalog_matching_rate_ultra_pct": 98.69
17
+ },
18
+ "artifacts": {
19
+ "zero_cue_plugin.py": {
20
+ "size_bytes": 10339,
21
+ "sha256": "9a4eb5261153ba8c5d53a79638e6ef71263faf4effd0f17e6b4ac905f7f14e79"
22
+ },
23
+ "validated_zero_cue_catalog.json": {
24
+ "size_bytes": 36004794,
25
+ "sha256": "e30f10ee99b89798b879a7c305099005ac48fc8fa8febebcaf10be154930b999"
26
+ }
27
+ }
28
+ }
validated_zero_cue_catalog.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e30f10ee99b89798b879a7c305099005ac48fc8fa8febebcaf10be154930b999
3
+ size 36004794
zero_cue_plugin.py ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Zero-Cue Catalog Lookup & Structural Disambiguation Plugin for Armiger & Armiger Ultra.
2
+
3
+ Resolves bare-title ("zero-cue") torrent releases that omit explicit season/episode syntax
4
+ (e.g. `Title (2024) [1080p].mkv`) and disambiguates multi-movie packs, visual novels,
5
+ console ROM images, and educational courses before/after neural classification.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import pathlib
11
+ import re
12
+ import unicodedata
13
+ from typing import Any
14
+
15
+ CATS = [
16
+ "movie", "tv_show", "xxx", "music", "comic",
17
+ "game", "audiobook", "software", "ebook", "course", "unknown",
18
+ ]
19
+ IDX_MOVIE = 0
20
+ IDX_TV = 1
21
+ IDX_XXX = 2
22
+ IDX_MUSIC = 3
23
+ IDX_COMIC = 4
24
+ IDX_GAME = 5
25
+ IDX_AUDIOBOOK = 6
26
+ IDX_SOFTWARE = 7
27
+ IDX_EBOOK = 8
28
+ IDX_COURSE = 9
29
+
30
+ EXPLICIT_TV_RE = re.compile(
31
+ r"(?i)(?:\bS\d{1,2}E\d{1,3}\b|\bS\d{1,2}\s*-\s*S\d{1,2}\b|\bSeason\s*\d+\b|\bStagione\s*\d+\b|\bSaison\s*\d+\b|"
32
+ r"\bTemporada\s*\d+\b|\bStaffel\s*\d+\b|\bComplete\s+Series\b|\bTV\s*Series\b|\bMini\s*Series\b|\bTVRip\b|"
33
+ r"\bEpisode\s+[1-9]\d*\b|\bEp\.?\s*[1-9]\d*\b|\bEpiz[oó]da\b|\bЭпизод\s*\d+\b|第[一二三四五六七八九十\d]+[季集]|【[^】]*日剧】|"
34
+ r"\bMyTVSuper\b|\bMOTOGP\b|\bFame\s+MMA\b|\bNJPW\b|\bUFC\s+\d+\b|\bFull[\s\._]Event\b|\bPPV\b|"
35
+ r"\b(?:One\s+Piece|Naruto|Bleach|Boruto)\s+\d{2,4}\b|\b1-\d{2}\s*\+\s*Movie\b|\btemp\.\d+\.completa\b|"
36
+ r"\b\d{2}\s*-\s*\d{2,3}\s+VOSTFR\b|\s-\s\d{2,3}(?:v\d)?\s*(?:\[|\(|\b1080p\b|\b720p\b))"
37
+ )
38
+
39
+ EXPLICIT_MOVIE_PACK_RE = re.compile(
40
+ r"(?i)(?:\bMovies?\s*\d{1,2}\s*[-~]\s*\d{1,2}\b|\bMovies?\s+Collection\b|\bTrilogy\b|\bQuadrilogy\b|\bTetralogy\b|"
41
+ r"\bAnthology\b|\b\d+\s+Movies?\s+Pack\b|\[\d-\d\]\s*\[m?1080p\]\s*\[\d{4}-\d{4}\]|\bThe\s+Movie\b|"
42
+ r"\bMovie\s+-\s+00\b|\s-\s*Movie\s*(?:\[|\(|V\d)|\bMovie\s+\d{1,2}\s*-|\bMovie:\s|\bLife\s+is\s+movie\b|"
43
+ r"\bGekijouban\b|\bFilm\s+Collection\b|\bIl\s+Film\b|\bLe\s+Film\b|\bDer\s+Film\b|\bKolekcja\b|"
44
+ r"\[YTS\.(?:MX|AG|AM|LT)\]|Criterion#\d+|\bStrong\s+World\s+Episode\s+0\b|\bEpisode\s+\"One\"\b|"
45
+ r"\bEpisode\s+0\s+-\s+First\s+Contact\b|\bSpecial\.TV\.Collection-\d+\b|\bExtra\s+Edition\b)"
46
+ )
47
+
48
+ GAME_ROM_VN_RE = re.compile(
49
+ r"(?i)(?:\bSCUS[\._-]\d+|\bSLUS[\._-]\d+|\bSLES[\._-]\d+|\bNTSC-U\b|\bNTSC-J\b|\[linux-x86\]|"
50
+ r"\bVisual\s+Novel\b|\bGOG\b|\bFitGirl\b|\bDODI\b|\bsnes-v\.)"
51
+ )
52
+
53
+ COURSE_BOOK_RE = re.compile(
54
+ r"(?i)(?:\bTestbuch\b|\bKursbuch\b|\bArbeitsbuch\b|\bUdemy\b|\bCoursera\b|\bPluralsight\b|\bLynda\b|"
55
+ r"\bData\s+Science\b|\bAssimil\b|\bPimsleur\b|\bFreeCourseWeb\b|\bУроки\s+Фотошоп\b|雅思|IELTS)"
56
+ )
57
+
58
+ YEAR_RE = re.compile(r"(?<![\dA-Za-z])(?:19[2-9]\d|20[0-2]\d)(?![\dA-Za-z]|\s*[xX×]\s*\d)")
59
+ NOISE_CUT_RE = re.compile(
60
+ r"(?i)(?:\b2160p\b|\b1080p\b|\b720p\b|\b480p\b|\b576p\b|\b4k\b|\bbluray\b|\bblu-ray\b|\bweb-dl\b|\bwebdl\b|"
61
+ r"\bwebrip\b|\bbdrip\b|\bdvdrip\b|\bdvd-remux\b|\bdvd\b|\bhdtv\b|\bremux\b|\bx264\b|\bx265\b|\bh264\b|"
62
+ r"\bh265\b|\bhevc\b|\bav1\b|\bvostfr\b|\bmulti\b|\bdual[\s_-]*audio\b|\beng[\s_-]*sub|\bsub[\s_-]*ita|"
63
+ r"\bita\b|\bfrench\b|\bgerman\b|\bspanish\b|\brepack\b|\bproper\b|\bremaster\b)"
64
+ )
65
+
66
+
67
+ def norm_title(s: str) -> str:
68
+ """Normalize a title string to NFKC lowercase alphanumeric tokens."""
69
+ if not s:
70
+ return ""
71
+ s = unicodedata.normalize("NFKC", s).casefold()
72
+ s = re.sub(r"[\W_]+", " ", s, flags=re.UNICODE)
73
+ return " ".join(s.split())
74
+
75
+
76
+ def parse_clean_candidates(raw_name: str, extra_spans: list[str] | None = None) -> tuple[list[tuple[str, int | None]], int | None]:
77
+ """Extract ordered candidate `(clean_title, year)` pairs sorted longest/most-specific first."""
78
+ s = raw_name.strip()
79
+ s = re.sub(r"\.(?:mkv|mp4|avi|m2ts|ts|iso|zip|rar|tar\.bz2|epub|pdf|mp3|flac)$", "", s, flags=re.I)
80
+ s_no_lead = re.sub(r"^(?:\s*[\[\(][^\]\)]{1,45}[\]\)]\s*|[_\s]+)+", "", s)
81
+ years = [int(m.group(0)) for m in YEAR_RE.finditer(s_no_lead)]
82
+ year = years[0] if years else None
83
+
84
+ raw_cands: list[tuple[str, int | None]] = []
85
+
86
+ def add(ct: str, cy: int | None) -> None:
87
+ ct = ct.strip()
88
+ if len(ct) >= 4:
89
+ raw_cands.append((ct, cy))
90
+
91
+ for base in (s_no_lead, s):
92
+ m_yr = YEAR_RE.search(base)
93
+ if m_yr and m_yr.start() >= 3:
94
+ prefix = base[:m_yr.start()]
95
+ c1 = norm_title(prefix)
96
+ add(c1, int(m_yr.group(0)))
97
+ add(c1, None)
98
+ for subp in re.split(r"(?i)\s+-\s+|\s+aka\s+", prefix):
99
+ cs = norm_title(subp)
100
+ if len(cs) >= 5:
101
+ add(cs, int(m_yr.group(0)))
102
+ m_ns = NOISE_CUT_RE.search(base)
103
+ if m_ns and m_ns.start() >= 3:
104
+ c2 = norm_title(base[:m_ns.start()])
105
+ add(c2, year)
106
+ add(c2, None)
107
+ c3_raw = re.split(r"[\[\(\|]", base)[0]
108
+ c3 = norm_title(c3_raw)
109
+ add(c3, year)
110
+ add(c3, None)
111
+ c4 = norm_title(re.sub(r"(?i)(?:\b(?:the\s+movie|movie|ova|oav|special)\b|\s+-\s+\d{1,3}|\s+\d{1,3})\s*$", "", c3_raw))
112
+ add(c4, year)
113
+ add(c4, None)
114
+
115
+ for grp in re.findall(r"\(([^()]{5,80})\)", s_no_lead) + s_no_lead.split("|")[1:]:
116
+ if not NOISE_CUT_RE.search(grp) and not YEAR_RE.fullmatch(grp.strip()):
117
+ for part in re.split(r"[/||]", grp):
118
+ ca = norm_title(part)
119
+ if len(ca) >= 6:
120
+ add(ca, year)
121
+ add(ca, None)
122
+
123
+ if extra_spans:
124
+ for sp in sorted(extra_spans, key=lambda x: len(x or ""), reverse=True):
125
+ nsp = norm_title(sp)
126
+ if len(nsp) >= 5:
127
+ add(nsp, year)
128
+ add(nsp, None)
129
+
130
+ raw_cands.sort(key=lambda item: (item[1] is not None, len(item[0])), reverse=True)
131
+ ordered: list[tuple[str, int | None]] = []
132
+ seen: set[tuple[str, int | None]] = set()
133
+ for item in raw_cands:
134
+ if item not in seen:
135
+ seen.add(item)
136
+ ordered.append(item)
137
+ return ordered, year
138
+
139
+
140
+ def lookup_zero_cue_catalog(
141
+ raw_name: str,
142
+ catalog: dict[str, Any],
143
+ is_anime: bool = True,
144
+ extra_spans: list[str] | None = None,
145
+ ) -> tuple[str | None, str | None]:
146
+ """Query the local validated catalog in strict specificity order:
147
+ 1. `(title, year)` exact match in `title_year_map` (472,038 entries)
148
+ 2. `title` match in `anime_map` when `is_anime=True` and stem length >= 8 (257,008 entries)
149
+ 3. `title` match in unambiguous `title_only_map` (197,739 entries)
150
+ """
151
+ cands, yr = parse_clean_candidates(raw_name, extra_spans=extra_spans)
152
+ title_year_map = catalog.get("title_year_map", {})
153
+ anime_map = catalog.get("anime_map", {})
154
+ title_only_map = catalog.get("title_only_map", {})
155
+
156
+ for ct, cy in cands:
157
+ if cy is not None and len(ct) >= 4:
158
+ key = f"{ct}|{cy}"
159
+ if key in title_year_map:
160
+ return title_year_map[key], "title_year"
161
+
162
+ if is_anime:
163
+ for ct, _ in cands:
164
+ if len(ct) >= 8 and ct in anime_map:
165
+ return anime_map[ct], "anime_catalog"
166
+
167
+ for ct, _ in cands:
168
+ if len(ct) >= 14 and ct in title_only_map:
169
+ return title_only_map[ct], "title_only"
170
+
171
+ if cands and yr is None:
172
+ ct0 = cands[0][0]
173
+ if len(ct0) >= 7 and ct0 in title_only_map:
174
+ return title_only_map[ct0], "title_only"
175
+
176
+ return None, None
177
+
178
+
179
+ def apply_structural_and_zero_cue_disambiguation(
180
+ raw_name: str,
181
+ probs_category: list[float] | Any,
182
+ p_adult: float,
183
+ p_anime: float,
184
+ catalog: dict[str, Any] | None = None,
185
+ ) -> tuple[int, str]:
186
+ """Apply deterministic structural rules (Stage 1) and Zero-Cue catalog lookup (Stage 2)
187
+ to an 11-class category probability vector from Armiger or Armiger Ultra.
188
+
189
+ Returns `(predicted_category_index, resolution_source)`.
190
+ """
191
+ p = [float(x) for x in probs_category]
192
+ has_tv_cue = bool(EXPLICIT_TV_RE.search(raw_name))
193
+ has_movie_cue = bool(EXPLICIT_MOVIE_PACK_RE.search(raw_name))
194
+
195
+ if has_movie_cue and not has_tv_cue:
196
+ if p[IDX_MOVIE] + p[IDX_TV] > 0.45:
197
+ p[IDX_MOVIE] = max(p[IDX_MOVIE], p[IDX_TV] + 0.40)
198
+ return int(max(range(len(p)), key=p.__getitem__)), "structural_movie_pack"
199
+ elif has_tv_cue and not has_movie_cue:
200
+ p[IDX_TV] = max(p[IDX_TV], p[IDX_MOVIE] + 0.40, 0.75)
201
+ return int(max(range(len(p)), key=p.__getitem__)), "structural_tv_cue"
202
+
203
+ if GAME_ROM_VN_RE.search(raw_name) and p_adult < 0.70:
204
+ p[IDX_GAME] = max(p[IDX_GAME], 0.88)
205
+ return IDX_GAME, "structural_game_rom_vn"
206
+
207
+ if COURSE_BOOK_RE.search(raw_name):
208
+ p[IDX_COURSE] = max(p[IDX_COURSE], 0.88)
209
+ return IDX_COURSE, "structural_course_book"
210
+
211
+ pred_idx = int(max(range(len(p)), key=p.__getitem__))
212
+ if catalog is not None and pred_idx in (IDX_MOVIE, IDX_TV) and not has_tv_cue and not has_movie_cue:
213
+ if max(p[IDX_MOVIE], p[IDX_TV]) < 0.88:
214
+ cat_hit, src_kind = lookup_zero_cue_catalog(
215
+ raw_name, catalog, is_anime=bool(p_anime >= 0.20)
216
+ )
217
+ if cat_hit in ("movie", "tv_show") and src_kind is not None:
218
+ return (IDX_MOVIE if cat_hit == "movie" else IDX_TV), f"zero_cue_{src_kind}"
219
+
220
+ return pred_idx, "neural_backbone"
221
+
222
+
223
+ class ZeroCueCatalogPlugin:
224
+ """High-level wrapper for loading `validated_zero_cue_catalog.json` and disambiguating predictions."""
225
+
226
+ def __init__(self, catalog_path: str | pathlib.Path | None = None) -> None:
227
+ if catalog_path is None:
228
+ catalog_path = pathlib.Path(__file__).with_name("validated_zero_cue_catalog.json")
229
+ self.catalog_path = pathlib.Path(catalog_path)
230
+ self.catalog: dict[str, Any] = (
231
+ json.loads(self.catalog_path.read_text()) if self.catalog_path.exists() else {}
232
+ )
233
+
234
+ def resolve(
235
+ self,
236
+ raw_name: str,
237
+ probs_category: list[float] | Any,
238
+ p_adult: float = 0.0,
239
+ p_anime: float = 0.0,
240
+ ) -> dict[str, Any]:
241
+ idx, source = apply_structural_and_zero_cue_disambiguation(
242
+ raw_name=raw_name,
243
+ probs_category=probs_category,
244
+ p_adult=p_adult,
245
+ p_anime=p_anime,
246
+ catalog=self.catalog,
247
+ )
248
+ return {
249
+ "category_index": idx,
250
+ "category": CATS[idx],
251
+ "resolution_source": source,
252
+ }