Imported from Casboko/novelai-modbot (
AGENTS.md). Install upstream withnpx skills add Casboko/novelai-modbot. Copyright stays with the author.
Repository Guidelines
プロジェクト構成とモジュール
本リポジトリは Discord サーバー向けモデレーション bot の処理パイプラインを Python で実装しています。主要なモジュールは app/ 以下にまとまり、main.py が Discord クライアントとルールエンジンを起動します。p0_scan.py → cli_wd14.py → analysis_merge.py → triage.py の順で添付画像取得から検知レポート生成までを担当します。
configs/にはルール定義rules.yamlや NudeNet/交差シグナル構成があり、運用パラメータ調整の際に編集します。models/wd14/は WD14 EVA02 重みとラベル情報のキャッシュ置き場です。初回推論時に自動的にダウンロードされ、差し替えはリビジョン指定で制御します。out/profiles/<profile>/が各フェーズ(p0〜p3)の成果物を保持する標準ワークスペースです。currentプロファイルが即応運用、legacyが過去アーカイブ用として利用されます。docs/とルート直下の P0/P1/P2 文書は運用手順の補足資料であり、作業スケジュールやエスカレーションの参考になります。- 依存は
requirements.base.txt(共通)とrequirements-cpu.txt/requirements-gpu.txt(いずれか片方を使用)に分離されています。requirements.txtは CPU プロファイルを指し、app/cache_*.sqliteが推論キャッシュです。削除すると再推論が走ります。
ビルド・テスト・開発コマンド
python -m app.p0_scan --profile current --date 2025-10-01: Discord から添付ファイルを収集し、out/profiles/current/p0/p0_2025-10-01.csvを生成します (.envのトークン必須)。python -m app.cli_wd14 --profile current --date 2025-10-01: WD14 推論を実行し、同日の p1 JSONL/メトリクスを自動出力します。python -m app.analysis_merge --profile current --date 2025-10-01: WD14 と NudeNet の結果を統合し、p2/p2_YYYY-MM-DD.jsonlに解析結果を保存します。python -m app.cli_scan --profile current --date 2025-10-01: 解析結果をルール評価し、p3/findings_YYYY-MM-DD.jsonlとメトリクスを生成します。python -m app.cli_report --profile current --date 2025-10-01 --severity red: 指定日の findings から CSV レポートを出力します。python -m app.cli_report --helpやpython -m app.cli_scan --helpで、--analysis-patternや--analysis-limit-daysなどのパーティション向けオプションを確認してください。- NSFW 辞書は
configs/rules.yamlのgroups.nsfw_generalを唯一の編集箇所とし、更新後は p1(必要な場合)→p2→p3 の順に再実行して反映を確認してください。
GPU 実行クイックスタート(WD14)
- 依存導入は必ずどちらか一方のみを選択します。
- CPU 環境:
pip install -r requirements-cpu.txt - GPU 環境 (CUDA/TensorRT):
pip install -r requirements-gpu.txt
- CPU 環境:
- 実行前に
python -c "import onnxruntime as ort; print(ort.get_available_providers())"でCUDAExecutionProviderやTensorrtExecutionProviderが列挙されることを確認してください。 - GPU 実行例:
python -m app.cli_wd14 --profile current --date 2025-10-01 --provider cuda --batch-size 48 --concurrency 24 --qps 4.0 WD14_PROVIDER(既定cpu)で CLI 未指定時のプロバイダを切り替えられます。環境に CUDA/TensorRT が存在しない場合は WARN を 1 度だけ出して自動的に CPU へフォールバックします。- メトリクス JSON (
--metrics) にはinfer_ms_avgとimg_per_secが追加されており、CPU/GPU の性能比較に利用できます。プロファイルがlegacyの場合はキャッシュ DB が自動的に分岐されます(必要に応じてWD14_CACHE_SUFFIXで明示切替も可能)。
シャーディング実行フロー(20,000枚規模)
- 分割:
python scripts/split_index.py --profile current --date 2025-10-01 --shards 10 - p1 (WD14):
python scripts/run_p1_sharded.py \ --profile current \ --date 2025-10-01 \ --shard-glob "out/profiles/current/p0/shards/shard_*.csv" \ --provider cuda --batch-size 48 --concurrency 24 --qps 4.0 \ --parallel 2 --resume.tmpファイルは完了時に自動 rename(失敗時は残置)。--resumeを付けると既存の最終ファイルをスキップし、途中停止後の再開が可能です。- 429 や 5xx が多い場合は
--qpsを下げるか--parallel 1に落として調整します。
- p2 (analysis merge):
python scripts/run_p2_sharded.py \ --profile current \ --date 2025-10-01 \ --shard-glob "out/profiles/current/p0/shards/shard_*.csv" \ --qps 4.0 --concurrency 16 \ --parallel 2 --resume \ --extra-args "--nudenet-mode auto"- ランナーは
--rules-config configs/rules_v2.yamlを自動付与します。別の辞書を使う場合は--extra-argsで明示してください。
- ランナーは
- マージ(任意):
python scripts/merge_jsonl.py --glob "out/p1/p1_wd14_*.jsonl" --out out/p1/p1_wd14_all.jsonl python scripts/merge_jsonl.py --glob "out/p2/p2_analysis_*.jsonl" --out out/p2/p2_analysis_all.jsonl
- 各ランナーは manifest JSON を更新し、
queued/running/done/failedを追跡します。ファイルはout/status/に保存され、再実行時も引き継がれます。 - SQLite キャッシュは WAL + 60 秒 timeout に設定済みですが、ロック待ちが発生する場合は
--parallel 1に落として運用してください。 - 失敗 shard は末尾で 1 回リトライします。複数回失敗する場合は manifest を確認し、
--resume付きでもう一度実行すると該当 shard のみが再試行されます。 --extra-args "--nudenet-mode auto"を指定すると、p2 での NudeNet 実行を WD14 スコアに基づいてゲートできます。auto(既定)はrating.questionable ≥ 0.35またはrating.explicit ≥ 0.20のときのみ推論を実行し、alwaysは全件実行、neverはキャッシュヒット以外をスキップします。- メトリクスには従来の
average_nudenet_latency_ms/from_cacheに加えて、nudenet.executed/nudenet.skipped/nudenet.p95_latency_msなどの詳細指標が出力されます。ゲート調整時はnudenet.executedとnudenet.skippedを並行して確認してください。
Runpod バッチ運用
- Runpod 上での大規模加工手順は
docs/runpod_batch_runbook.mdを参照してください。 - Network Volume, S3 同期、レート制御、A/B 比較、レビュー用 CSV の出力までを一冊にまとめています。
コーディングスタイルと命名規約
Python 3.11 を想定し、PEP 8 準拠の 4 スペースインデントを徹底してください。関数・変数は snake_case、クラスは PascalCase で統一し、型ヒントと from __future__ import annotations を前提に遅延評価を維持します。自動整形ツールは同梱されていないため、ruff や black をローカルで実行し差分がない状態でコミットしてください。非同期処理では asyncio ループのキャンセル管理が重要なので、タイムアウト値や RateLimiter 利用箇所の命名を明確にしましょう。
テスト指針
現時点で自動テストスイートは未整備です。新規機能を追加する際は tests/ ディレクトリを作成し pytest ベースのテストを導入してください。I/O を伴う処理は Discord クライアントや Hugging Face API をモックし、副作用のないユニットテストを優先します。主要フローごとに test_<module>.py 形式で命名し、スキャンからルール評価までのハッピーパスと代表的なエラーケースを最低限カバーしてください。回帰を防ぐため、重い推論は monkeypatch でキャッシュ層にスタブを挿入し、データサンプルは tests/fixtures/ に配置しましょう。
DSL レイヤ導入の進め方
configs/rules.yamlにversion: 2を記載すると DSL が有効になり、既存ロジックと併用されます(未指定/1は従来どおり)。- DSL では
groups(タグ/ワイルドカード定義)、features(中間式)、rules(when/reasons)を記述します。例:version: 2 groups: nsfw_general: ["bikini", "see_through", "underboob"] features: combo: "rating.explicit * exposure_peak" rules: - id: RED-NSFW-101 severity: red priority: 10 when: "(!channel.is_nsfw) && (rating.explicit >= 0.6 || sum('nsfw_general') >= 0.25)" reasons: ["exp={rating.explicit:.2f}", "sum={sum('nsfw_general'):.2f}"] - 利用可能な変数・関数
- 変数:
rating.*,exposure_peak,minors_peak,channel.is_nsfw,message.is_spoiler,attachment_countなど(既存メトリクスはmetrics経由で DSL にバインド済み)。 - 関数:
score(tag),sum(group),max(group),any(group, gt=0.35),count(group, gt=0.35),topk_sum(group, k, gt=0.35),clamp(x, lo, hi),nude.has(flag),nude.any(prefix="EXPOSED_", min=1)など。 - 新規メトリクス:
exposure_area/exposure_count(analysis_mergeがnudity_area_ratio/nudity_box_countを計上した値)。
- 変数:
app/engine/配下に Safe AST ベースの DSL ランタイムを実装済み。DslProgram.evaluate()の結果はmetrics.dsl/metrics.winningに格納され、最終判定は DSL ベースで一貫します(legacy は--allow-legacy併用時のフォールバックのみ)。- 厳格モードが必要な場合は
dsl_mode: strictをrules.yamlに追加すると未知変数やゼロ除算で即エラーになります(既定はwarnモードで 0/False にフォールバック)。 - DSL の単体テストは
tests/engine/test_dsl_program.pyを参考に追加してください。主要ケース(命中、非命中、安全性と禁止ノード)を網羅することが推奨です。
p3 スキャン(DSL対応)
- 本番実行例:
python -m app.cli_scan \\ --analysis out/p2/p2_analysis_all.jsonl \\ --findings out/p3/findings.jsonl \\ --rules configs/rules.yaml \\ --metrics out/metrics/p3_run.json - ドライラン(結果を出さずメトリクスのみ確認):
python -m app.cli_scan \\ --analysis out/p2/p2_analysis_all.jsonl \\ --rules configs/rules.yaml \\ --metrics out/metrics/p3_dry.json \\ --dry-run --print-configで DSL グループ/ルール数を要約表示できます。--limit/--offsetを使うとサンプリング実行が可能です。
A/B 比較フロー
- 現行ルール(A)と候補ルール(B)を用意
- A/B を実行し、差分を確認
python -m app.cli_rules_ab \\ --analysis out/p2/p2_analysis_all.jsonl \\ --rulesA configs/rules.yaml \\ --rulesB configs/rules_v2.yaml \\ --out-dir out/exports \\ --sample-diff 200 \\ --samples-minimal \\ --samples-redact-urls p3_ab_compare.jsonで counts/delta/混同行列、p3_ab_diff.csvで差分列、p3_ab_diff_samples.jsonlでレビュー用サンプルを確認- 差分レビュー後、合意したルールを
configs/rules.yamlに昇格
プロファイル運用ツール
scripts/profile_rotate.py --profile current --retention-days 14 --dry-run: 保持期間を超えたパーティションをlegacyへローテーションします(--dry-runで内容確認、--forceで上書き)。scripts/cache_clone.py --profile legacy: 共有キャッシュをプロファイル専用 DB に複製します。scripts/migrate_out_to_profiles.py --profile legacy --dry-run: 旧out/配下の成果物を新しいプロファイル構成へ移行します。
strict / warn モード
warn(既定): 未知識別子や式エラーは 0/False にフォールバックし WARN を 1ルール×エラー種あたり最大5回表示strict: ローダ/評価器のいずれかで例外化し、問題ルールを即時洗い出し- モードの優先順位は
--lock-mode> CLI--dsl-mode> ENVMODBOT_DSL_MODE> YAMLdsl_mode> warn です。--lock-modeを使うと A/B 両側を同一ポリシーで強制実行します。 - strict を常用したい場合は
MODBOT_DSL_MODE=strictを環境変数に設定するか、CLI で--dsl-mode strictを指定してください。両者を固定したい場合は--lock-mode strictを利用してください。 - legacy ルール(
version: 1)が混ざった場合は既定で比較を停止 (exit code 2) します。レビューだけ続行したい場合は--allow-legacyを付けるとp3_ab_compare.jsonにnote="skipped due to legacy ruleset"が追記され、CSV/サンプルは生成されません。 --samples-minimalを付けるとサンプル JSONL が最小ビュー(severity/rule/reasons/metrics のみ)で出力されます。--samples-redact-urlsを併用すると URL フィールドはnull、文中の URL は[URL]に置換されます。--out-dir DIRを指定するとDIR/p3_ab_compare.json・DIR/p3_ab_diff.csv・DIR/p3_ab_diff_samples.jsonlが自動生成されます(個別指定よりも運用が容易)。- レガシー構成(
version: 1)が残っている場合は--allow-legacy --fallback greenで強制的にseverity=greenとして出力できます(もしくは--fallback skipで書き込みを抑止)。 - WARN が多い場合は strict で健全性を確認してから本番に適用してください
コミットとプルリクエスト
コミットメッセージは feat(scope): summary 形式の Conventional Commits が利用されています。変更内容と影響範囲を明確にするため fix, chore, docs なども適宜活用してください。プルリクエストには目的、主な変更点、検証ログ (python -m app.cli_scan --help など) を記載し、関連 issue やチケット番号をリンクします。レビューではキャッシュ破棄の有無や Discord API 制限への影響を説明し、Discord 出力や通知内容を変更した場合はスクリーンショット、サンプルログ、生成 CSV の抜粋を添付してください。
セキュリティと設定
.env に DISCORD_BOT_TOKEN、GUILD_ID、LOG_CHANNEL_ID などの機密値を設定します。ファイルは git 管理外で保持し、共有は安全な秘密情報マネージャー経由で行ってください。外部モデルを更新する際は configs/ と models/ の差分を確認し、秘匿情報を誤ってコミットしないよう git status と .gitignore を実行前に点検しましょう。ローカル検証ではテスト用トークンを使用し、本番トークンは CI やホスティング環境のシークレットストアに限定してください。