Spencer Norton commited on
Release armiger-zero-cue-catalog v2.0.0 standalone modular plugin
Browse files- .gitattributes +1 -0
- README.md +92 -0
- config.json +28 -0
- validated_zero_cue_catalog.json +3 -0
- zero_cue_plugin.py +252 -0
.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 & 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 |
+
}
|