게시판SGF 기보로 학습하는 바둑 LLM 인공지능예제 소스

예제 소스

SGF 기보로 학습하는 바둑 LLM 인공지능 · 파일 169개

watch_human_vs_vlm.py

#!/usr/bin/env python3
"""사람 vs VLM 실전 모니터 — serve jsonl + baduk 터미널만 따라간다."""

from __future__ import annotations

import json
import time
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
JSONL = ROOT / "logs/vlm_eval_day/play_r5.jsonl"
OUT_DIR = ROOT / "logs/live_human_vs_vlm"
LIVE = OUT_DIR / "LIVE.md"
EVENTS = OUT_DIR / "events.jsonl"
TERM_DIR = Path.home() / ".cursor/projects/Volumes-MY-WORK-mywork-rl-go/terminals"


def iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def coord(row, col, is_pass=False) -> str:
    if is_pass or row is None or col is None:
        return "PASS"
    letters = "ABCDEFGHJKLMNOPQRST"
    try:
        return f"{letters[int(col)]}{19 - int(row)} ({row},{col})"
    except Exception:
        return f"({row},{col})"


def side_name(ai_side) -> str:
    return "흑(B)" if int(ai_side) == 1 else "백(W)" if int(ai_side) == 2 else f"side={ai_side}"


def parse_term_header(path: Path) -> dict:
    meta = {}
    try:
        text = path.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return meta
    for line in text.splitlines()[:20]:
        if line.startswith("command:"):
            meta["command"] = line.split(":", 1)[1].strip().strip('"')
        if line.startswith("last_command:"):
            meta["last_command"] = line.split(":", 1)[1].strip()
        if line.startswith("title:"):
            meta["title"] = line.split(":", 1)[1].strip()
    return meta


def is_baduk_terminal(path: Path) -> bool:
    meta = parse_term_header(path)
    blob = " ".join(str(v) for v in meta.values())
    if "watch_human_vs_vlm" in blob or "daemon_text_client" in blob or "vlm_server serve" in blob:
        return False
    return "baduk" in blob.lower()


def write_live(moves: list[dict], qt_tail: list[str], note: str) -> None:
    lines = [
        f"# 사람 vs VLM 실전 — {iso()}",
        "",
        note,
        "",
        f"- VLM 응답 수(이번 세션): **{len(moves)}**",
        f"- jsonl: `{JSONL}`",
        "",
        "## VLM 수순",
        "",
    ]
    if not moves:
        lines.append("_아직 VLM 수가 없습니다. Qt에서 [시작] 후 두면 여기 쌓입니다._")
    else:
        lines.append("| # | 시각 | 색 | 좌표 | retry | illegal | urgent | Δ | prefer |")
        lines.append("|---|------|----|------|-------|---------|--------|---|---------|")
        for i, m in enumerate(moves, 1):
            ts = (m.get("ts") or "")[11:19]
            lines.append(
                "| {i} | {ts} | {side} | {c} | {retry} | {ill} | {urg} | {d} | {pref} |".format(
                    i=i,
                    ts=ts,
                    side=side_name(m.get("ai_side", 0)),
                    c=coord(m.get("row"), m.get("col"), bool(m.get("pass"))),
                    retry=m.get("retry_count", 0),
                    ill=m.get("illegal_reason") or "-",
                    urg="Y" if m.get("urgent_atari") else "-",
                    d=m.get("score_delta", "-"),
                    pref=m.get("vlm_prefer") or "-",
                )
            )
    lines += ["", "## Qt 새 로그", ""]
    if qt_tail:
        lines += [f"- `{x}`" for x in qt_tail[-24:]]
    else:
        lines.append(
            "_baduk 터미널 대기 중. Cursor 터미널에서 "
            "`./build/baduk.app/Contents/MacOS/baduk` 실행 후 "
            "흑=사람 / 백=VLM → [시작]._"
        )
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    LIVE.write_text("\n".join(lines) + "\n", encoding="utf-8")


def main() -> int:
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    baseline = 0
    if JSONL.exists():
        with JSONL.open(encoding="utf-8") as f:
            baseline = sum(1 for _ in f)
    (OUT_DIR / "baseline.txt").write_text(str(baseline), encoding="utf-8")
    print(f"[watch] baseline jsonl lines={baseline}", flush=True)
    print("[watch] serve 유지 — 사람 vs VLM 대기 (baduk 터미널만 추적)", flush=True)

    moves: list[dict] = []
    qt_offsets: dict[str, int] = {}
    qt_recent: list[str] = []
    last_size = JSONL.stat().st_size if JSONL.exists() else 0
    write_live(moves, [], f"- **state**: waiting  baseline=`{baseline}`  mode=`흑=휴리스틱 / 백=VLM`")

    while True:
        qt_path = None
        if TERM_DIR.is_dir():
            for p in sorted(TERM_DIR.glob("*.txt"), key=lambda x: x.stat().st_mtime, reverse=True):
                if is_baduk_terminal(p):
                    qt_path = p
                    break

        if qt_path is not None:
            key = str(qt_path)
            data = qt_path.read_bytes()
            off = qt_offsets.get(key)
            if off is None:
                qt_offsets[key] = len(data)
            elif len(data) < off:
                qt_offsets[key] = 0
                off = 0
            if off is not None and len(data) > off:
                chunk = data[off:].decode("utf-8", errors="replace")
                qt_offsets[key] = len(data)
                for raw in chunk.splitlines():
                    s = raw.strip()
                    if not s or s.startswith("---"):
                        continue
                    interesting = (
                        "[VLM]" in s
                        or "VLM " in s
                        or "거부" in s
                        or "PASS" in s
                        or "시작" in s
                        or "계가" in s
                        or "기권" in s
                        or "패(" in s
                        or "atari" in s.lower()
                        or "low-line" in s
                        or "selfFill" in s
                        or "아다리" in s
                    )
                    if interesting:
                        qt_recent.append(s)
                        if len(qt_recent) > 80:
                            qt_recent = qt_recent[-80:]
                        print(f"[qt] {s}", flush=True)

        if JSONL.exists():
            size = JSONL.stat().st_size
            if size < last_size:
                last_size = size
            if size > last_size:
                with JSONL.open(encoding="utf-8") as f:
                    f.seek(last_size)
                    chunk = f.read()
                last_size = size
                for line in chunk.splitlines():
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        rec = json.loads(line)
                    except json.JSONDecodeError:
                        continue
                    if rec.get("event") != "move":
                        continue
                    moves.append(rec)
                    with EVENTS.open("a", encoding="utf-8") as ef:
                        ef.write(json.dumps(rec, ensure_ascii=False) + "\n")
                    print(
                        "[vlm] {side} {c} retry={r} illegal={ill} urgent={u} Δ={d} prefer={p}".format(
                            side=side_name(rec.get("ai_side", 0)),
                            c=coord(rec.get("row"), rec.get("col"), bool(rec.get("pass"))),
                            r=rec.get("retry_count", 0),
                            ill=rec.get("illegal_reason") or "-",
                            u="Y" if rec.get("urgent_atari") else "-",
                            d=rec.get("score_delta", "-"),
                            p=rec.get("vlm_prefer") or "-",
                        ),
                        flush=True,
                    )

        write_live(
            moves,
            qt_recent,
            f"- **state**: watching  baseline=`{baseline}`  qt=`{qt_path.name if qt_path else '-'}`  mode=`흑=휴리스틱 / 백=VLM`",
        )
        time.sleep(0.8)


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