AniFileBERT / anifilebert /labels.py
ModerRAS's picture
Implement schema v2 anime filename labels
ed49faa
Raw
History Blame Contribute Delete
5.38 kB
"""Shared BIO label schema and helpers."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Dict, Optional, Tuple
LABEL_SCHEMA_VERSION = 2
TITLE_SUFFIXES = ("CHS", "CHT", "JPN", "LATIN", "MIXED")
TITLE_PRIORITY = ("CHS", "CHT", "JPN", "MIXED", "LATIN")
FILE_TITLE_ENTITIES = tuple(f"TITLE_{suffix}" for suffix in TITLE_SUFFIXES)
PATH_TITLE_ENTITIES = tuple(f"PATH_TITLE_{suffix}" for suffix in TITLE_SUFFIXES)
TITLE_ENTITIES = FILE_TITLE_ENTITIES + PATH_TITLE_ENTITIES
TITLE_LIKE_ENTITIES = TITLE_ENTITIES + ("TITLE",)
SEASON_LIKE_ENTITIES = ("SEASON", "PATH_SEASON")
DEFAULT_TITLE_ENTITY = "TITLE_MIXED"
_FALLBACK_LABELS = (
"O",
"B-TITLE_CHS",
"I-TITLE_CHS",
"B-TITLE_CHT",
"I-TITLE_CHT",
"B-TITLE_JPN",
"I-TITLE_JPN",
"B-TITLE_LATIN",
"I-TITLE_LATIN",
"B-TITLE_MIXED",
"I-TITLE_MIXED",
"B-PATH_TITLE_CHS",
"I-PATH_TITLE_CHS",
"B-PATH_TITLE_CHT",
"I-PATH_TITLE_CHT",
"B-PATH_TITLE_JPN",
"I-PATH_TITLE_JPN",
"B-PATH_TITLE_LATIN",
"I-PATH_TITLE_LATIN",
"B-PATH_TITLE_MIXED",
"I-PATH_TITLE_MIXED",
"B-PATH_SEASON",
"I-PATH_SEASON",
"B-SEASON",
"I-SEASON",
"B-EPISODE",
"I-EPISODE",
"B-SPECIAL",
"I-SPECIAL",
"B-GROUP",
"I-GROUP",
"B-RESOLUTION",
"I-RESOLUTION",
"B-SOURCE",
"I-SOURCE",
"B-TAG",
"I-TAG",
)
def _load_schema_labels() -> Tuple[str, ...]:
schema_path = Path(__file__).resolve().parents[1] / "label_schema.json"
try:
with schema_path.open("r", encoding="utf-8") as fh:
payload = json.load(fh)
except OSError:
return _FALLBACK_LABELS
labels = payload.get("labels")
if not isinstance(labels, list) or not labels:
return _FALLBACK_LABELS
if not all(isinstance(label, str) and label for label in labels):
return _FALLBACK_LABELS
return tuple(labels)
LABELS = _load_schema_labels()
LEGACY_15_LABELS = (
"O",
"B-TITLE",
"I-TITLE",
"B-SEASON",
"I-SEASON",
"B-EPISODE",
"I-EPISODE",
"B-SPECIAL",
"I-SPECIAL",
"B-GROUP",
"I-GROUP",
"B-RESOLUTION",
"I-RESOLUTION",
"B-SOURCE",
"I-SOURCE",
)
LABEL2ID = {label: idx for idx, label in enumerate(LABELS)}
ID2LABEL = {idx: label for idx, label in enumerate(LABELS)}
def make_label2id() -> Dict[str, int]:
return dict(LABEL2ID)
def make_id2label() -> Dict[int, str]:
return dict(ID2LABEL)
def split_bio_label(label: str) -> Tuple[Optional[str], Optional[str]]:
if not isinstance(label, str) or label == "O":
return None, None
prefix, sep, entity = label.partition("-")
if sep != "-" or prefix not in {"B", "I"} or not entity:
return None, None
return prefix, entity
def label_entity(label: str) -> Optional[str]:
return split_bio_label(label)[1]
def canonical_entity(entity: str) -> str:
return DEFAULT_TITLE_ENTITY if entity == "TITLE" else entity
def canonical_bio_label(label: str) -> str:
prefix, entity = split_bio_label(label)
if prefix is None or entity is None:
return "O" if label == "O" else label
return f"{prefix}-{canonical_entity(entity)}"
def is_title_entity(entity: Optional[str]) -> bool:
return entity in TITLE_LIKE_ENTITIES
def is_file_title_entity(entity: Optional[str]) -> bool:
return entity in FILE_TITLE_ENTITIES or entity == "TITLE"
def is_path_title_entity(entity: Optional[str]) -> bool:
return entity in PATH_TITLE_ENTITIES
def is_title_like_label(label: str) -> bool:
return is_title_entity(label_entity(label))
def is_season_entity(entity: Optional[str]) -> bool:
return entity in SEASON_LIKE_ENTITIES
def is_season_like_label(label: str) -> bool:
return is_season_entity(label_entity(label))
def is_same_entity_label(label: str, entity: str) -> bool:
return label_entity(label) == entity
def title_language(entity: Optional[str]) -> str:
if entity == "TITLE":
return "MIXED"
if not entity:
return "MIXED"
if entity.startswith("PATH_TITLE_"):
return entity.removeprefix("PATH_TITLE_")
if entity.startswith("TITLE_"):
return entity.removeprefix("TITLE_")
return "MIXED"
def title_entity_priority(entity: Optional[str]) -> Tuple[int, int]:
language = title_language(entity)
language_rank = TITLE_PRIORITY.index(language) if language in TITLE_PRIORITY else len(TITLE_PRIORITY)
path_rank = 1 if is_path_title_entity(entity) else 0
return path_rank, language_rank
def label_migration_sources(target_label: str) -> Tuple[str, ...]:
"""Return old-label candidates that can initialize a target label row."""
if target_label == "O":
return ("O",)
prefix, entity = split_bio_label(target_label)
if prefix is None or entity is None:
return (target_label,)
sources = [target_label]
if is_title_entity(entity):
sources.append(f"{prefix}-TITLE")
elif entity == "PATH_SEASON":
sources.append(f"{prefix}-SEASON")
return tuple(dict.fromkeys(sources))
def infer_legacy_id2label(num_labels: int) -> Optional[Dict[int, str]]:
if num_labels == len(LEGACY_15_LABELS):
return {idx: label for idx, label in enumerate(LEGACY_15_LABELS)}
if num_labels == len(LABELS):
return make_id2label()
return None