Catalogue: classify collection into system/faction/force-org class

- dlphelper/warhammer_data.py: keyword taxonomy (40k factions incl. Chaos and
  Xenos super-faction units, plus AoS/Heresy/Kill Team/Necromunda/Epic/OW)
- dlphelper/catalogue.py: manifest+directory scanner, conservative keyword
  classifier (plural-tolerant word boundaries, longest match wins), markdown
  and jsonl reports, overrides support, audited --apply moves
- catalogue.py CLI; tests/test_catalogue.py offline classifier test
- real-data check on the user's dirs: 1624/3333 files classified so far
This commit is contained in:
2026-10-04 20:25:21 +02:00
parent 64abd7126f
commit acb3415878
6 changed files with 848 additions and 7 deletions
+340
View File
@@ -0,0 +1,340 @@
"""Warhammer taxonomy and unit keyword tables used by the catalogue.
Two layers:
- SYSTEMS: game system -> faction -> keyword aliases (matched against file
names). Faction detection is the primary signal; unit names double as
faction hints since they are listed under their faction.
- CLASSES: keyword -> force-organization class (40k Force Organization slots:
HQ, Troops, Elites, Fast Attack, Heavy Support, Flyer, Dedicated Transport,
Lords of War, Fortification).
The unit table grows with the collection; it does not attempt to enumerate
every unit Games Workshop ever released - unknown files land in the
"unclassified" bucket of the catalogue report so gaps become visible.
"""
from __future__ import annotations
# ---------------------------------------------------------------------------
# Game systems and factions (keyword aliases, lowercase)
# ---------------------------------------------------------------------------
SYSTEMS: dict[str, dict[str, list[str]]] = {
"Warhammer 40,000": {
# -- Imperium -------------------------------------------------------
"Adeptus Astartes": [
"space marine", "astartes", "primaris", "ultramarines", "blood angels",
"dark angels", "space wolves", "black templars", "imperial fists",
"salamanders", "raven guard", "iron hands", "white scars", "grey knights",
"deathwatch", "crimson fists", "flesh tearers", "ravenwing", "deathwing",
"inner circle", "wulfen", "thunderwolf", "intercessor", "reiver", "aggressor", "inceptor",
"hellblaster", "eradicator", "bladeguard", "sternguard", "devastator",
"tactical squad", "terminator squad", "assault marine",
"redemptor", "contemptor", "dreadnought", "invictor", "repulsor",
"land raider", "land speeder", "predator", "vindicator", "whirlwind",
"drop pod", "razorback", "stormraven", "storm talon", "stormhawk",
"caestus", "chaplain", "librarian", "apothecary", "vexilus",
],
"Astra Militarum": [
"astra militarum", "imperial guard", "cadian", "catachan", "krieg",
"death korps", "tempestus", "scion", "valhallan", "steel legion",
"tallarn", "vostroyan", "tanith", "gaunt's ghosts", "leman russ",
"chimera", "basilisk", "hydra", "wyvern", "manticore", "deathstrike",
"sentinel", "taurox", "commissar", "bullgryn", "ogryn", "ratling",
"heavy weapon squad", "baneblade", "stormlord", "shadowsword",
"hellhammer", "malcador", "macharius", "valkyrie", "vulture",
"primaris psyker", "astropath", "priest",
],
"Adeptus Mechanicus": [
"adeptus mechanicus", "mechanicum", "admech", "skitarii",
"cult mechanicus", "sicarian", "ruststalker", "infiltrator",
"kataphron", "kastelan", "ironstrider", "sydonian",
"onager", "dunecrawler", "maniple", "archmagos", "magos", "dominus",
"manipulus", "enginseer", "transuranic", "tech-priest", "cybernetica",
"thallax", "castellax", "vorax", "ordinatus", "belisarius cawl",
],
"Adepta Sororitas": [
"adepta sororitas", "sisters of battle", "sister of battle",
"battle sister", "repentia", "retributor", "canoness", "dialogus",
"hospitaller", "seraphim", "zephyrim", "paragon warsuit",
"morvenn vahl", "junith", "immersa", "exorcist", "immolator",
],
"Adeptus Custodes": [
"adeptus custodes", "custodes", "custodian", "allarus", "vertus praetor",
"venatari", "aquilon", "sagittarum", "hetaeron", "trajann", "constantin valdor",
"sisters of silence", "witchseeker", "prosecutor", "vigilator",
"caladius", "coronus", "telemon", "achillus", "galatus", "pallas",
],
"Imperial Knights": [
"imperial knight", "knight errant", "knight paladin", "knight crusader",
"knight warden", "knight gallant", "knight lancer", "knight acheron",
"knight castellan", "knight valiant", "knight preceptor", "questoris",
"armiger", "warglaive", "helverin", "morialtis", "canis rex",
"cerastus", "atropos", "seneschal",
],
"Inquisition & Officio Assassinorum": [
"inquisition", "inquisitor", "astra telepathica", "witch hunter",
"coteaz", "eisenhorn", "ravenor", "kyria draxus",
"officio assassinorum", "culexus", "vindicare", "callidus", "eversor",
"saint celestine", "living saint", "assassin",
],
# -- Chaos ----------------------------------------------------------
"Chaos Space Marines": [
"chaos space marine", "black legion", "iron warriors", "word bearers",
"night lords", "alpha legion", "red corsairs", "world eaters",
"emperor's children", "chaos terminator", "chosen", "havoc",
"raptor", "warp talon", "chaos cultist", "cultist", "daemon prince",
"chaos lord", "chaos sorcerer", "abaddon", "huron blackheart",
"kharn", "lucius the eternal", "ahriman", "typhus", "fabius bile",
"vashtorr", "defiler", "forgefiend", "maulerfiend", "helldrake",
"helbrute", "chaos spawn", "obliterator", "chaos rhino",
"chaos vindicator", "spartan assault", "mastodon", "typhon",
],
"Death Guard": [
"death guard", "nurgle", "plague marine", "deathshroud", "mortarion",
"plaguecaster", "poxwalker", "blightlord", "foetid bloat-drone",
"plagueburst crawler", "myphitic blight-hauler", "blight hauler",
"tallyman", "foul blightspawn", "biologus putrifier",
"plague surgeon", "necrosius",
],
"Thousand Sons": [
"thousand sons", "tzeentch", "rubric marine", "rubricae",
"scarab occult", "magnus the red", "ahzek ahriman", "arhiman",
"exalted sorcerer", "osiron", "mutalith vortex beast", "tzaangor",
"prospero",
],
"Chaos Daemons": [
"chaos daemon", "chaos daemons", "daemonette", "bloodletter",
"plaguebearer", "horror", "screamer", "flesh hound", "juggernaut",
"keeper of secrets", "skarbrand", "kairos fateweaver", "rotigus",
"be'lakor", "great unclean one", "lord of change", "soul grinder",
"skull cannon", "burning chariot", "exalted flamer",
"fiend of slaanesh", "plague drone", "nurgling", "herald",
"skulltaker", "khorne", "slaanesh", "skull lord",
"shalaxi helbane",
],
"Chaos Knights": [
"chaos knight", "knight desecrator", "knight despoiler",
"knight rampager", "kharibdyss", "renegade knight",
],
# -- Xenos ----------------------------------------------------------
"Orks": [
"ork", "orks", "orkz", "nob", "boyz", "grot", "weirdboy", "painboy",
"warboss", "ghazghkull", "meganob", "deff dread", "killa kan",
"deffkopta", "stormboy", "kommando", "burna boy", "loota",
"trukk", "battlewagon", "gorkanaut", "morkanaut", "stompa", "squig",
"zagstruk", "snikrot", "speed freek", "boomdakka", "flash gitz",
"wazbom blasta", "dakka jet", "burna-bomber", "blitza-bomber",
"da troopz", "dad and sonz", "ass sassins", "abby boi",
],
"T'au Empire": [
"tau", "t'au", "ta'u", "kroot", "vespid", "battlesuit", "crisis suit",
"broadside", "r'varna", "ghostkeel", "stormsurge",
"fire warrior", "pathfinder team", "devilfish", "hammerhead",
"sky ray", "piranha", "sniper drone team", "ethereal",
"commander farsight", "commander shadowsun", "aun'va", "darkstrider",
"longstrike", "mont'ka", "assorted drones", "breacher",
"bomber", "tetra", "barracuda", "sun shark", "razorshark",
"kv128 stormsurge", "tx4 piranha", "kv46",
],
"Tyranids": [
"tyranid", "hive tyrant", "swarmlord", "carnifex", "hormagaunt",
"termagant", "termagaunt", "genestealer brood", "zoanthrope", "venomthrope",
"exocrine", "haruspex", "tyranid prime", "tyranid warrior",
"ravener", "gargoyle", "spore mine", "mawloc", "trygon",
"harridan", "harpy", "crone", "tervigon", "barbed hierodule",
"hierodule", "hierophant", "neurothrope", "deathleaper", "lictor", "pyrovore",
"biovore", "broodlord", "bio-titan", "dominatrix",
],
"Genestealer Cults": [
"genestealer cult", "genestealer", "aberrant", "acolyte", "neophyte hybrid",
"primus", "magus", "patriarch", "purestrain genestealer",
"sanctus", "kelermorph", "goliath truck", "rockgrinder",
"jackal alcaeus", "cult limo", "hijacked",
],
"Necrons": [
"necron", "c'tan", "canoptek", "wraith", "spyder", "scarab",
"tesseract vault", "doomsday ark", "annihilation barge",
"ghost ark", "monolith", "doom scythe", "night scythe",
"imotekh", "szarekh", "nemesor zahndrekh", "vargard obyron",
"lokhrust", "skorpekh", "flayed one", "deathmark", "lychguard",
"necron warrior", "necron immortal", "ovoid", "dzaan",
],
"Aeldari": [
"aeldari", "eldar", "craftworld", "ulthwe", "biel-tan", "iyanden",
"alaitoc", "saim-hann", "ynnari", "farseer", "warlock",
"spiritseer", "autarch", "asurmen", "guardian defender",
"ranger", "wraithguard", "wraithblades", "wraithlord",
"wraithknight", "dire avenger", "howling banshee",
"striking scorpion", "swooping hawk", "warp spider",
"fire dragon", "dark reaper", "shining spears", "windrider",
"vyper", "wave serpent", "fire prism", "night spinner",
"war walker", "hemlock wraithfighter", "crimson hunter",
"shade runner", "skyrunner",
],
"Drukhari": [
"drukhari", "dark eldar", "kabal", "wych", "wrack", "haemonculus",
"coven", "reaver jetbike", "hellion", "scourge", "mandrake",
"kabalite warrior", "raider", "ravager", "venom", "razorwing",
"voidraven", "grotesque", "talos", "cronos",
"beast pack", "khymera", "razorwing flock", "asdrubael vect",
"lilith", "sathonyx", "the twin lance", "luriel",
],
"Harlequins": [
"harlequin", "death jester", "shadowseer", "solitaire",
"troupe", "starweaver", "voidweaver",
],
"Ynnari": [
"ynnari", "ynnead", "visarch", "yncarne", "yvraine",
],
},
"Other Systems": {
"Age of Sigmar": [
"age of sigmar", "stormcast", "eternals", "orruk", "warclan",
"kruleboyz", "gloomspite", "sylvaneth", "seraphon", "ossiarch",
"nighthaunt", "lumineth", "daughters of khaine", "fyreslayers",
"kharadron", "idoneth", "blades of khorne", "maggotkin",
"gaunt summoner", "soulblight", "freeguild", "cities of sigmar",
"skaven", "chaos warriors", "saurus", "skink", "liberator",
"sequitor", "evocator", "vindictor", "kaldor draigo",
],
"Horus Heresy": [
"horus heresy", "legiones astartes", "pre-heresy",
"leviathan pattern", "fire raptor", "sicaran", "fellblade",
"kratos", "justaerin", "eisenstein", "veteran tactical",
"seeker squad", "suzerain", "invictarus", "fulgurite",
],
"Kill Team": [
"kill team", "killteam", "killzone", "operative",
"hand of the archon", "void-dance troupe", "veteran guardsmen",
],
"Necromunda": [
"necromunda", "goliath gang", "escher", "orlock", "cawdor",
"van saar", "ash wastes", "underhive",
],
"Legions Imperialis / Epic": [
"legions imperialis", "titan legio", "reaver titan",
"warhound titan", "warlord titan", "imperator titan",
"warmaster titan",
],
"Warhammer The Old World": [
"the old world", "bretonnia", "greenskins", "tomb kings",
"khemri", "chaos dwarf", "dog of war", "grail knight",
],
},
}
# ---------------------------------------------------------------------------
# Force Organization classes (keyword -> class)
# ---------------------------------------------------------------------------
CLASSES: list[tuple[str, list[str]]] = [
("Lords of War", [
"titan", "stompa", "gargantuan", "hierophant", "harridan", "hierodule",
"baneblade", "stormlord", "shadowsword", "hellhammer", "malcador",
"imperator", "warmaster", "tesseract vault", "plaguereaper",
"warhound", "warlord titan",
]),
("Fortification", ["fortification", "bunker", "bastion", "wall of martyrs"]),
("Flyer", [
"flyer", "heldrake", "dakka jet", "burna-bomber", "blitza-bomber",
"bomber", "sky ray", "stormraven", "storm talon", "stormhawk",
"caestus", "valkyrie", "vulture", "thunderbolt", "lightning",
"marauder", "razorwing", "voidraven", "crimson hunter",
"hemlock", "doom scythe", "night scythe", "harpy", "crone",
"fire raptor", "barracuda", "sun shark", "razorshark",
"wazbom blasta",
]),
("Dedicated Transport", [
"transport", "rhino", "razorback", "chimera", "taurox", "trukk",
"battlewagon", "devilfish", "wave serpent", "ghost ark",
"land raider", "repulsor", "drop pod", "starweaver", "voidweaver",
"goliath truck", "rockgrinder", "cult limo", "raider", "venom",
"spartan assault",
]),
("Fast Attack", [
"fast attack", "assault marine", "assault intercessor", "inceptor",
"outrider", "bike squad", "scout biker", "thunderwolf", "raptor",
"warp talon", "flesh hound", "screamer", "fiend of slaanesh",
"hellion", "reaver jetbike", "scourge", "mandrake", "swooping hawk",
"shining spears", "windrider", "vyper", "war walker",
"canoptek wraith", "canoptek scarab", "canoptek spyder",
"ravener", "gargoyle", "spore mine", "mawloc", "trygon",
"pyrovore", "khymera", "beast pack", "razorwing flock",
"skyrunner", "zephyrim", "seraphim", "sentinel", "jackal",
"stormboy", "deffkopta", "warbiker", "skorpekh destroyer",
"sicarian", "ironstrider", "sydonian",
]),
("Heavy Support", [
"heavy support", "devastator", "eradicator", "hellblaster", "havoc",
"loota", "burna boy", "carnifex", "exocrine", "haruspex",
"zoanthrope", "venomthrope", "biovore", "hierodule",
"wraithlord", "wraithknight", "fire prism", "night spinner",
"predator", "vindicator", "whirlwind", "basilisk", "hydra",
"wyvern", "manticore", "deathstrike", "onager dunecrawler",
"kastelan", "kataphron", "hammerhead", "broadside", "stormsurge",
"r'varna", "soul grinder", "defiler", "forgefiend", "maulerfiend",
"plagueburst crawler", "bloat-drone", "blight hauler", "mutalith",
"annihilation barge", "doomsday ark", "obront", "dark reaper",
"retributor", "paragon warsuit", "morvenn", "exorcist", "immolator",
"caladius", "coronus grav", "telemon", "achillus", "galatus",
"pallas grav", "skorpekh",
]),
("Elites", [
"elites", "veteran", "terminator", "sternguard", "bladeguard",
"aggressor", "reiver", "vanguard veteran", "dreadnought",
"redemptor", "contemptor", "leviathan pattern", "wulfen",
"wolf guard", "deathshroud", "plague marine", "blightlord",
"poxwalker", "rubric marine", "rubricae", "scarab occult",
"tzaangor", "chosen", "possessed", "obliterator", "daemonette",
"bloodletter", "plaguebearer", "nurgling", "juggernaut",
"chaos cultist", "cultist", "acolyte hybrid", "aberrant",
"neophyte", "purestrain", "genestealer", "lictor", "deathmark",
"flayed one", "lychguard", "wraithguard", "wraithblades",
"howling banshee", "striking scorpion", "warp spider",
"fire dragon", "incubi", "wrack", "grotesque", "talos", "cronos",
"troupe", "killa kan", "deff dread", "meganob", "nobz", "nob",
"kommando", "vespid", "sanguinary guard", "sanguinor",
"ancient", "allarus", "vertus praetor", "venatari", "aquilon",
"sagittarum", "witchseeker", "prosecutor", "vigilator",
"vindicare", "callidus", "eversor", "culexus", "assassin",
]),
("HQ", [
"hq", "captain", "chapter master", "primarch", "lieutenant",
"librarian", "chaplain", "apothecary", "farseer", "warlock",
"spiritseer", "autarch", "warboss", "big mek", "weirdboy",
"painboy", "commander", "ethereal", "hive tyrant", "swarmlord",
"tyranid prime", "neurothrope", "deathleaper", "patriarch",
"primus", "magus", "sanctus", "kelermorph", "overlord",
"imotekh", "szarekh", "zahndrekh", "obyron", "plaguecaster",
"necrosius", "chaos lord", "chaos sorcerer", "daemon prince",
"exalted champion", "herald", "skulltaker", "kairos", "rotigus",
"be'lakor", "great unclean one", "lord of change",
"keeper of secrets", "skarbrand", "kharn", "abaddon", "typhus",
"mortarion", "magnus", "ahriman", "lucius", "fabius bile",
"vashtorr", "canoness", "morvenn", "junith", "immersa",
"dialogus", "hospitaller", "trajann", "valdor", "inquisitor",
"coteaz", "eisenhorn", "ravenor", "draxus", "celestine",
"commissar", "priest", "enginseer", "dominus", "manipulus",
"archmagos", "magos", "belisarius cawl", "farsight", "shadowsun",
"aun'va", "darkstrider", "longstrike", "shade runner",
]),
("Troops", [
"troops", "battleline", "tactical squad", "intercessor",
"incursor", "infiltrator", "infantry squad", "conscript",
"krieg", "catachan", "cadian", "vostroyan", "penal legion",
"ratling", "veteran squad", "fire warrior", "pathfinder team",
"kroot", "termagant", "hormagaunt", "necron warrior",
"necron immortal", "guardian defender", "storm guardian",
"kabalite warrior", "boyz", "gretchin",
"scion", "scout squad",
]),
]
# Flattened per-system faction keyword index for quick matching
FACTION_ALIASES: dict[str, dict[str, str]] = {
system: {alias: faction for faction, aliases in factions.items() for alias in aliases}
for system, factions in SYSTEMS.items()
}
UNIT_CLASSES: dict[str, str] = {kw: klass for klass, kws in CLASSES for kw in kws}