"""Build per-character file index from English_JSON. For each character in Contacts.json, produces character_data/{id}.json with: mentions - direct conversation/chat files (filename matches character) references - other files where the character's name appears in content """ import json import os import re from pathlib import Path ROOT = Path(__file__).parent JSON_DIR = ROOT / "English_JSON" OUT_DIR = ROOT / "character_data" def _normalise_names(info: dict) -> list[str]: names = info.get("name", []) if isinstance(names, str): names = [names] if names else [] detective = info.get("detective_name") or "" if detective: names = list(names) + [detective] return [n for n in names if n] def load_characters() -> dict: """Merge all Contacts.json files into {id: {name, search_patterns, file_ids}}.""" chars: dict = {} for contacts_path in sorted(JSON_DIR.rglob("Contacts.json")): data = json.loads(contacts_path.read_text(encoding="utf-8")) for char_id, info in data.items(): if char_id in chars: continue names = _normalise_names(info) chars[char_id] = { "name": names, # Patterns for content search (word-boundary for plain words, substring otherwise) "search_patterns": [_make_pattern(n) for n in names], # Tokens to look for in filenames (ID + name variants, all lowercase) "file_ids": {char_id.lower()} | {n.lower() for n in names}, } return chars def _make_pattern(name: str) -> re.Pattern: if re.match(r"^[A-Za-z]+$", name): return re.compile(r"\b" + re.escape(name) + r"\b", re.IGNORECASE) return re.compile(re.escape(name), re.IGNORECASE) def _is_chat_file(path: Path) -> bool: return any(p in ("Conversations", "Filler Chats") for p in path.parts) def _filename_matches(stem: str, file_ids: set[str]) -> bool: stem_lower = stem.lower() # Try each identifier: exact match, ends-with, or space-separated token tokens = set(re.split(r"[^a-z0-9]+", stem_lower)) for fid in file_ids: if fid in tokens or stem_lower == fid or stem_lower.endswith(fid): return True return False def _flatten_strings(obj) -> list[str]: if isinstance(obj, str): return [obj] if isinstance(obj, list): out = [] for item in obj: out.extend(_flatten_strings(item)) return out if isinstance(obj, dict): out = [] for k, v in obj.items(): out.append(k) out.extend(_flatten_strings(v)) return out return [] def _content_matches(text: str, patterns: list[re.Pattern]) -> bool: return any(p.search(text) for p in patterns) def main() -> None: OUT_DIR.mkdir(exist_ok=True) chars = load_characters() results = { char_id: {"id": char_id, "name": info["name"], "mentions": [], "references": []} for char_id, info in chars.items() } for json_file in sorted(JSON_DIR.rglob("*.json")): rel_path = str(json_file.relative_to(ROOT)) is_chat = _is_chat_file(json_file) stem = json_file.stem try: data = json.loads(json_file.read_text(encoding="utf-8")) except Exception: continue # Flatten content once per file; reuse across all character checks flat_text = " ".join(_flatten_strings(data)) for char_id, info in chars.items(): if is_chat and _filename_matches(stem, info["file_ids"]): results[char_id]["mentions"].append(rel_path) elif info["search_patterns"] and _content_matches(flat_text, info["search_patterns"]): results[char_id]["references"].append(rel_path) for char_id, char_data in results.items(): out_path = OUT_DIR / f"{char_id}.json" out_path.write_text( json.dumps(char_data, indent=2, ensure_ascii=False), encoding="utf-8" ) print(f"Written {len(results)} character files to {OUT_DIR}/") if __name__ == "__main__": main()