File size: 5,786 Bytes
2e3e276 ceb51d3 2e3e276 ceb51d3 2e3e276 ceb51d3 2e3e276 ceb51d3 2e3e276 bc06d0b 2e3e276 bc06d0b 2e3e276 bc06d0b ceb51d3 2e3e276 ceb51d3 2e3e276 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | from __future__ import annotations
import html
import json
import os
import subprocess
import sys
import threading
import time
from contextlib import asynccontextmanager
from pathlib import Path
import psutil
from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse
from huggingface_hub import HfApi
from trainer.common import ARTIFACT_ROOT, LOG_ROOT, STATUS_ROOT, ensure_dirs, read_json
PIPELINE_PID = STATUS_ROOT / "pipeline.pid"
PIPELINE_LOG = LOG_ROOT / "pipeline.log"
def cost_watchdog() -> None:
maximum = int(os.environ.get("MAX_PAID_SECONDS", "0"))
token = os.environ.get("TRAINING_HF_TOKEN")
space_id = os.environ.get("SPACE_ID")
if maximum <= 0 or not token or not space_id:
return
time.sleep(maximum)
try:
HfApi(token=token).pause_space(space_id)
except Exception as exc:
print(f"Cost watchdog could not pause the Space: {exc}", flush=True)
def pipeline_running() -> bool:
try:
pid = int(PIPELINE_PID.read_text(encoding="utf-8").strip())
return psutil.pid_exists(pid)
except (FileNotFoundError, ValueError):
return False
def start_pipeline() -> None:
ensure_dirs()
if pipeline_running():
return
status = read_json(STATUS_ROOT / "status.json", {}) or {}
if status.get("phase") == "complete" and os.environ.get("FORCE_RETRAIN", "0") != "1":
return
log_handle = PIPELINE_LOG.open("a", encoding="utf-8", buffering=1)
process = subprocess.Popen(
[sys.executable, "-m", "trainer.run_pipeline"],
cwd=Path(__file__).resolve().parent,
stdout=log_handle,
stderr=subprocess.STDOUT,
start_new_session=True,
)
PIPELINE_PID.write_text(str(process.pid), encoding="utf-8")
def tail(path: Path, max_bytes: int = 30000) -> str:
if not path.exists():
return "No logs yet."
with path.open("rb") as handle:
handle.seek(0, 2)
size = handle.tell()
handle.seek(max(0, size - max_bytes))
return handle.read().decode("utf-8", errors="replace")
@asynccontextmanager
async def lifespan(_: FastAPI):
ensure_dirs()
threading.Thread(target=cost_watchdog, name="cost-watchdog", daemon=True).start()
if os.environ.get("AUTO_START", "1") == "1":
start_pipeline()
yield
app = FastAPI(title="SAMS Qwen v7 Approved-Wording Evaluator", lifespan=lifespan)
@app.get("/health")
def health() -> dict[str, object]:
return {"ok": True, "pipeline_running": pipeline_running()}
@app.get("/api/status")
def status() -> JSONResponse:
payload = read_json(STATUS_ROOT / "status.json", {}) or {"phase": "starting"}
payload["pipeline_running"] = pipeline_running()
payload["persistent_root"] = str(STATUS_ROOT.parent)
return JSONResponse(payload)
@app.get("/api/artifacts")
def artifact_index() -> JSONResponse:
items = []
for path in sorted(ARTIFACT_ROOT.rglob("*")):
if path.is_file():
relative = path.relative_to(ARTIFACT_ROOT).as_posix()
items.append({"path": relative, "size": path.stat().st_size, "url": f"/download/{relative}"})
return JSONResponse({"artifacts": items})
@app.get("/download/{artifact_path:path}")
def download_artifact(artifact_path: str) -> FileResponse:
requested = (ARTIFACT_ROOT / artifact_path).resolve()
root = ARTIFACT_ROOT.resolve()
if requested != root and root not in requested.parents:
raise HTTPException(status_code=400, detail="Invalid artifact path")
if not requested.is_file():
raise HTTPException(status_code=404, detail="Artifact not found")
return FileResponse(requested, filename=requested.name)
@app.get("/logs")
def logs() -> PlainTextResponse:
return PlainTextResponse(tail(PIPELINE_LOG))
@app.get("/")
def index() -> HTMLResponse:
status = read_json(STATUS_ROOT / "status.json", {}) or {"phase": "starting", "message": "Initializing"}
artifacts = sorted(str(path.relative_to(ARTIFACT_ROOT)) for path in ARTIFACT_ROOT.rglob("*") if path.is_file())
body = f"""
<!doctype html>
<html><head><meta charset="utf-8"><meta http-equiv="refresh" content="30">
<title>SAMS Qwen v7 Approved-Wording Evaluator</title>
<style>
body {{ font-family: system-ui, sans-serif; max-width: 1050px; margin: 2rem auto; padding: 0 1rem; background:#0b1020; color:#eef2ff; }}
.card {{ background:#151c33; border:1px solid #2c385f; border-radius:14px; padding:1rem 1.2rem; margin:1rem 0; }}
code, pre {{ background:#090d18; border-radius:8px; }} pre {{ padding:1rem; overflow:auto; max-height:34rem; white-space:pre-wrap; }}
.phase {{ color:#8dd8ff; font-size:1.4rem; font-weight:700; }} a {{ color:#9bd2ff; }}
</style></head><body>
<h1>SAMS Qwen3-1.7B approved-wording selector v7</h1>
<p>Source-grounded synthetic data requiring domain review. Safety decisions bypass this model.</p>
<div class="card"><div class="phase">{html.escape(str(status.get('phase', 'unknown')))}</div>
<p>{html.escape(str(status.get('message', '')))}</p>
<p>Updated: {html.escape(str(status.get('updated_at', 'not yet')))} - Process running: {pipeline_running()}</p></div>
<div class="card"><h2>Status</h2><pre>{html.escape(json.dumps(status, indent=2, sort_keys=True))}</pre></div>
<div class="card"><h2>Artifacts</h2><pre>{html.escape(chr(10).join(artifacts) or 'None yet')}</pre></div>
<div class="card"><h2>Recent pipeline log</h2><pre>{html.escape(tail(PIPELINE_LOG))}</pre></div>
</body></html>
"""
return HTMLResponse(body)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "7860")))
|