"""Classify sounds into group / category and infer characters from Wwise names. Scheme (documented in docs/categories.md): group Speech | Music | Ambience | SFX | Unknown category finer bucket derived from the name prefix (e.g. "Interview tapes", "Combat barks") Characters: the authoritative speaker comes from the curated catalog (data/catalog/characters.csv, built from the user's per-character lists). For other lines `infer_characters` returns the characters/roles that the *name* mentions -- a hint, not a speaker label (conversations alternate speakers between variations). """ import re # (regex on the upper-case name, group, category) -- first match wins _RULES = [ (r"^(CHR_RADIOSCANNER|PSO_CARRADIO|SPEECH_RADIO|SPEECH_TV|SPEECH_CAMERAS|DIA_)", "Speech", "Radio & TV"), (r"^SPEECH_TAPE_", "Speech", "Interview tapes"), (r"^SPEECH_INTERROGATION_", "Speech", "Interrogations"), (r"^SPEECH_(CINEMATIC|NEW)_", "Speech", "Cinematics"), (r"^SPEECH_BOSS_", "Speech", "Boss fights"), (r"^SPEECH_RIDDLE(R|S)_", "Speech", "Riddler"), (r"^SPEECH_SIDE_", "Speech", "Side missions"), (r"^SPEECH_DLC_", "Speech", "DLC"), (r"^SPEECH_PRED_", "Speech", "Predator chatter"), (r"^SPEECH_(COMBAT|BULLY)_", "Speech", "Combat barks"), (r"^SPEECH_EMOTE_", "Speech", "Efforts & emotes"), (r"^SPEECH_REACT_", "Speech", "Reactions"), (r"^SPEECH_(STREETS|ZONES|OUTCAST|VERTICAL)_", "Speech", "Open-world chatter"), (r"^SPEECH_CHAR_", "Speech", "Character lines"), (r"^SPEECH_(CH|SEQUENCE|STEEL|MUSEUM|CHURCH|COURT|UNDER|GCPD|ZP1)_", "Speech", "Story missions"), (r"^SPEECH_(GAME|MAP|AUGMENTED|FORENSICS|TEMP)_", "Speech", "Gameplay & hints"), (r"^NPC_", "Ambience", "Crowds"), (r"^SPEECH_", "Speech", "Other speech"), (r"^MUSIC_PDLC", "Music", "Music (DLC)"), (r"^MUSIC", "Music", "Music"), (r"^(AMB|PSA)_", "Ambience", "Ambience"), (r"^WND_", "Ambience", "Wind"), (r"^P?VOL_", "Ambience", "Room tone"), (r"^SEQ_", "SFX", "Scripted sequences"), (r"^OBJ_", "SFX", "Objects"), (r"^WPN_", "SFX", "Weapons"), (r"^CHR_", "SFX", "Character foley"), (r"^GLOBAL", "SFX", "Global"), ] _RULES = [(re.compile(p), g, c) for p, g, c in _RULES] GROUPS = ("Speech", "Music", "Ambience", "SFX", "Unknown") def classify(name: str, pack: str = None): """-> (group, category). `pack` = name of the stream pack a media belongs to, if any.""" n = name.upper() if pack: p = pack.upper() if p.startswith("MUSIC"): return "Music", "Music (DLC)" if "PDLC" in p else "Music" for rx, g, c in _RULES: if rx.search(n): return g, c return "Unknown", "Unknown" # --- characters ------------------------------------------------------------------------------- # display names for the labels used in the user's catalog files (data/catalog/source/ita/*.txt) USER_LABELS = { "Alfred": "Alfred", "Azrael": "Azrael", "Bane": "Bane", "Batman": "Batman", "Calendar": "Calendar Man", "Catwoman": "Catwoman", "Croc": "Killer Croc", "Deadshot": "Deadshot", "Freeze": "Mr. Freeze", "Gordon": "Gordon", "Ivy": "Poison Ivy", "Joker": "Joker", "Madhatter": "Mad Hatter", "Oracle": "Oracle", "Penguin": "Penguin", "Quinn": "Harley Quinn", "Ras": "Ra's al Ghul", "Riddler": "Riddler", "Robin": "Robin", "Strange": "Hugo Strange", "Talia": "Talia al Ghul", "Twoface": "Two-Face", "Unknown": "Unknown", "Vicky": "Vicki Vale", "Zsazz": "Victor Zsasz", } # name tokens -> character / role (matched on whole "_"-separated tokens, digits ignored) _TOKENS = { "Batman": ("BATMAN", "BM"), "Catwoman": ("CATWOMAN", "CAT", "CW"), "Joker": ("JOKER",), "Penguin": ("PENGUIN", "PENG"), "Two-Face": ("TWOFACE", "TF"), "Riddler": ("RIDDLER",), "Mr. Freeze": ("FREEZE", "MRFREEZE", "FREEZESUIT"), "Poison Ivy": ("IVY",), "Hugo Strange": ("STRANGE", "HUGO"), "Robin": ("ROBIN",), "Oracle": ("ORACLE",), "Alfred": ("ALFRED", "ALF"), "Harley Quinn": ("QUINN", "HARLEY"), "Ra's al Ghul": ("RAS",), "Talia al Ghul": ("TALIA",), "Victor Zsasz": ("ZSASZ", "ZSAZZ"), "Bane": ("BANE",), "Deadshot": ("DEADSHOT",), "Mad Hatter": ("MADHATTER", "HATTER"), "Calendar Man": ("CALENDAR", "CM"), "Hush": ("HUSH",), "Clayface": ("CLAYFACE", "CLAY"), "Solomon Grundy": ("GRUNDY",), "Azrael": ("AZRAEL",), "Vicki Vale": ("VALE", "VICKI", "VICKY", "VIC"), "Gordon": ("GORDON",), "Nightwing": ("NIGHTWING", "NW"), "Killer Croc": ("CROC",), # roles "Thug": ("THUG", "THUGS", "GENTHUG", "JTHUG", "JTHUGS", "PTHUG", "PTHUGS", "BULLY"), "TYGER guard": ("TYGER",), "Ninja": ("NINJA", "ASSASSIN"), "Doctor": ("DOC", "DOCTOR"), "Cop": ("COP", "COPS", "GCPD"), "Political prisoner": ("HOBO", "OUTCAST", "OC"), "Hostage": ("HOST", "HOSTAGE"), "Guard": ("GUARD", "GUARDS"), } _TOK2CHAR = {t: c for c, toks in _TOKENS.items() for t in toks} _STRIP_DIGITS = re.compile(r"\d+$") def infer_characters(name: str): """Characters/roles mentioned by a sound name (ordered, unique). Empty if none.""" n = name.upper() out = [] if n.startswith("SPEECH_TAPE_"): out.append("Hugo Strange") # Strange's interview tapes for tok in re.split(r"[_\s]+", n): t = _STRIP_DIGITS.sub("", tok) c = _TOK2CHAR.get(t) or _TOK2CHAR.get(tok) if c and c not in out: out.append(c) return out