Spaces:
Running
Running
Download runner.py from Telearn/Project_Demo: direct link, hf CLI and curl.
- Browser
- Download file 24.6 kB
-
https://huggingface.co/spaces/Telearn/Project_Demo/resolve/main/runner.py
- Command line
-
hf download hf://spaces/Telearn/Project_Demo/runner.py
-
curl -L -o runner.py https://huggingface.co/spaces/Telearn/Project_Demo/resolve/main/runner.py
24.6 kB
| """ | |
| Headless runner for student Pygame / OpenCV projects. | |
| Connects back to the server over a private Unix socket, monkeypatches | |
| display/input APIs, and streams JPEG frames + logs to the browser. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import io | |
| import json | |
| import os | |
| import queue | |
| import socket | |
| import sys | |
| import tempfile | |
| import threading | |
| import time | |
| import numpy as np | |
| # Parse CLI before importing pygame/opencv (env must be set first) | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--project_path", required=True, help="Path to the student's main.py") | |
| parser.add_argument("--socket-path", required=True, help="Private Unix socket used for IPC") | |
| parser.add_argument( | |
| "--models_dir", | |
| default=None, | |
| help="Shared MediaPipe / model assets directory", | |
| ) | |
| args = parser.parse_args() | |
| os.environ["SDL_VIDEODRIVER"] = "dummy" | |
| os.environ["SDL_AUDIODRIVER"] = "dummy" | |
| # ----------------- IPC ----------------- | |
| send_queue: queue.Queue = queue.Queue() | |
| cv2_keys: queue.Queue = queue.Queue() | |
| stdin_queue: queue.Queue = queue.Queue() | |
| # Adaptive frame pacing: start at ~30 FPS, back off under pressure | |
| _min_frame_interval = 0.033 | |
| _max_frame_interval = 0.08 | |
| _current_frame_interval = 0.033 | |
| JPEG_QUALITY = 55 | |
| class KeyStateWrapper: | |
| def __init__(self): | |
| self._keys = {} | |
| def __getitem__(self, key): | |
| return self._keys.get(key, False) | |
| def __setitem__(self, key, value): | |
| self._keys[key] = bool(value) | |
| def __len__(self): | |
| return 512 | |
| shared_keys = KeyStateWrapper() | |
| shared_mouse_pos = (0, 0) | |
| shared_mouse_buttons = (False, False, False) | |
| shared_webcam_frame = None | |
| last_pygame_frame_time = 0.0 | |
| last_cv2_frame_time = 0.0 | |
| import pygame # noqa: E402 — after SDL env | |
| KEY_MAP = { | |
| "ArrowLeft": pygame.K_LEFT, | |
| "ArrowRight": pygame.K_RIGHT, | |
| "ArrowUp": pygame.K_UP, | |
| "ArrowDown": pygame.K_DOWN, | |
| " ": pygame.K_SPACE, | |
| "Spacebar": pygame.K_SPACE, | |
| "Enter": pygame.K_RETURN, | |
| "Escape": pygame.K_ESCAPE, | |
| "Backspace": pygame.K_BACKSPACE, | |
| "Tab": pygame.K_TAB, | |
| "Shift": pygame.K_LSHIFT, | |
| "Control": pygame.K_LCTRL, | |
| "Alt": pygame.K_LALT, | |
| "a": pygame.K_a, "b": pygame.K_b, "c": pygame.K_c, "d": pygame.K_d, "e": pygame.K_e, | |
| "f": pygame.K_f, "g": pygame.K_g, "h": pygame.K_h, "i": pygame.K_i, "j": pygame.K_j, | |
| "k": pygame.K_k, "l": pygame.K_l, "m": pygame.K_m, "n": pygame.K_n, "o": pygame.K_o, | |
| "p": pygame.K_p, "q": pygame.K_q, "r": pygame.K_r, "s": pygame.K_s, "t": pygame.K_t, | |
| "u": pygame.K_u, "v": pygame.K_v, "w": pygame.K_w, "x": pygame.K_x, "y": pygame.K_y, | |
| "z": pygame.K_z, | |
| "0": pygame.K_0, "1": pygame.K_1, "2": pygame.K_2, "3": pygame.K_3, "4": pygame.K_4, | |
| "5": pygame.K_5, "6": pygame.K_6, "7": pygame.K_7, "8": pygame.K_8, "9": pygame.K_9, | |
| } | |
| def _adjust_frame_interval(queue_depth: int) -> None: | |
| global _current_frame_interval | |
| if queue_depth > 2: | |
| _current_frame_interval = min(_max_frame_interval, _current_frame_interval * 1.15) | |
| elif queue_depth == 0: | |
| _current_frame_interval = max(_min_frame_interval, _current_frame_interval * 0.95) | |
| # ----------------- private IPC threads ----------------- | |
| def reader_thread_func(sock): | |
| global shared_webcam_frame, shared_keys, shared_mouse_pos, shared_mouse_buttons | |
| print("[Runner-IPC] Reader thread started.") | |
| try: | |
| while True: | |
| type_bytes = sock.recv(4) | |
| if not type_bytes or len(type_bytes) < 4: | |
| break | |
| msg_type = type_bytes.decode("utf-8", errors="ignore") | |
| len_bytes = sock.recv(4) | |
| if not len_bytes or len(len_bytes) < 4: | |
| break | |
| length = int.from_bytes(len_bytes, byteorder="big") | |
| payload = bytearray() | |
| while len(payload) < length: | |
| chunk = sock.recv(length - len(payload)) | |
| if not chunk: | |
| break | |
| payload.extend(chunk) | |
| if len(payload) < length: | |
| break | |
| if msg_type == "KEYS": | |
| data = json.loads(payload.decode("utf-8")) | |
| event_type = data.get("type") | |
| key_name = data.get("key") | |
| if key_name and len(key_name) == 1: | |
| key_name = key_name.lower() | |
| pg_key = KEY_MAP.get(key_name) | |
| if pg_key is not None: | |
| if event_type == "keydown": | |
| shared_keys[pg_key] = True | |
| try: | |
| pygame.event.post( | |
| pygame.event.Event( | |
| pygame.KEYDOWN, | |
| key=pg_key, | |
| unicode=data.get("unicode", ""), | |
| ) | |
| ) | |
| except Exception: | |
| pass | |
| elif event_type == "keyup": | |
| shared_keys[pg_key] = False | |
| try: | |
| pygame.event.post(pygame.event.Event(pygame.KEYUP, key=pg_key)) | |
| except Exception: | |
| pass | |
| elif msg_type == "MOUS": | |
| data = json.loads(payload.decode("utf-8")) | |
| shared_mouse_pos = (data.get("x", 0), data.get("y", 0)) | |
| btn_left = data.get("btnLeft", False) | |
| btn_middle = data.get("btnMiddle", False) | |
| btn_right = data.get("btnRight", False) | |
| shared_mouse_buttons = (btn_left, btn_middle, btn_right) | |
| try: | |
| event_type = data.get("type") | |
| if event_type == "mousemove": | |
| pygame.event.post( | |
| pygame.event.Event( | |
| pygame.MOUSEMOTION, | |
| pos=shared_mouse_pos, | |
| rel=(0, 0), | |
| buttons=shared_mouse_buttons, | |
| ) | |
| ) | |
| elif event_type == "mousedown": | |
| button_idx = data.get("button", 0) + 1 | |
| pygame.event.post( | |
| pygame.event.Event( | |
| pygame.MOUSEBUTTONDOWN, | |
| pos=shared_mouse_pos, | |
| button=button_idx, | |
| ) | |
| ) | |
| elif event_type == "mouseup": | |
| button_idx = data.get("button", 0) + 1 | |
| pygame.event.post( | |
| pygame.event.Event( | |
| pygame.MOUSEBUTTONUP, | |
| pos=shared_mouse_pos, | |
| button=button_idx, | |
| ) | |
| ) | |
| except Exception: | |
| pass | |
| elif msg_type == "CAMF": | |
| shared_webcam_frame = bytes(payload) | |
| elif msg_type == "CV2K": | |
| data = json.loads(payload.decode("utf-8")) | |
| key_code = data.get("key_code", -1) | |
| if key_code != -1: | |
| cv2_keys.put(key_code) | |
| elif msg_type == "INPT": | |
| text = payload.decode("utf-8", errors="ignore") | |
| stdin_queue.put(text) | |
| elif msg_type == "QUIT": | |
| try: | |
| pygame.event.post(pygame.event.Event(pygame.QUIT)) | |
| except Exception: | |
| pass | |
| except Exception as e: | |
| print(f"[Runner-IPC] Reader error: {e}", file=sys.__stderr__) | |
| finally: | |
| print("[Runner-IPC] Reader thread finished.", file=sys.__stderr__) | |
| try: | |
| sock.close() | |
| except Exception: | |
| pass | |
| def writer_thread_func(sock): | |
| print("[Runner-IPC] Writer thread started.") | |
| try: | |
| while True: | |
| msg_type, payload = send_queue.get() | |
| if msg_type == b"EXIT": | |
| try: | |
| sock.sendall(msg_type) | |
| sock.sendall((0).to_bytes(4, byteorder="big")) | |
| except Exception: | |
| pass | |
| break | |
| sock.sendall(msg_type) | |
| sock.sendall(len(payload).to_bytes(4, byteorder="big")) | |
| sock.sendall(payload) | |
| except Exception as e: | |
| print(f"[Runner-IPC] Writer error: {e}", file=sys.__stderr__) | |
| finally: | |
| print("[Runner-IPC] Writer thread finished.", file=sys.__stderr__) | |
| try: | |
| sock.close() | |
| except Exception: | |
| pass | |
| sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | |
| try: | |
| sock.connect(args.socket_path) | |
| except Exception as e: | |
| print(f"Error connecting to server IPC socket: {e}") | |
| sys.exit(1) | |
| reader_thread = threading.Thread(target=reader_thread_func, args=(sock,), daemon=True) | |
| writer_thread = threading.Thread(target=writer_thread_func, args=(sock,), daemon=True) | |
| reader_thread.start() | |
| writer_thread.start() | |
| # ----------------- Buffered stdout/stderr ----------------- | |
| class StreamToIPC: | |
| """Buffer log fragments and flush on newline or timer to reduce IPC chatter.""" | |
| def __init__(self, original_stream, flush_interval=0.05): | |
| self.original_stream = original_stream | |
| self.flush_interval = flush_interval | |
| self._buf = [] | |
| self._lock = threading.Lock() | |
| self._last_flush = time.time() | |
| self._stop = threading.Event() | |
| self._flusher = threading.Thread(target=self._flush_loop, daemon=True) | |
| self._flusher.start() | |
| def write(self, buf): | |
| if not buf: | |
| return 0 | |
| self.original_stream.write(buf) | |
| self.original_stream.flush() | |
| with self._lock: | |
| self._buf.append(buf) | |
| if "\n" in buf or sum(len(x) for x in self._buf) > 2048: | |
| self._flush_unlocked() | |
| return len(buf) | |
| def _flush_unlocked(self): | |
| if not self._buf: | |
| return | |
| text = "".join(self._buf) | |
| self._buf.clear() | |
| self._last_flush = time.time() | |
| if text: | |
| send_queue.put((b"LOGS", text.encode("utf-8"))) | |
| def _flush_loop(self): | |
| while not self._stop.wait(self.flush_interval): | |
| with self._lock: | |
| if self._buf and (time.time() - self._last_flush) >= self.flush_interval: | |
| self._flush_unlocked() | |
| def flush(self): | |
| self.original_stream.flush() | |
| with self._lock: | |
| self._flush_unlocked() | |
| sys.stdout = StreamToIPC(sys.stdout) | |
| sys.stderr = StreamToIPC(sys.stderr) | |
| # ----------------- Stdin / input() ----------------- | |
| import builtins | |
| class CustomStdin: | |
| def readline(self, limit=-1): | |
| return stdin_queue.get() | |
| def read(self, size=-1): | |
| return stdin_queue.get() | |
| def fileno(self): | |
| raise OSError("Not a real file descriptor") | |
| def isatty(self): | |
| return True | |
| sys.stdin = CustomStdin() | |
| original_input = builtins.input | |
| def custom_input(prompt=""): | |
| if prompt: | |
| sys.stdout.write(str(prompt)) | |
| sys.stdout.flush() | |
| line = stdin_queue.get() | |
| if line.endswith("\r\n"): | |
| return line[:-2] | |
| if line.endswith("\n") or line.endswith("\r"): | |
| return line[:-1] | |
| return line | |
| builtins.input = custom_input | |
| # ----------------- Pygame patches ----------------- | |
| original_set_mode = pygame.display.set_mode | |
| def custom_set_mode(size, flags=0, depth=0, display=0, vsync=0): | |
| surf = original_set_mode(size, flags, depth, display, vsync) | |
| print(f"[Pygame] 初始化視窗大小為: {size}") | |
| return surf | |
| pygame.display.set_mode = custom_set_mode | |
| def _encode_surface_jpeg(surf) -> bytes | None: | |
| """Encode pygame surface to JPEG via numpy + cv2 for controllable quality.""" | |
| try: | |
| import cv2 | |
| # pygame surface -> RGB array | |
| arr = pygame.surfarray.array3d(surf) | |
| # array3d is (w, h, 3); OpenCV wants (h, w, 3) BGR | |
| arr = np.transpose(arr, (1, 0, 2)) | |
| bgr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR) | |
| # Optional downscale if very large | |
| h, w = bgr.shape[:2] | |
| max_w = 960 | |
| if w > max_w: | |
| scale = max_w / float(w) | |
| bgr = cv2.resize(bgr, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) | |
| ok, jpeg = cv2.imencode(".jpg", bgr, [cv2.IMWRITE_JPEG_QUALITY, JPEG_QUALITY]) | |
| if ok: | |
| return jpeg.tobytes() | |
| except Exception: | |
| # Fallback to pygame save | |
| try: | |
| buffer = io.BytesIO() | |
| pygame.image.save(surf, buffer, "JPEG") | |
| return buffer.getvalue() | |
| except Exception: | |
| return None | |
| return None | |
| def send_pygame_frame(): | |
| global last_pygame_frame_time | |
| now = time.time() | |
| qsize = send_queue.qsize() | |
| _adjust_frame_interval(qsize) | |
| if now - last_pygame_frame_time < _current_frame_interval: | |
| return | |
| if qsize > 3: | |
| return | |
| last_pygame_frame_time = now | |
| surf = pygame.display.get_surface() | |
| if not surf: | |
| return | |
| jpeg_bytes = _encode_surface_jpeg(surf) | |
| if jpeg_bytes: | |
| send_queue.put((b"FRME", jpeg_bytes)) | |
| original_flip = pygame.display.flip | |
| original_update = pygame.display.update | |
| def custom_flip(): | |
| send_pygame_frame() | |
| original_flip() | |
| def custom_update(*args, **kwargs): | |
| send_pygame_frame() | |
| original_update(*args, **kwargs) | |
| pygame.display.flip = custom_flip | |
| pygame.display.update = custom_update | |
| def custom_key_get_pressed(): | |
| return shared_keys | |
| pygame.key.get_pressed = custom_key_get_pressed | |
| def custom_mouse_get_pos(): | |
| return shared_mouse_pos | |
| pygame.mouse.get_pos = custom_mouse_get_pos | |
| def custom_mouse_get_pressed(num_buttons=3): | |
| return shared_mouse_buttons | |
| pygame.mouse.get_pressed = custom_mouse_get_pressed | |
| def find_chinese_font_path(): | |
| paths = [ | |
| "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", | |
| "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", | |
| "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", | |
| "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", | |
| "/usr/share/fonts/truetype/droid/DroidSansFallback.ttf", | |
| "/System/Library/Fonts/Supplemental/Songti.ttc", | |
| "/System/Library/Fonts/STHeiti Light.ttc", | |
| "/System/Library/Fonts/STHeiti Medium.ttc", | |
| "/System/Library/Fonts/PingFang.ttc", | |
| "/Library/Fonts/Arial Unicode.ttf", | |
| "C:\\Windows\\Fonts\\msjh.ttc", | |
| "C:\\Windows\\Fonts\\msjhbd.ttc", | |
| "C:\\Windows\\Fonts\\msyh.ttc", | |
| "C:\\Windows\\Fonts\\simsun.ttc", | |
| ] | |
| for p in paths: | |
| if os.path.exists(p): | |
| return p | |
| candidates = [ | |
| "wenquanyizenhei", "wenquanyimicrohei", "wqy-zenhei", "wqy-microhei", | |
| "microsoftjhenghei", "pingfangtc", "songti", "stheitimedium", | |
| "arialunicodems", "droidsansfallback", | |
| ] | |
| for c in candidates: | |
| match = pygame.font.match_font(c) | |
| if match: | |
| return match | |
| return None | |
| original_SysFont = pygame.font.SysFont | |
| def custom_SysFont(name, size, bold=False, italic=False): | |
| name_lower = name.lower() if isinstance(name, str) else "" | |
| is_chinese_request = any( | |
| x in name_lower | |
| for x in ["jheng", "hei", "kai", "ming", "song", "cjk", "wqy", "unicode", "yahei", "wenquanyi"] | |
| ) or (name is None) | |
| if is_chinese_request: | |
| path = find_chinese_font_path() | |
| if path: | |
| try: | |
| return pygame.font.Font(path, size) | |
| except Exception: | |
| pass | |
| return original_SysFont(name, size, bold, italic) | |
| pygame.font.SysFont = custom_SysFont | |
| original_Font = pygame.font.Font | |
| def custom_Font(filename, size): | |
| if filename is None: | |
| path = find_chinese_font_path() | |
| if path: | |
| try: | |
| return original_Font(path, size) | |
| except Exception: | |
| pass | |
| return original_Font(filename, size) | |
| pygame.font.Font = custom_Font | |
| # ----------------- OpenCV patches ----------------- | |
| import cv2 # noqa: E402 | |
| def custom_imshow(winname, mat): | |
| global last_cv2_frame_time | |
| now = time.time() | |
| qsize = send_queue.qsize() | |
| _adjust_frame_interval(qsize) | |
| if now - last_cv2_frame_time < _current_frame_interval: | |
| return | |
| if qsize > 3: | |
| return | |
| last_cv2_frame_time = now | |
| if mat is None: | |
| return | |
| try: | |
| frame = mat | |
| h, w = frame.shape[:2] | |
| max_w = 960 | |
| if w > max_w: | |
| scale = max_w / float(w) | |
| frame = cv2.resize(frame, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) | |
| ret, jpeg = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, JPEG_QUALITY]) | |
| if ret: | |
| send_queue.put((b"FRME", jpeg.tobytes())) | |
| except Exception: | |
| pass | |
| cv2.imshow = custom_imshow | |
| def custom_wait_key(delay=0): | |
| start_time = time.time() | |
| timeout = (delay / 1000.0) if delay > 0 else float("inf") | |
| while True: | |
| if not cv2_keys.empty(): | |
| return cv2_keys.get() | |
| if time.time() - start_time >= timeout: | |
| break | |
| time.sleep(0.005) | |
| return -1 | |
| cv2.waitKey = custom_wait_key | |
| class MockVideoCapture: | |
| def __init__(self, source=0): | |
| self.source = source | |
| self.is_opened = True | |
| self.frame_counter = 0 | |
| def isOpened(self): | |
| return self.is_opened | |
| def read(self): | |
| global shared_webcam_frame | |
| if not self.is_opened: | |
| return False, None | |
| if shared_webcam_frame is not None: | |
| try: | |
| img_np = np.frombuffer(shared_webcam_frame, dtype=np.uint8) | |
| frame = cv2.imdecode(img_np, cv2.IMREAD_COLOR) | |
| if frame is not None: | |
| return True, frame | |
| except Exception as e: | |
| print(f"[OpenCV-Mock] 解碼 Webcam 畫面錯誤: {e}", file=sys.__stderr__) | |
| width, height = 640, 480 | |
| frame = np.zeros((height, width, 3), dtype=np.uint8) | |
| self.frame_counter += 1 | |
| t = time.time() | |
| cx = int(320 + 120 * np.sin(t * 3)) | |
| cy = int(240 + 80 * np.cos(t * 2)) | |
| cv2.circle(frame, (cx, cy), 40, (150, 80, 230), -1) | |
| cv2.putText( | |
| frame, | |
| "Webcam Connecting...", | |
| (160, 240), | |
| cv2.FONT_HERSHEY_SIMPLEX, | |
| 0.8, | |
| (240, 240, 240), | |
| 2, | |
| ) | |
| cv2.putText( | |
| frame, | |
| "Please enable webcam in browser", | |
| (130, 280), | |
| cv2.FONT_HERSHEY_SIMPLEX, | |
| 0.6, | |
| (200, 200, 200), | |
| 1, | |
| ) | |
| return True, frame | |
| def release(self): | |
| self.is_opened = False | |
| def get(self, propId): | |
| if propId == cv2.CAP_PROP_FRAME_WIDTH: | |
| return 640 | |
| if propId == cv2.CAP_PROP_FRAME_HEIGHT: | |
| return 480 | |
| if propId == cv2.CAP_PROP_FPS: | |
| return 30 | |
| return 0 | |
| def set(self, propId, value): | |
| return True | |
| original_VideoCapture = cv2.VideoCapture | |
| def custom_VideoCapture(source=0): | |
| if isinstance(source, int) or source == "0" or source == 0: | |
| print("[OpenCV] 攔截實體鏡頭 VideoCapture(0) 啟用瀏覽器 Webcam 串流。") | |
| return MockVideoCapture(source) | |
| print(f"[OpenCV] 載入影片檔案: {source}") | |
| return original_VideoCapture(source) | |
| cv2.VideoCapture = custom_VideoCapture | |
| # ----------------- Shared models + project setup ----------------- | |
| def ensure_shared_models(project_dir: str, models_dir: str | None) -> None: | |
| """Symlink common MediaPipe models from shared models/ if missing in project.""" | |
| if not models_dir or not os.path.isdir(models_dir): | |
| return | |
| known = ( | |
| "hand_landmarker.task", | |
| "face_landmarker.task", | |
| "pose_landmarker.task", | |
| "gesture_recognizer.task", | |
| ) | |
| for name in known: | |
| dest = os.path.join(project_dir, name) | |
| src = os.path.join(models_dir, name) | |
| if os.path.exists(dest) or not os.path.isfile(src): | |
| continue | |
| try: | |
| os.symlink(src, dest) | |
| print(f"[Runner] 連結共用模型: {name} -> {src}") | |
| except OSError: | |
| # Fallback: copy is too heavy; tell user models_dir path via env | |
| try: | |
| # Hard-link if same filesystem | |
| os.link(src, dest) | |
| print(f"[Runner] 硬連結共用模型: {name}") | |
| except OSError as e: | |
| print(f"[Runner] 無法連結模型 {name}: {e}(請將檔案放在專案目錄或 models/)") | |
| import runpy | |
| project_path = os.path.abspath(args.project_path) | |
| project_dir = os.path.dirname(project_path) | |
| print(f"[Runner] 切換工作目錄至: {project_dir}") | |
| os.chdir(project_dir) | |
| sys.path.insert(0, project_dir) | |
| models_dir = args.models_dir | |
| if not models_dir: | |
| # Default: <repo>/models next to runner.py | |
| models_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models") | |
| ensure_shared_models(project_dir, models_dir) | |
| # Keras dummy assets → temp session dir (do not pollute student project) | |
| try: | |
| script_content = open(project_path, "r", errors="ignore").read() | |
| if "keras_model.h5" in script_content: | |
| session_tmp = tempfile.mkdtemp(prefix="telearn_keras_") | |
| h5_path = os.path.join(project_dir, "keras_model.h5") | |
| labels_path = os.path.join(project_dir, "labels.txt") | |
| tmp_h5 = os.path.join(session_tmp, "keras_model.h5") | |
| tmp_labels = os.path.join(session_tmp, "labels.txt") | |
| need_h5 = not os.path.exists(h5_path) | |
| need_labels = not os.path.exists(labels_path) | |
| if need_h5 or need_labels: | |
| print(f"[Runner] Keras demo 資源寫入暫存目錄: {session_tmp}") | |
| if need_h5: | |
| print("[Runner] 偵測到 Keras 專案且缺乏 keras_model.h5,嘗試自動生成 Dummy 模型...") | |
| try: | |
| try: | |
| import keras | |
| except ImportError: | |
| from tensorflow import keras # type: ignore | |
| model = keras.models.Sequential( | |
| [ | |
| keras.layers.Input(shape=(224, 224, 3)), | |
| keras.layers.Flatten(), | |
| keras.layers.Dense(3, activation="softmax"), | |
| ] | |
| ) | |
| model.save(tmp_h5) | |
| try: | |
| os.symlink(tmp_h5, h5_path) | |
| except OSError: | |
| # Last resort: only if project dir is writable and no existing file | |
| if not os.path.exists(h5_path): | |
| import shutil | |
| shutil.copy2(tmp_h5, h5_path) | |
| print("[Runner] 成功生成 dummy keras_model.h5") | |
| except Exception as ex: | |
| print(f"[Runner] 無法自動生成模型(可能未安裝 tensorflow/keras):{ex}") | |
| if need_labels: | |
| print("[Runner] 偵測到缺乏 labels.txt,自動生成預設分類標籤...") | |
| try: | |
| with open(tmp_labels, "w", encoding="utf-8") as f: | |
| f.write("0 背景\n1 喝水中\n2 其他動作\n") | |
| try: | |
| os.symlink(tmp_labels, labels_path) | |
| except OSError: | |
| if not os.path.exists(labels_path): | |
| with open(labels_path, "w", encoding="utf-8") as f: | |
| f.write("0 背景\n1 喝水中\n2 其他動作\n") | |
| print("[Runner] 成功生成 labels.txt") | |
| except Exception as ex: | |
| print(f"[Runner] 無法自動生成 labels.txt:{ex}") | |
| except Exception as e: | |
| print(f"[Runner] 檢查/生成 Keras 依賴資源時出錯: {e}") | |
| try: | |
| print(f"[Runner] 開始執行學生專案: {os.path.basename(project_path)}") | |
| runpy.run_path(project_path, run_name="__main__") | |
| except SystemExit as e: | |
| print(f"[Runner] 專案正常結束 (SystemExit: {e})") | |
| except Exception: | |
| import traceback | |
| print("[Runner] 專案執行出錯:") | |
| traceback.print_exc(file=sys.stderr) | |
| finally: | |
| print("[Runner] 清理資源,結束執行。") | |
| try: | |
| sys.stdout.flush() | |
| sys.stderr.flush() | |
| except Exception: | |
| pass | |
| send_queue.put((b"EXIT", b"")) | |
| time.sleep(0.5) | |