118 lines
5.8 KiB
Python
118 lines
5.8 KiB
Python
"""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_.*CROWD", "Ambience", "Crowds"),
|
|
(r"^SPEECH_", "Speech", "Other speech"),
|
|
(r"^MUSIC_PDLC", "Music", "Music (DLC)"),
|
|
(r"^(MUSIC|MUS_)", "Music", "Music"),
|
|
(r"^(AMB|PSA)_", "Ambience", "Ambience"),
|
|
(r"^PSO_", "Ambience", "Ambient emitters"),
|
|
(r"^WND_", "Ambience", "Wind"),
|
|
(r"^WTR_", "Ambience", "Weather"),
|
|
(r"^P?VOL_", "Ambience", "Room tone"),
|
|
(r"FOOTSTEP", "SFX", "Footsteps"),
|
|
(r"^(CHR|NPC)_.*(COMBAT|BONEBREAK|BODYFALL)", "SFX", "Combat"),
|
|
(r"^(SEQ|MTN)_", "SFX", "Scripted sequences"),
|
|
(r"^OBJ_", "SFX", "Objects"),
|
|
(r"^(PX|PHY)_", "SFX", "Physics"),
|
|
(r"^WPN_", "SFX", "Weapons"),
|
|
(r"^DVC_", "SFX", "Gadgets"),
|
|
(r"^(GUI|SBJ)_", "SFX", "Interface"),
|
|
(r"^VFX_", "SFX", "Visual effects"),
|
|
(r"^NPC_", "SFX", "Enemies & NPCs"),
|
|
(r"^(CHR|GEN)_", "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, bank: str = None):
|
|
"""-> (group, category). `pack` = name of the stream pack a media belongs to, if any; `bank` = name of
|
|
the soundbank holding an in-memory sound (its prefix is more reliable than event names)."""
|
|
n = (bank or 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
|