Token Classification
Transformers
ONNX
Safetensors
English
Japanese
Chinese
bert
anime
filename-parsing
Eval Results (legacy)
Instructions to use ModerRAS/AniFileBERT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ModerRAS/AniFileBERT with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="ModerRAS/AniFileBERT")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("ModerRAS/AniFileBERT") model = AutoModelForTokenClassification.from_pretrained("ModerRAS/AniFileBERT", device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 13,629 Bytes
33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c 7509455 33bb11c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | """Build annotation units from a DMHY prefix DAG.
The DAG producer keeps repeated suffix structure shared across many raw
prefixes. This tool turns those shared nodes into compact, traceable units for
LLM or human review without calling any remote service.
"""
from __future__ import annotations
import argparse
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Iterable
from tools.annotate_dmhy_prefix_graph import (
heuristic_patch,
string_list,
)
DEFAULT_DAG = Path("datasets/AnimeName/dmhy_prefix_dag.json")
DEFAULT_OUTPUT = Path("datasets/AnimeName/dmhy_prefix_dag.annotation_units.jsonl")
@dataclass(frozen=True)
class Args:
dag: Path
output: Path
min_reachable_terminals: int
min_incoming_count: int
limit: int | None
example_count: int
include_terminal_sink_units: bool
def parse_args() -> Args:
parser = argparse.ArgumentParser(
description="Emit DAG-aware DMHY prefix annotation units as JSONL"
)
parser.add_argument("--dag", type=Path, default=DEFAULT_DAG, help="Input dmhy_prefix_dag.json")
parser.add_argument(
"--output",
type=Path,
default=DEFAULT_OUTPUT,
help="Output annotation unit JSONL",
)
parser.add_argument(
"--min-reachable-terminals",
type=int,
default=2,
help="Select non-root nodes reaching at least this many terminals",
)
parser.add_argument(
"--min-incoming-count",
type=int,
default=2,
help="Select nodes with at least this many incoming DAG edges",
)
parser.add_argument("--limit", type=int, default=None, help="Maximum units to write")
parser.add_argument(
"--example-count",
type=int,
default=5,
help="Maximum examples retained per example field",
)
parser.add_argument(
"--include-terminal-sink-units",
action="store_true",
help="Include terminal nodes with no outgoing edges as annotation units",
)
ns = parser.parse_args()
return Args(
dag=ns.dag,
output=ns.output,
min_reachable_terminals=max(1, ns.min_reachable_terminals),
min_incoming_count=max(1, ns.min_incoming_count),
limit=ns.limit,
example_count=max(1, ns.example_count),
include_terminal_sink_units=ns.include_terminal_sink_units,
)
def load_dag(path: Path) -> dict[str, Any]:
if not path.exists():
raise SystemExit(f"DAG not found: {path}")
try:
dag = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise SystemExit(f"invalid DAG JSON in {path}: {exc}") from exc
if not isinstance(dag, dict):
raise SystemExit(f"invalid DAG schema in {path}: root must be an object")
if not isinstance(dag.get("nodes"), list):
raise SystemExit(f"invalid DAG schema in {path}: missing nodes list")
if not isinstance(dag.get("terminals"), list):
raise SystemExit(f"invalid DAG schema in {path}: missing terminals list")
return dag
def node_id(node: dict[str, Any], fallback: int) -> int:
value = node.get("id", fallback)
try:
return int(value)
except (TypeError, ValueError):
raise SystemExit(f"invalid node id: {value!r}") from None
def terminal_id(terminal: dict[str, Any], fallback: int) -> str:
value = terminal.get("terminal_id", terminal.get("id", fallback))
return str(value)
def int_field(row: dict[str, Any], key: str, default: int = 0) -> int:
try:
return int(row.get(key, default) or default)
except (TypeError, ValueError):
return default
def build_indexes(dag: dict[str, Any]) -> tuple[dict[int, dict[str, Any]], dict[int, list[dict[str, Any]]]]:
nodes: dict[int, dict[str, Any]] = {}
for fallback, node in enumerate(dag["nodes"]):
if not isinstance(node, dict):
continue
nodes[node_id(node, fallback)] = node
terminals_by_node: dict[int, list[dict[str, Any]]] = {}
for fallback, terminal in enumerate(dag["terminals"]):
if not isinstance(terminal, dict):
continue
try:
terminal_node_id = int(terminal.get("node_id"))
except (TypeError, ValueError):
continue
terminal = dict(terminal)
terminal["_terminal_id"] = terminal_id(terminal, fallback)
terminal["_terminal_index"] = fallback
terminals_by_node.setdefault(terminal_node_id, []).append(terminal)
return nodes, terminals_by_node
class ReachableTerminalIndex:
def __init__(
self,
nodes: dict[int, dict[str, Any]],
terminals_by_node: dict[int, list[dict[str, Any]]],
) -> None:
self.nodes = nodes
self.terminals_by_node = terminals_by_node
self.memo: dict[int, list[dict[str, Any]]] = {}
self.visiting: set[int] = set()
def get(self, start_node_id: int) -> list[dict[str, Any]]:
if start_node_id in self.memo:
return self.memo[start_node_id]
if start_node_id in self.visiting:
raise SystemExit(f"cycle detected while traversing DAG at node {start_node_id}")
self.visiting.add(start_node_id)
found = list(self.terminals_by_node.get(start_node_id, []))
node = self.nodes.get(start_node_id, {})
for edge in node.get("children") or []:
if not isinstance(edge, dict):
continue
try:
target = int(edge.get("target"))
except (TypeError, ValueError):
continue
found.extend(self.get(target))
self.visiting.remove(start_node_id)
deduped = dedupe_terminals(found)
self.memo[start_node_id] = deduped
return deduped
def dedupe_terminals(terminals: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
seen: set[str] = set()
result: list[dict[str, Any]] = []
for terminal in terminals:
tid = str(terminal.get("_terminal_id") or terminal.get("terminal_id") or "")
if not tid or tid in seen:
continue
seen.add(tid)
result.append(terminal)
return result
def limited_unique(values: Iterable[str], limit: int) -> list[str]:
seen: set[str] = set()
result: list[str] = []
for value in values:
if not value or not value.strip() or value in seen:
continue
seen.add(value)
result.append(value)
if len(result) >= limit:
break
return result
def edge_labels(node: dict[str, Any], limit: int) -> list[str]:
labels = []
for edge in node.get("children") or []:
if isinstance(edge, dict) and edge.get("label") is not None:
labels.append(str(edge["label"]))
return limited_unique(labels, limit)
def has_outgoing_edges(node: dict[str, Any]) -> bool:
return any(isinstance(edge, dict) for edge in node.get("children") or [])
def aggregate_examples(terminals: list[dict[str, Any]], key: str, limit: int) -> list[str]:
values: list[str] = []
for terminal in terminals:
if key == "prefix":
value = terminal.get("prefix")
if value is not None:
values.append(str(value))
else:
values.extend(string_list(terminal.get(key)))
return limited_unique(values, limit)
def aggregate_needs_review(terminals: list[dict[str, Any]]) -> bool:
for index, terminal in enumerate(terminals):
annotations = terminal.get("annotations")
if isinstance(annotations, dict) and annotations.get("needs_llm_review") is not None:
if bool(annotations["needs_llm_review"]):
return True
continue
if heuristic_patch(terminal, index)["needs_llm_review"]:
return True
return False
def annotation_template() -> dict[str, Any]:
return {
"episode_title_suffixes": [],
"media_suffixes": [],
"title_candidates": [],
"notes": None,
}
def recommended_prompt(kind: str, terminal_count: int) -> str:
if kind == "shared_suffix":
return (
"Review the shared DAG suffix examples and mark episode-title text, media metadata, "
f"and possible title candidates for {terminal_count} linked terminals."
)
return "Review this terminal cluster and mark episode-title text, media metadata, and title candidates."
def make_unit(
node: dict[str, Any],
node_id_value: int,
terminals: list[dict[str, Any]],
kind: str,
example_count: int,
) -> dict[str, Any]:
terminal_ids = [str(terminal["_terminal_id"]) for terminal in terminals]
reachable_weight = int_field(node, "reachable_weight")
if reachable_weight <= 0:
reachable_weight = sum(int_field(terminal, "weight", int_field(terminal, "count", 1)) for terminal in terminals)
return {
"unit_id": f"dag-node-{node_id_value}",
"node_id": node_id_value,
"kind": kind,
"incoming_count": int_field(node, "incoming_count"),
"reachable_terminals": len(terminals),
"reachable_weight": reachable_weight,
"terminal_ids": terminal_ids,
"prefix_examples": aggregate_examples(terminals, "prefix", example_count),
"value_examples": aggregate_examples(terminals, "value_examples", example_count),
"suffix_examples": aggregate_examples(terminals, "suffix_examples", example_count),
"common_edge_labels": edge_labels(node, example_count),
"needs_llm_review": aggregate_needs_review(terminals),
"recommended_prompt": recommended_prompt(kind, len(terminals)),
"annotations": annotation_template(),
}
def node_reachable_count(node: dict[str, Any]) -> int:
return int_field(node, "reachable_terminals")
def node_reachable_weight(node: dict[str, Any]) -> int:
return int_field(node, "reachable_weight")
def selected_units(dag: dict[str, Any], args: Args) -> list[dict[str, Any]]:
nodes, terminals_by_node = build_indexes(dag)
reachable_index = ReachableTerminalIndex(nodes, terminals_by_node)
root = int(dag.get("root", 0) or 0)
shared_candidates: list[tuple[tuple[int, int, int, int, int], int, dict[str, Any]]] = []
for current_id, node in nodes.items():
if not args.include_terminal_sink_units and not has_outgoing_edges(node):
continue
incoming_count = int_field(node, "incoming_count")
reachable_count = node_reachable_count(node)
by_shared_incoming = incoming_count >= args.min_incoming_count
by_reachable = current_id != root and reachable_count >= args.min_reachable_terminals
if by_shared_incoming or by_reachable:
sort_key = (
0,
-node_reachable_weight(node),
-reachable_count,
-incoming_count,
current_id,
)
shared_candidates.append((sort_key, current_id, node))
shared_candidates.sort(key=lambda item: item[0])
candidates: list[tuple[tuple[int, int, int, int, int], dict[str, Any]]] = []
for sort_key, current_id, node in shared_candidates:
if args.limit is not None and len(candidates) >= args.limit:
break
terminals = reachable_index.get(current_id)
if not terminals:
continue
unit = make_unit(node, current_id, terminals, "shared_suffix", args.example_count)
candidates.append((sort_key, unit))
if args.limit is not None and len(candidates) >= args.limit:
return [unit for _sort_key, unit in candidates[: max(0, args.limit)]]
covered_terminal_ids = {
terminal_id
for _sort_key, unit in candidates
for terminal_id in unit["terminal_ids"]
}
for current_id, terminals in terminals_by_node.items():
node = nodes.get(current_id, {"id": current_id})
if not args.include_terminal_sink_units and not has_outgoing_edges(node):
continue
uncovered = [terminal for terminal in terminals if terminal["_terminal_id"] not in covered_terminal_ids]
if not uncovered:
continue
unit = make_unit(node, current_id, uncovered, "terminal_cluster", args.example_count)
sort_key = (
1,
-unit["reachable_weight"],
-unit["reachable_terminals"],
-unit["incoming_count"],
current_id,
)
candidates.append((sort_key, unit))
candidates.sort(key=lambda item: item[0])
units = [unit for _sort_key, unit in candidates]
if args.limit is not None:
units = units[: max(0, args.limit)]
return units
def write_jsonl(path: Path, rows: Iterable[dict[str, Any]]) -> int:
path.parent.mkdir(parents=True, exist_ok=True)
count = 0
with path.open("w", encoding="utf-8", newline="\n") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n")
count += 1
return count
def main() -> None:
args = parse_args()
dag = load_dag(args.dag)
units = selected_units(dag, args)
count = write_jsonl(args.output, units)
summary = {
"dag": str(args.dag),
"output": str(args.output),
"annotation_units": count,
"min_reachable_terminals": args.min_reachable_terminals,
"min_incoming_count": args.min_incoming_count,
"example_count": args.example_count,
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
|