#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 目的: VRChat表示名とTOML guest名を fuzzy matching する 特徴: - 記号除去 - 全角半角統一 - 小文字化 - 日本語 -> ローマ字変換 - rapidfuzz による類似度 - ambiguous 判定 - テストケース付き 前提: pip install rapidfuzz pykakasi 期待結果: - 類似候補を score 順に表示 - 危険な曖昧ケースを検知 """ import re import sys import unicodedata from typing import List, Tuple try: from rapidfuzz import fuzz from rapidfuzz import process except Exception as e: print(f"[FATAL] rapidfuzz import failed: {e}") input("Press Enter to exit...") sys.exit(1) try: from pykakasi import kakasi except Exception as e: print(f"[FATAL] pykakasi import failed: {e}") input("Press Enter to exit...") sys.exit(1) # ========================= # 設定 # ========================= AUTO_LINK_SCORE = 95 AMBIGUOUS_DIFF = 10 TEST_GUESTS = [ "okada", "tanaka", "yamada", "messykenty", ] TEST_CURRENT_USERS = [ "岡田", "ikada", "okada_", "OKADA", "tanaka!!!", "田中", "やまだ", "messy_kenty", "messy.kenty", "MESSYKENTY", "ma", "めっしーけんてぃ", "山田", ] # ========================= # ローマ字変換 # ========================= kks = kakasi() def jp_to_romaji(text: str) -> str: try: converted = kks.convert(text) result = [] for item in converted: result.append(item["hepburn"]) return "".join(result) except Exception as e: print(f"[ERROR] romaji convert failed: {text} : {e}") return text # ========================= # 正規化 # ========================= def normalize_name(name: str) -> str: """ 正規化内容: - 全角半角統一 - 小文字化 - 日本語→ローマ字 - 記号除去 """ # Unicode normalize name = unicodedata.normalize("NFKC", name) # 小文字化 name = name.lower() # 日本語→ローマ字 name = jp_to_romaji(name) # 記号除去 name = re.sub(r"[^a-z0-9]", "", name) return name.strip() # ========================= # 類似候補検索 # ========================= def find_candidates( target_guest: str, current_users: List[str], ) -> List[Tuple[str, float]]: normalized_target = normalize_name(target_guest) normalized_map = {} for user in current_users: normalized_map[user] = normalize_name(user) results = [] for original_name, normalized_name in normalized_map.items(): score = fuzz.ratio( normalized_target, normalized_name, ) results.append((original_name, score)) results.sort(key=lambda x: x[1], reverse=True) return results # ========================= # 判定 # ========================= def evaluate_guest_match( guest_name: str, current_users: List[str], ): print("") print("=" * 60) print(f"[CHECK] guest={guest_name}") normalized_guest = normalize_name(guest_name) print(f"[INFO] normalized_guest={normalized_guest}") candidates = find_candidates( guest_name, current_users, ) if not candidates: print("[ERROR] no candidates") return print("") print("[CHECK] candidate list") for idx, (name, score) in enumerate(candidates[:5], start=1): print(f" {idx}. {name:<20} score={score:.2f}") top_name, top_score = candidates[0] second_score = 0 if len(candidates) >= 2: second_score = candidates[1][1] diff = top_score - second_score print("") print(f"[INFO] top_score={top_score:.2f}") print(f"[INFO] second_score={second_score:.2f}") print(f"[INFO] diff={diff:.2f}") # 自動確定 if ( top_score >= AUTO_LINK_SCORE and diff >= AMBIGUOUS_DIFF ): print("") print("[INFO] AUTO LINK SAFE") print(f"[INFO] linked => {top_name}") return # 曖昧 print("") print("[WARN] ambiguous match") print("[WARN] auto link disabled") print("") print("[ACTION] manual review required") # ========================= # main # ========================= def main(): print("[CHECK] normalize preview") for user in TEST_CURRENT_USERS: print( f"{user:<20}" f" => " f"{normalize_name(user)}" ) print("") print("[CHECK] guest matching test") for guest in TEST_GUESTS: evaluate_guest_match( guest, TEST_CURRENT_USERS, ) print("") print("[INFO] completed") if __name__ == "__main__": print("[CHECK] start") answer = input( "[ACTION] run fuzzy test? (yes/no): " ).strip().lower() if answer != "yes": print("[FATAL] cancelled") input("Press Enter to exit...") sys.exit(1) main()