apocaliss92's picture
camstack-train 0.1.0: YOLOv9 root detector fine-tune (job.json in, result.json + bundle.tar out)
ebeed67 verified
Raw History Blame Contribute Delete
2.71 kB
"""``CAMSTACK_EVENT {json}`` progress lines on stdout.
The hub reads the provider's log and parses these lines; it never needs to
understand anything else the container prints. Every event carries ``v`` (the
event format version, 1), ``ts`` (epoch ms) and ``type``. One event per line,
compact JSON, flushed immediately.
"""
from __future__ import annotations
import json
import sys
import time
from typing import Any, Callable, Optional, TextIO
PREFIX = "CAMSTACK_EVENT "
EVENT_VERSION = 1
STAGES = ("prepare", "replay", "train", "export", "eval", "package")
class Emitter:
def __init__(
self,
stream: Optional[TextIO] = None,
clock: Callable[[], float] = time.time,
) -> None:
self._stream = stream
self._clock = clock
def emit(self, event_type: str, **fields: Any) -> dict[str, Any]:
event: dict[str, Any] = {
"v": EVENT_VERSION,
"ts": int(self._clock() * 1000),
"type": event_type,
**fields,
}
stream = self._stream if self._stream is not None else sys.stdout
stream.write(PREFIX + json.dumps(event, separators=(",", ":"), sort_keys=False) + "\n")
stream.flush()
return event
def stage(self, stage: str) -> None:
if stage not in STAGES:
raise ValueError(f"unknown stage {stage!r}")
self.emit("stage", stage=stage)
def epoch(
self,
epoch: int,
epochs: int,
box_loss: Optional[float],
map50: Optional[float],
map50_95: Optional[float],
eta_sec: Optional[float],
) -> None:
self.emit(
"epoch",
epoch=epoch,
epochs=epochs,
boxLoss=box_loss,
map50=map50,
map50_95=map50_95,
etaSec=eta_sec,
)
def metric(self, name: str, value: float) -> None:
self.emit("metric", name=name, value=value)
def artifact(self, path: str, size: int, sha256: str) -> None:
self.emit("artifact", path=path, bytes=size, sha256=sha256)
def error(self, stage: str, code: str, message: str) -> None:
self.emit("error", stage=stage, code=code, message=message)
def done(self, status: str, result_sha256: Optional[str]) -> None:
self.emit("done", status=status, resultSha256=result_sha256)
def parse_line(line: str) -> Optional[dict[str, Any]]:
"""The event on a log line, or ``None`` for any other line."""
if not line.startswith(PREFIX):
return None
try:
value = json.loads(line[len(PREFIX):])
except json.JSONDecodeError:
return None
return value if isinstance(value, dict) else None