Move app settings into config files

This commit is contained in:
messypy
2026-06-03 21:56:45 +09:00
committed by every_holiday
parent 6f22223394
commit da8a9b3c3f
13 changed files with 159 additions and 166 deletions

View File

@@ -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

View File

@@ -3,6 +3,8 @@ __pycache__/
.env
config/config.toml
config/secrets.toml
config/guests.txt
runtime/
.vrchat_cookie.txt
src/.vrchat_cookie.txt

View File

@@ -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

View File

@@ -0,0 +1,3 @@
GuestUserA
GuestUserB
GuestUserC

View File

@@ -0,0 +1,6 @@
[discord]
webhook_url = ""
[vrchat]
username = ""
password = ""

View File

@@ -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`

View File

@@ -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),

View File

@@ -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

View File

@@ -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"

View File

@@ -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:

View File

@@ -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(

View File

@@ -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

View File

@@ -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()