stevenkhan commited on
Commit
165a194
·
verified ·
1 Parent(s): a2bfe50

Upload clashcr/core/recorder.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. clashcr/core/recorder.py +149 -0
clashcr/core/recorder.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Evidence recorder: save frames, timestamps, metadata, and optional labels.
2
+
3
+ Produces a recording directory with:
4
+ - frames/ raw screenshots (optional, can be lossless PNG or JPEG)
5
+ - metadata.jsonl per-frame metadata
6
+ - labels.csv user annotations (timestamp, frame, side, card_key, confidence, note)
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import csv
11
+ import json
12
+ import logging
13
+ import time
14
+ from dataclasses import dataclass, field, asdict
15
+ from pathlib import Path
16
+ from typing import Optional, Dict, Any
17
+
18
+ import cv2
19
+ import numpy as np
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ @dataclass
25
+ class FrameMetadata:
26
+ timestamp: float
27
+ frame_idx: int
28
+ window_title: str
29
+ window_hwnd: int
30
+ window_width: int
31
+ window_height: int
32
+ dpi_scale: float
33
+ battle_state: str
34
+ battle_confidence: float
35
+ mode_hint: str
36
+ arena_roi: Optional[tuple] = None
37
+ own_roi: Optional[tuple] = None
38
+ opponent_roi: Optional[tuple] = None
39
+
40
+
41
+ @dataclass
42
+ class LabelEntry:
43
+ timestamp: float
44
+ frame_idx: int
45
+ side: str # 'opponent', 'own', 'unknown'
46
+ card_key: str
47
+ confidence: float
48
+ manual_note: str = ""
49
+ source: str = "manual" # or 'auto' for model predictions
50
+
51
+
52
+ class EvidenceRecorder:
53
+ def __init__(self, output_dir: str, save_frames: bool = True,
54
+ jpeg_quality: int = 85, max_fps: float = 8.0):
55
+ self.output_dir = Path(output_dir)
56
+ self.save_frames = save_frames
57
+ self.jpeg_quality = jpeg_quality
58
+ self.max_fps = max_fps
59
+ self.min_interval = 1.0 / max_fps
60
+
61
+ self.frames_dir = self.output_dir / "frames"
62
+ self.metadata_path = self.output_dir / "metadata.jsonl"
63
+ self.labels_path = self.output_dir / "labels.csv"
64
+ self.predictions_path = self.output_dir / "predictions.csv"
65
+
66
+ self._frame_idx = 0
67
+ self._last_capture_time = 0.0
68
+ self._metadata_file = None
69
+ self._labels_file = None
70
+ self._labels_writer = None
71
+ self._predictions_file = None
72
+ self._predictions_writer = None
73
+ self._is_open = False
74
+
75
+ def open(self) -> None:
76
+ self.output_dir.mkdir(parents=True, exist_ok=True)
77
+ if self.save_frames:
78
+ self.frames_dir.mkdir(parents=True, exist_ok=True)
79
+
80
+ self._metadata_file = open(self.metadata_path, "w", encoding="utf-8")
81
+ self._labels_file = open(self.labels_path, "w", newline="", encoding="utf-8")
82
+ self._labels_writer = csv.writer(self._labels_file)
83
+ self._labels_writer.writerow(["timestamp", "frame_idx", "side", "card_key",
84
+ "confidence", "manual_note", "source"])
85
+
86
+ self._predictions_file = open(self.predictions_path, "w", newline="", encoding="utf-8")
87
+ self._predictions_writer = csv.writer(self._predictions_file)
88
+ self._predictions_writer.writerow(["timestamp", "frame_idx", "side", "card_key",
89
+ "confidence", "evidence", "resolver_reason"])
90
+ self._is_open = True
91
+ logger.info("Recording opened: %s", self.output_dir)
92
+
93
+ def close(self) -> None:
94
+ if self._metadata_file:
95
+ self._metadata_file.close()
96
+ if self._labels_file:
97
+ self._labels_file.close()
98
+ if self._predictions_file:
99
+ self._predictions_file.close()
100
+ self._is_open = False
101
+ logger.info("Recording closed: %s", self.output_dir)
102
+
103
+ def __enter__(self):
104
+ self.open()
105
+ return self
106
+
107
+ def __exit__(self, exc_type, exc_val, exc_tb):
108
+ self.close()
109
+ return False
110
+
111
+ def record_frame(self, frame: np.ndarray, meta: FrameMetadata) -> bool:
112
+ if not self._is_open:
113
+ return False
114
+ now = time.monotonic()
115
+ if now - self._last_capture_time < self.min_interval:
116
+ return False
117
+ self._last_capture_time = now
118
+
119
+ if self.save_frames:
120
+ path = self.frames_dir / f"frame_{self._frame_idx:06d}.jpg"
121
+ cv2.imwrite(str(path), frame, [cv2.IMWRITE_JPEG_QUALITY, self.jpeg_quality])
122
+
123
+ meta.frame_idx = self._frame_idx
124
+ self._metadata_file.write(json.dumps(asdict(meta)) + "\n")
125
+ self._frame_idx += 1
126
+ return True
127
+
128
+ def add_label(self, label: LabelEntry) -> None:
129
+ if not self._is_open or self._labels_writer is None:
130
+ return
131
+ self._labels_writer.writerow([
132
+ f"{label.timestamp:.3f}", label.frame_idx, label.side,
133
+ label.card_key, f"{label.confidence:.3f}", label.manual_note, label.source
134
+ ])
135
+ self._labels_file.flush()
136
+
137
+ def add_prediction(self, timestamp: float, frame_idx: int, side: str,
138
+ card_key: str, confidence: float, evidence: str,
139
+ resolver_reason: str) -> None:
140
+ if not self._is_open or self._predictions_writer is None:
141
+ return
142
+ self._predictions_writer.writerow([
143
+ f"{timestamp:.3f}", frame_idx, side, card_key,
144
+ f"{confidence:.3f}", evidence, resolver_reason
145
+ ])
146
+ self._predictions_file.flush()
147
+
148
+ def get_frame_path(self, frame_idx: int) -> Path:
149
+ return self.frames_dir / f"frame_{frame_idx:06d}.jpg"