Refine VRC OSC project architecture

This commit is contained in:
messypy
2026-06-03 22:04:22 +09:00
committed by every_holiday
parent 6a2c9bb39c
commit 66f94eaa9e
10 changed files with 37 additions and 275 deletions

View File

@@ -4,6 +4,12 @@ VRChat の OSC パラメータを受けて、Discord ミュート、VRChat ロ
## 起動
依存関係を入れる。
```powershell
python -m pip install -r requirements.txt
```
PowerShell から起動する。
```powershell
@@ -18,6 +24,23 @@ python src\bin\vrc_osc.py
OSC は `127.0.0.1:9001` で待ち受ける。
## 構成
```text
bin/ PowerShell 起動スクリプト
config/ example とローカル設定置き場
src/app.py アプリ本体のエントリ
src/bin/ Python 起動スクリプト
src/common/ パス、設定ロード、runtime ログなどの共通基盤
src/discord_control/ Discord ウィンドウ操作
src/ocr/ OCR 実行
src/osc/ OSC gateway
src/screen/ VRChat ウィンドウ検出とキャプチャ
src/tools/ 単体実行用の補助ツール
src/translate/ 翻訳
src/vrc_log/ VRChat ログ解析と名簿照合
```
## 設定ファイル
実設定は `config/config.toml` に置く。このファイルは Git に入れない。
@@ -110,7 +133,7 @@ password = ""
`discord.webhook_url` は VRC ログ結果の Discord Webhook 通知に使う。
`vrchat.username` / `vrchat.password` は `src/getUser.py` を使う場合だけ必要。
`vrchat.username` / `vrchat.password` は `src/tools/resolve_vrchat_user.py` を使う場合だけ必要。
## OSC パラメータ

View File

@@ -0,0 +1,10 @@
deep-translator
paddleocr
pillow
pyautogui
pygetwindow
pykakasi
pyperclip
python-osc
rapidfuzz
vrchatapi

View File

@@ -1,265 +0,0 @@
#!/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_",
"",
"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()

View File

@@ -13,7 +13,7 @@ if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from osc.gateway import VrcOscGateway
from discord.discord_actions import setDiscordMute
from discord_control.actions import setDiscordMute
from ocr.ocr_actions import runOcrFromScreen
from translate.translate_actions import saveTranslationText
from translate.translate_actions import translateTextToJapanese

View File

@@ -1 +0,0 @@

View File

@@ -6,7 +6,7 @@ SRC_DIR = Path(__file__).resolve().parents[1]
if str(SRC_DIR) not in sys.path:
sys.path.insert(0, str(SRC_DIR))
from bin.app import main
from app import main
if __name__ == "__main__":

View File

@@ -1,5 +0,0 @@
from bin.app import main
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ from pathlib import Path
from common.config_loader import loadVrcLogConfig
from common.config_loader import getSecretValue
from common.runtime_log import appendRuntimeLog
from discord.discord_actions import setDiscordMuted
from discord_control.actions import setDiscordMuted
_monitor_thread = None
_monitor_stop_event = threading.Event()