Refine VRC OSC project architecture
This commit is contained in:
@@ -4,6 +4,12 @@ VRChat の OSC パラメータを受けて、Discord ミュート、VRChat ロ
|
|||||||
|
|
||||||
## 起動
|
## 起動
|
||||||
|
|
||||||
|
依存関係を入れる。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
PowerShell から起動する。
|
PowerShell から起動する。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -18,6 +24,23 @@ python src\bin\vrc_osc.py
|
|||||||
|
|
||||||
OSC は `127.0.0.1:9001` で待ち受ける。
|
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 に入れない。
|
実設定は `config/config.toml` に置く。このファイルは Git に入れない。
|
||||||
@@ -110,7 +133,7 @@ password = ""
|
|||||||
|
|
||||||
`discord.webhook_url` は VRC ログ結果の Discord Webhook 通知に使う。
|
`discord.webhook_url` は VRC ログ結果の Discord Webhook 通知に使う。
|
||||||
|
|
||||||
`vrchat.username` / `vrchat.password` は `src/getUser.py` を使う場合だけ必要。
|
`vrchat.username` / `vrchat.password` は `src/tools/resolve_vrchat_user.py` を使う場合だけ必要。
|
||||||
|
|
||||||
## OSC パラメータ
|
## OSC パラメータ
|
||||||
|
|
||||||
|
|||||||
10
VRWT_Tool/VRC_OSC/requirements.txt
Normal file
10
VRWT_Tool/VRC_OSC/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
deep-translator
|
||||||
|
paddleocr
|
||||||
|
pillow
|
||||||
|
pyautogui
|
||||||
|
pygetwindow
|
||||||
|
pykakasi
|
||||||
|
pyperclip
|
||||||
|
python-osc
|
||||||
|
rapidfuzz
|
||||||
|
vrchatapi
|
||||||
@@ -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_",
|
|
||||||
"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()
|
|
||||||
@@ -13,7 +13,7 @@ if str(SRC_DIR) not in sys.path:
|
|||||||
sys.path.insert(0, str(SRC_DIR))
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
from osc.gateway import VrcOscGateway
|
from osc.gateway import VrcOscGateway
|
||||||
from discord.discord_actions import setDiscordMute
|
from discord_control.actions import setDiscordMute
|
||||||
from ocr.ocr_actions import runOcrFromScreen
|
from ocr.ocr_actions import runOcrFromScreen
|
||||||
from translate.translate_actions import saveTranslationText
|
from translate.translate_actions import saveTranslationText
|
||||||
from translate.translate_actions import translateTextToJapanese
|
from translate.translate_actions import translateTextToJapanese
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -6,7 +6,7 @@ SRC_DIR = Path(__file__).resolve().parents[1]
|
|||||||
if str(SRC_DIR) not in sys.path:
|
if str(SRC_DIR) not in sys.path:
|
||||||
sys.path.insert(0, str(SRC_DIR))
|
sys.path.insert(0, str(SRC_DIR))
|
||||||
|
|
||||||
from bin.app import main
|
from app import main
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
from bin.app import main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
|||||||
from common.config_loader import loadVrcLogConfig
|
from common.config_loader import loadVrcLogConfig
|
||||||
from common.config_loader import getSecretValue
|
from common.config_loader import getSecretValue
|
||||||
from common.runtime_log import appendRuntimeLog
|
from common.runtime_log import appendRuntimeLog
|
||||||
from discord.discord_actions import setDiscordMuted
|
from discord_control.actions import setDiscordMuted
|
||||||
|
|
||||||
_monitor_thread = None
|
_monitor_thread = None
|
||||||
_monitor_stop_event = threading.Event()
|
_monitor_stop_event = threading.Event()
|
||||||
|
|||||||
Reference in New Issue
Block a user