Move app settings into config files
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
DISCORD_WEBHOOK_URL=
|
||||
VRCHAT_USERNAME=
|
||||
VRCHAT_PASSWORD=
|
||||
|
||||
OCR_CROP_LEFT=0.05
|
||||
OCR_CROP_TOP=0.35
|
||||
OCR_CROP_RIGHT=0.95
|
||||
OCR_CROP_BOTTOM=0.95
|
||||
OCR_PREPROCESS=1
|
||||
OCR_SCALE=1.5
|
||||
OCR_USE_ANGLE_CLS=0
|
||||
2
VRWT_Tool/VRC_OSC/.gitignore
vendored
2
VRWT_Tool/VRC_OSC/.gitignore
vendored
@@ -3,6 +3,8 @@ __pycache__/
|
||||
|
||||
.env
|
||||
config/config.toml
|
||||
config/secrets.toml
|
||||
config/guests.txt
|
||||
runtime/
|
||||
.vrchat_cookie.txt
|
||||
src/.vrchat_cookie.txt
|
||||
|
||||
@@ -5,7 +5,18 @@ name = ""
|
||||
names = []
|
||||
|
||||
[guest]
|
||||
names = []
|
||||
file = "config/guests.txt"
|
||||
|
||||
[notice]
|
||||
missing_count = 0
|
||||
|
||||
[ocr.crop]
|
||||
left = 0.05
|
||||
top = 0.35
|
||||
right = 0.95
|
||||
bottom = 0.95
|
||||
|
||||
[ocr]
|
||||
preprocess = true
|
||||
scale = 1.5
|
||||
use_angle_cls = false
|
||||
|
||||
3
VRWT_Tool/VRC_OSC/config/guests.example.txt
Normal file
3
VRWT_Tool/VRC_OSC/config/guests.example.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
GuestUserA
|
||||
GuestUserB
|
||||
GuestUserC
|
||||
6
VRWT_Tool/VRC_OSC/config/secrets.example.toml
Normal file
6
VRWT_Tool/VRC_OSC/config/secrets.example.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[discord]
|
||||
webhook_url = ""
|
||||
|
||||
[vrchat]
|
||||
username = ""
|
||||
password = ""
|
||||
@@ -34,65 +34,83 @@ names = [
|
||||
]
|
||||
|
||||
[guest]
|
||||
names = [
|
||||
"GuestUserA",
|
||||
"GuestUserB"
|
||||
]
|
||||
file = "config/guests.txt"
|
||||
|
||||
[notice]
|
||||
missing_count = 3
|
||||
|
||||
[ocr.crop]
|
||||
left = 0.05
|
||||
top = 0.35
|
||||
right = 0.95
|
||||
bottom = 0.95
|
||||
|
||||
[ocr]
|
||||
preprocess = true
|
||||
scale = 1.5
|
||||
use_angle_cls = false
|
||||
```
|
||||
|
||||
`self.name` は Discord 自動ミュート判定に使う。
|
||||
|
||||
`staff.names` は表示対象だが guest 人数には含めない。
|
||||
|
||||
`guest.names` は guest 人数の母数と在室判定に使う。
|
||||
`guest.file` は guest 名簿ファイル。guest 人数の母数と在室判定に使う。
|
||||
|
||||
`notice.missing_count` は「あと何人以内で全員集合か」の通知範囲。例えば guest が 23 人で `missing_count = 3` の場合、20〜22 人のときに未入室 guest を出す。
|
||||
|
||||
## 秘密情報
|
||||
|
||||
`.env` に置く。このファイルは Git に入れない。
|
||||
|
||||
雛形は `.env.example`。
|
||||
|
||||
```env
|
||||
DISCORD_WEBHOOK_URL=
|
||||
VRCHAT_USERNAME=
|
||||
VRCHAT_PASSWORD=
|
||||
|
||||
OCR_CROP_LEFT=0.05
|
||||
OCR_CROP_TOP=0.35
|
||||
OCR_CROP_RIGHT=0.95
|
||||
OCR_CROP_BOTTOM=0.95
|
||||
OCR_PREPROCESS=1
|
||||
OCR_SCALE=1.5
|
||||
OCR_USE_ANGLE_CLS=0
|
||||
```
|
||||
|
||||
`DISCORD_WEBHOOK_URL` は VRC ログ結果の Discord Webhook 通知に使う。
|
||||
|
||||
`VRCHAT_USERNAME` / `VRCHAT_PASSWORD` は `src/getUser.py` を使う場合だけ必要。
|
||||
|
||||
OCR 設定は速度と精度の調整用。
|
||||
|
||||
`OCR_CROP_*` は VRChat ウィンドウ内の相対座標。`0.0` から `1.0` で指定する。デフォルトは画面下側の中央寄りだけを読む。
|
||||
`ocr.crop` は VRChat ウィンドウ内の相対座標。`0.0` から `1.0` で指定する。デフォルトは画面下側の中央寄りだけを読む。
|
||||
|
||||
フル画面 OCR に戻す場合。
|
||||
|
||||
```env
|
||||
OCR_CROP_LEFT=0
|
||||
OCR_CROP_TOP=0
|
||||
OCR_CROP_RIGHT=1
|
||||
OCR_CROP_BOTTOM=1
|
||||
```toml
|
||||
[ocr.crop]
|
||||
left = 0
|
||||
top = 0
|
||||
right = 1
|
||||
bottom = 1
|
||||
```
|
||||
|
||||
`OCR_PREPROCESS=1` はグレースケール化、コントラスト補正、シャープ化、拡大を行う。
|
||||
`ocr.preprocess = true` はグレースケール化、コントラスト補正、シャープ化、拡大を行う。
|
||||
|
||||
`OCR_SCALE` は前処理時の拡大率。大きいほど小さい文字に強くなるが遅くなる。
|
||||
`ocr.scale` は前処理時の拡大率。大きいほど小さい文字に強くなるが遅くなる。
|
||||
|
||||
`OCR_USE_ANGLE_CLS=0` は角度分類を切って高速化する。縦書きや回転文字を読む必要がある場合だけ `1` にする。
|
||||
`ocr.use_angle_cls = false` は角度分類を切って高速化する。縦書きや回転文字を読む必要がある場合だけ `true` にする。
|
||||
|
||||
## guest 名簿
|
||||
|
||||
`config/guests.txt` に 1 行 1 名で書く。このファイルは Git に入れない。
|
||||
|
||||
雛形は `config/guests.example.txt`。
|
||||
|
||||
```text
|
||||
GuestUserA
|
||||
GuestUserB
|
||||
GuestUserC
|
||||
```
|
||||
|
||||
空行と `#` で始まる行は無視する。
|
||||
|
||||
## 秘密情報
|
||||
|
||||
秘密情報は `config/secrets.toml` に置く。このファイルは Git に入れない。
|
||||
|
||||
雛形は `config/secrets.example.toml`。
|
||||
|
||||
```toml
|
||||
[discord]
|
||||
webhook_url = ""
|
||||
|
||||
[vrchat]
|
||||
username = ""
|
||||
password = ""
|
||||
```
|
||||
|
||||
`discord.webhook_url` は VRC ログ結果の Discord Webhook 通知に使う。
|
||||
|
||||
`vrchat.username` / `vrchat.password` は `src/getUser.py` を使う場合だけ必要。
|
||||
|
||||
## OSC パラメータ
|
||||
|
||||
@@ -188,6 +206,8 @@ OCR text、翻訳 text、VRC ログ結果は `runtime/runtime.log` に追記す
|
||||
|
||||
- `.env`
|
||||
- `config/config.toml`
|
||||
- `config/secrets.toml`
|
||||
- `config/guests.txt`
|
||||
- `runtime/`
|
||||
- `__pycache__/`
|
||||
- `*.pyc`
|
||||
|
||||
@@ -5,7 +5,9 @@ from common.project_paths import ROOT_DIR
|
||||
|
||||
CONFIG_DIR = ROOT_DIR / "config"
|
||||
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
||||
SECRETS_FILE = CONFIG_DIR / "secrets.toml"
|
||||
DEFAULT_EVENT_FILE = CONFIG_DIR / "event.toml"
|
||||
DEFAULT_GUEST_FILE = CONFIG_DIR / "guests.txt"
|
||||
|
||||
|
||||
def loadTomlFile(path):
|
||||
@@ -26,6 +28,47 @@ def resolveConfigPath(value, default_path):
|
||||
return (ROOT_DIR / candidate).resolve()
|
||||
|
||||
|
||||
def loadNameListFile(path):
|
||||
if not path.exists():
|
||||
return []
|
||||
|
||||
names = []
|
||||
|
||||
for line in path.read_text(encoding="utf-8-sig", errors="replace").splitlines():
|
||||
name = line.strip()
|
||||
if not name or name.startswith("#"):
|
||||
continue
|
||||
names.append(name)
|
||||
|
||||
return names
|
||||
|
||||
|
||||
def loadSecretConfig():
|
||||
return loadTomlFile(SECRETS_FILE)
|
||||
|
||||
|
||||
def getSecretValue(section_name, key, default=""):
|
||||
secrets = loadSecretConfig()
|
||||
section = secrets.get(section_name, {})
|
||||
return str(section.get(key, default)).strip()
|
||||
|
||||
|
||||
def loadOcrConfig():
|
||||
config = loadTomlFile(CONFIG_FILE)
|
||||
ocr = config.get("ocr", {})
|
||||
crop = ocr.get("crop", {})
|
||||
|
||||
return {
|
||||
"crop_left": float(crop.get("left", 0.05)),
|
||||
"crop_top": float(crop.get("top", 0.35)),
|
||||
"crop_right": float(crop.get("right", 0.95)),
|
||||
"crop_bottom": float(crop.get("bottom", 0.95)),
|
||||
"preprocess": bool(ocr.get("preprocess", True)),
|
||||
"scale": float(ocr.get("scale", 1.5)),
|
||||
"use_angle_cls": bool(ocr.get("use_angle_cls", False)),
|
||||
}
|
||||
|
||||
|
||||
def loadVrcLogConfig():
|
||||
config = loadTomlFile(CONFIG_FILE)
|
||||
vrc_log = config.get("vrc_log", {})
|
||||
@@ -43,7 +86,18 @@ def loadVrcLogConfig():
|
||||
if not event_config:
|
||||
event_config = vrc_log or config
|
||||
|
||||
guest_names = event_config.get("guest", {}).get("names", [])
|
||||
guest_section = event_config.get("guest", {})
|
||||
guest_names = guest_section.get("names", [])
|
||||
guest_file_value = (
|
||||
guest_section.get("file")
|
||||
or event_config.get("guest_file")
|
||||
or config.get("guest_file")
|
||||
)
|
||||
guest_file = resolveConfigPath(guest_file_value, DEFAULT_GUEST_FILE)
|
||||
|
||||
if not guest_names:
|
||||
guest_names = loadNameListFile(guest_file)
|
||||
|
||||
staff_names = event_config.get("staff", {}).get("names", [])
|
||||
|
||||
if not guest_names and isinstance(event_config.get("guest_names"), list):
|
||||
@@ -68,6 +122,7 @@ def loadVrcLogConfig():
|
||||
return {
|
||||
"config_file": CONFIG_FILE,
|
||||
"event_file": event_file,
|
||||
"guest_file": guest_file,
|
||||
"guest_names": [str(name).strip() for name in guest_names if str(name).strip()],
|
||||
"staff_names": [str(name).strip() for name in staff_names if str(name).strip()],
|
||||
"missing_count": int(missing_count),
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import os
|
||||
|
||||
from common.project_paths import ENV_FILE
|
||||
|
||||
|
||||
def readDotEnvValue(key):
|
||||
if not ENV_FILE.exists():
|
||||
return ""
|
||||
|
||||
for line in ENV_FILE.read_text(encoding="utf-8-sig", errors="replace").splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if not stripped.startswith(key + "="):
|
||||
continue
|
||||
|
||||
value = stripped.split("=", 1)[1].strip()
|
||||
return value.strip('"').strip("'")
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def getEnvValue(key, default=""):
|
||||
value = os.environ.get(key, "").strip()
|
||||
if value:
|
||||
return value
|
||||
|
||||
value = readDotEnvValue(key).strip()
|
||||
if value:
|
||||
return value
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def getEnvBool(key, default=False):
|
||||
value = getEnvValue(key, "").strip().lower()
|
||||
if not value:
|
||||
return default
|
||||
|
||||
if value in ("1", "true", "yes", "on"):
|
||||
return True
|
||||
if value in ("0", "false", "no", "off"):
|
||||
return False
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def getEnvFloat(key, default=0.0):
|
||||
value = getEnvValue(key, "").strip()
|
||||
if not value:
|
||||
return default
|
||||
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return default
|
||||
@@ -4,4 +4,3 @@ SRC_DIR = Path(__file__).resolve().parents[1]
|
||||
ROOT_DIR = SRC_DIR.parent
|
||||
RUNTIME_DIR = ROOT_DIR / "runtime"
|
||||
RUNTIME_LOG_FILE = RUNTIME_DIR / "runtime.log"
|
||||
ENV_FILE = ROOT_DIR / ".env"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import re
|
||||
from http.cookiejar import MozillaCookieJar
|
||||
|
||||
@@ -9,6 +8,8 @@ from vrchatapi.exceptions import UnauthorizedException
|
||||
from vrchatapi.models.two_factor_auth_code import TwoFactorAuthCode
|
||||
from vrchatapi.models.two_factor_email_code import TwoFactorEmailCode
|
||||
|
||||
from common.config_loader import getSecretValue
|
||||
|
||||
COOKIE_FILE = ".vrchat_cookie.txt"
|
||||
|
||||
USER_AGENT = (
|
||||
@@ -17,29 +18,10 @@ USER_AGENT = (
|
||||
)
|
||||
|
||||
|
||||
def read_dotenv_value(key):
|
||||
env_path = ".env"
|
||||
if not os.path.exists(env_path):
|
||||
return ""
|
||||
|
||||
with open(env_path, "r", encoding="utf-8-sig", errors="replace") as file:
|
||||
for line in file:
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if not stripped.startswith(key + "="):
|
||||
continue
|
||||
|
||||
value = stripped.split("=", 1)[1].strip()
|
||||
return value.strip('"').strip("'")
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def get_required_secret(key):
|
||||
value = os.environ.get(key, "").strip() or read_dotenv_value(key).strip()
|
||||
def get_required_secret(section, key):
|
||||
value = getSecretValue(section, key)
|
||||
if not value:
|
||||
raise RuntimeError(f"{key} is not set")
|
||||
raise RuntimeError(f"{section}.{key} is not set in config/secrets.toml")
|
||||
return value
|
||||
|
||||
|
||||
@@ -126,8 +108,8 @@ def main():
|
||||
return
|
||||
|
||||
configuration = vrchatapi.Configuration(
|
||||
username=get_required_secret("VRCHAT_USERNAME"),
|
||||
password=get_required_secret("VRCHAT_PASSWORD"),
|
||||
username=get_required_secret("vrchat", "username"),
|
||||
password=get_required_secret("vrchat", "password"),
|
||||
)
|
||||
|
||||
with vrchatapi.ApiClient(configuration) as api_client:
|
||||
|
||||
@@ -14,8 +14,7 @@ from PIL import Image
|
||||
from PIL import ImageFilter
|
||||
from PIL import ImageOps
|
||||
|
||||
from common.env_config import getEnvBool
|
||||
from common.env_config import getEnvFloat
|
||||
from common.config_loader import loadOcrConfig
|
||||
from common.runtime_log import appendRuntimeLog
|
||||
from screen.screen_actions import captureOcrRegion
|
||||
|
||||
@@ -32,9 +31,10 @@ def getOcrEngine():
|
||||
|
||||
if _ocr_engine is None:
|
||||
log("INFO", "PaddleOCR initializing")
|
||||
config = loadOcrConfig()
|
||||
options = {
|
||||
"lang": "japan",
|
||||
"use_angle_cls": getEnvBool("OCR_USE_ANGLE_CLS", False),
|
||||
"use_angle_cls": config["use_angle_cls"],
|
||||
"show_log": False,
|
||||
}
|
||||
|
||||
@@ -112,7 +112,8 @@ def printOcrResult(text):
|
||||
|
||||
|
||||
def preprocessOcrImage(image_path):
|
||||
enabled = getEnvBool("OCR_PREPROCESS", True)
|
||||
config = loadOcrConfig()
|
||||
enabled = config["preprocess"]
|
||||
if not enabled:
|
||||
return image_path
|
||||
|
||||
@@ -122,7 +123,7 @@ def preprocessOcrImage(image_path):
|
||||
image = ImageOps.autocontrast(image)
|
||||
image = image.filter(ImageFilter.SHARPEN)
|
||||
|
||||
scale = max(1.0, min(2.0, getEnvFloat("OCR_SCALE", 1.5)))
|
||||
scale = max(1.0, min(2.0, config["scale"]))
|
||||
if scale > 1.0:
|
||||
width, height = image.size
|
||||
resampling = getattr(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pyautogui
|
||||
import pygetwindow as gw
|
||||
|
||||
from common.env_config import getEnvFloat
|
||||
from common.config_loader import loadOcrConfig
|
||||
from common.project_paths import RUNTIME_DIR
|
||||
|
||||
SCREENSHOT_PATH = RUNTIME_DIR / "latest_screenshot.png"
|
||||
@@ -100,13 +100,14 @@ def clampRatio(value):
|
||||
|
||||
def getOcrRegion():
|
||||
left, top, width, height = getVrchatRegion()
|
||||
config = loadOcrConfig()
|
||||
|
||||
# Default to the lower central area. It is much faster than OCRing the
|
||||
# whole VRChat window and usually contains chat/subtitle text.
|
||||
crop_left = clampRatio(getEnvFloat("OCR_CROP_LEFT", 0.05))
|
||||
crop_top = clampRatio(getEnvFloat("OCR_CROP_TOP", 0.35))
|
||||
crop_right = clampRatio(getEnvFloat("OCR_CROP_RIGHT", 0.95))
|
||||
crop_bottom = clampRatio(getEnvFloat("OCR_CROP_BOTTOM", 0.95))
|
||||
crop_left = clampRatio(config["crop_left"])
|
||||
crop_top = clampRatio(config["crop_top"])
|
||||
crop_right = clampRatio(config["crop_right"])
|
||||
crop_bottom = clampRatio(config["crop_bottom"])
|
||||
|
||||
if crop_right <= crop_left:
|
||||
crop_left = 0.0
|
||||
|
||||
@@ -9,7 +9,7 @@ from datetime import timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from common.config_loader import loadVrcLogConfig
|
||||
from common.project_paths import ENV_FILE
|
||||
from common.config_loader import getSecretValue
|
||||
from common.runtime_log import appendRuntimeLog
|
||||
from discord.discord_actions import setDiscordMuted
|
||||
|
||||
@@ -49,34 +49,14 @@ def log(level, message):
|
||||
print(f"[{level}] {now} {message}", flush=True)
|
||||
|
||||
|
||||
def readDotEnvValue(key):
|
||||
if not ENV_FILE.exists():
|
||||
return ""
|
||||
|
||||
for line in ENV_FILE.read_text(encoding="utf-8-sig", errors="replace").splitlines():
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
if not stripped.startswith(key + "="):
|
||||
continue
|
||||
|
||||
value = stripped.split("=", 1)[1].strip()
|
||||
return value.strip('"').strip("'")
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
def getWebhookUrl():
|
||||
webhook_url = os.environ.get("DISCORD_WEBHOOK_URL", "").strip()
|
||||
if webhook_url:
|
||||
return webhook_url
|
||||
return readDotEnvValue("DISCORD_WEBHOOK_URL")
|
||||
return getSecretValue("discord", "webhook_url")
|
||||
|
||||
|
||||
def sendWebhook(text):
|
||||
webhook_url = getWebhookUrl().strip()
|
||||
if not webhook_url:
|
||||
log("ERROR", f"DISCORD_WEBHOOK_URL が未設定です env_file={ENV_FILE}")
|
||||
log("ERROR", "Discord webhook URL is not set config=config/secrets.toml")
|
||||
return False
|
||||
|
||||
message = str(text).strip()
|
||||
|
||||
Reference in New Issue
Block a user