Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .claude/skills/run-analysis/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,22 @@ of every segment (Read the PNGs — never ship an unviewed render).
data in `src/data.json` regenerated from `leaderboard.json`). Assets must live under
`public/` (staticFile) — junctions break the bundler.
- Timelapse source: `ffmpeg -i master.mkv -vf "setpts=PTS/60,fps=24,crop=trunc(iw/2)*2:trunc(ih/2)*2" -an`.
- **Dashboard clip rig** (source: `scripts/record_dashboard.mjs`) — headless Playwright
recording, never screen capture. Four processes, in order:
- **Dashboard clip rig** (source: `D:\RLE_media\capture\record_dashboard.mjs` — the
rle-media repo, which is the set-up scratch dir with playwright already installed) —
headless Playwright recording, never screen capture. Four processes, in order:
1. RimWorld + RIMAPI up (the dashboard's connect gate probes :8765; CORS is fine, but
the URL field starts EMPTY — the script fills it and clicks Connect).
2. `./.venv/Scripts/python scripts/serve_dashboard.py results/replay` (CORS server :9000).
3. Dashboard: `PORT=3001 BROWSER=none bun run start` in rimapi-dashboard — NOT :3000
(Remotion's render server squats on 3000; craco exits "already running" instead of failing loud).
4. `python scripts/replay_ticks.py --model <name> --interval 2 --loop` (feeds
results/replay/latest_tick.json from the run's tick_snapshots).
Then record: set up a scratch dir once (`bun add playwright && bunx playwright install
chromium`), copy record_dashboard.mjs next to it, and run with **node** — Playwright's
chromium launch hangs 180s under bun on Windows. The script seeds the 5 RLE widgets via
a localStorage `dashboard_presets` preset (fresh browser contexts have no layout, and the
RLE widgets aren't in the default). Record 1920x1080 and 1080x1920 passes; trim the
Then record from `D:\RLE_media\capture\` (playwright already installed there; if starting
fresh elsewhere, `bun add playwright && bunx playwright install chromium` first) and run
with **node** — Playwright's chromium launch hangs 180s under bun on Windows. The script
seeds the 5 RLE widgets via a localStorage `dashboard_presets` preset (the dashboard now
also ships built-in `RLE Capture`/`RLE Vertical` presets selectable via `?preset=`, and
`?api=<url>` skips the connect gate — either path works). Record 1920x1080 and 1080x1920 passes; trim the
connect/load preamble (~8s) when converting webm → mp4
(`ffmpeg -ss 8 -i in.webm -c:v libx264 -crf 19 -pix_fmt yuv420p -r 30 out.mp4`).
- Verify every render before calling it done: extract stills of each segment with ffmpeg
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RLE — RimWorld Learning Environment
# RLE — RimWorld Learning Environment

Multi-agent benchmark where 7 Felix Agent SDK role-specialized LLM agents manage a RimWorld colony. Think FLE (Factorio Learning Environment) but for multi-agent coordination under uncertainty.

Expand Down Expand Up @@ -382,9 +382,11 @@ scripts/
├── analyze_spread.py # Cross-model leaderboard vs baseline + failure taxonomy
├── visualize_results.py # Matplotlib CSV plotter
├── serve_dashboard.py # CORS-enabled file server for dashboard
├── obs_record.py # OBS recording start/stop/status via obs-websocket (per-model files)
├── obs_studio.py # OBS scene setup (Game/Dashboard/PiP/Vertical) + live score ticker
├── replay_ticks.py # Replay a run's tick snapshots into the dashboard (for capture)
├── record_dashboard.mjs # Headless Playwright dashboard recording (run with node)
└── capture_run_media.sh # During-run game-window stills + tick snapshots
# (post-run Playwright dashboard recording lives in the rle-media repo: capture/record_dashboard.mjs)
docker/
├── Dockerfile # HeadlessRim + Xvfb (debian:bookworm-slim)
├── docker-compose.yml # Volume mounts for game files, mods, saves
Expand Down
86 changes: 86 additions & 0 deletions scripts/obs_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# /// script
# dependencies = ["obsws-python>=1.7"]
# ///
"""Control OBS recording via obs-websocket (v5) for benchmark run capture.

Usage (run via uv so the dependency resolves without polluting the venv):
uv run scripts/obs_record.py status
uv run scripts/obs_record.py start --label fable5
uv run scripts/obs_record.py stop

The WebSocket password is read from the OBS_WS_PASSWORD env var, falling back
to OBS's own plugin config file. `start --label X` sets the recording filename
to ``rle_<label>_<timestamp>`` so per-model footage is self-identifying.

Requires OBS running with the WebSocket server enabled (Tools > WebSocket
Server Settings, port 4455).
"""

from __future__ import annotations

import argparse
import json
import os
import sys
from pathlib import Path

import obsws_python as obs


def _password() -> str:
env = os.environ.get("OBS_WS_PASSWORD")
if env:
return env
cfg = (
Path(os.environ["APPDATA"])
/ "obs-studio"
/ "plugin_config"
/ "obs-websocket"
/ "config.json"
)
data = json.loads(cfg.read_text(encoding="utf-8-sig"))
password: str = data["server_password"]
return password


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("command", choices=["start", "stop", "status"])
parser.add_argument("--label", default="", help="filename label for start")
parser.add_argument("--port", type=int, default=4455)
args = parser.parse_args()

client = obs.ReqClient(
host="localhost", port=args.port, password=_password(), timeout=10
)

status = client.get_record_status()
if args.command == "status":
print(f"recording={status.output_active} timecode={status.output_timecode}")
return 0

if args.command == "start":
if status.output_active:
print("already recording", file=sys.stderr)
return 1
if args.label:
client.set_profile_parameter(
"Output",
"FilenameFormatting",
f"rle_{args.label}_%CCYY-%MM-%DD_%hh-%mm-%ss",
)
client.start_record()
print(f"recording started label={args.label or '(default)'}")
return 0

# stop
if not status.output_active:
print("not recording", file=sys.stderr)
return 1
resp = client.stop_record()
print(f"recording stopped -> {resp.output_path}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading