#!/usr/bin/env python3 """VCN-LUPINE daemon — runs on VPS, bridges IPC to GPU node over MKV/H.264. Listens on: - unix:/run/vcn-lupine/daemon.sock (IPC from libcuda shim + braid encoders) - tcp:14834 (MKV stream to GPU node) Tag routing: TAG_LUPINE → LUPINE server on GPU node (HTTP/2 → NVIDIA GPU) TAG_STRAND/CROSSING/PIST → VCN compute path on GPU node (AMD VCN) Schema: vcn_lupine_daemon_v1 """ import argparse import asyncio import json import logging import os import struct import socket import sys import threading from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from vcn_lupine_bridge import ( TAG_STRAND, TAG_CROSSING, TAG_PIST, TAG_LUPINE, FLAG_REPLY, FRAME_HDR_SIZE, pack_frame, unpack_frame, pack_reply, unpack_reply, tag_name, bridge_receipt, ) from vcn_lupine_opcodes import OPCODE_NAMES, LUPINE_OPCODES SOCKET_PATH = Path("/run/vcn-lupine/daemon.sock") GPU_NODE_HOST = os.environ.get("LUPINE_GPU_NODE", "100.88.57.96") GPU_NODE_PORT = int(os.environ.get("LUPINE_GPU_PORT", "14834")) DAEMON_PORT = 14834 logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) log = logging.getLogger("vcn-lupine-daemon") # ── MKV/H.264 encode/decode helpers ─────────────────────────────────────────── def encode_frame_to_mkv(frame_bytes: bytes, seq: int, output_path: str = "/dev/stdout") -> bytes: """Encode raw frame bytes as H.264/MKV using FFmpeg. Uses libx264 (software) when no VCN hardware is available. Output is written to a named pipe or returned as bytes. """ import subprocess, tempfile, os w, h = 1920, 1080 pipe_dir = tempfile.mkdtemp(prefix="vcn_lupine_") in_fifo = os.path.join(pipe_dir, "in.fifo") out_mkv = os.path.join(pipe_dir, "out.mkv") os.mkfifo(in_fifo) payload_len_bytes = struct.pack(" list[tuple[int, bytes]]: """Decode H.264/MKV to raw frames. Returns list of (seq, frame_bytes). Uses FFmpeg to demux + decode. """ import subprocess, tempfile, os if not mkv_data: return [] tmp_in = tempfile.NamedTemporaryFile(suffix=".mkv", delete=False) tmp_in.write(mkv_data) tmp_in.close() tmp_out_dir = tempfile.mkdtemp(prefix="vcn_lupine_decode_") tmp_out = os.path.join(tmp_out_dir, "frame.raw") cmd = [ "ffmpeg", "-y", "-i", tmp_in.name, "-f", "rawvideo", "-vcodec", "rawvideo", "-pix_fmt", "yuv420p", tmp_out, ] try: subprocess.run(cmd, capture_output=True, timeout=10, check=True) except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: log.error("FFmpeg decode failed: %s", str(e)) return [] finally: os.unlink(tmp_in.name) os.rmdir(tmp_out_dir) frames = [] frame_size = 1920 * 1080 * 3 // 2 # yuv420p try: with open(tmp_out, "rb") as f: data = f.read() offset = 0 seq = 0 while offset + 12 <= len(data): seq_read = struct.unpack(" dict: """Send a LUPINE CUDA call to the remote GPU node over HTTP/2. The GPU node runs the LUPINE server at gpu_node:GPU_NODE_PORT. This is a simple HTTP/1.1 POST for portability; the GPU node should accept both HTTP/1.1 and HTTP/2. """ import urllib.request, urllib.error, json as _json payload = _json.dumps({"opcode": opcode, "args": args}).encode("utf-8") req = urllib.request.Request( f"http://{GPU_NODE_HOST}:14833/cuda", data=payload, headers={"Content-Type": "application/json", "X-LUPINE-Request": "1"}, method="POST", ) try: with urllib.request.urlopen(req, timeout=timeout) as resp: return _json.loads(resp.read()) except urllib.error.URLError as e: raise RuntimeError(f"LUPINE call failed: {e}") # ── IPC protocol ────────────────────────────────────────────────────────────── IPC_FRAME = " tuple[int, int, bytes]: """Read a frame from the IPC Unix socket.""" header = b"" while len(header) < 8: chunk = sock.recv(8 - len(header)) if not chunk: raise EOFError("IPC socket closed") header += chunk tag, seq = struct.unpack(IPC_FRAME, header) len_buf = b"" while len(len_buf) < 4: chunk = sock.recv(4 - len(len_buf)) if not chunk: raise EOFError("IPC socket closed") len_buf += chunk payload_len = struct.unpack(" bytes: """Send a frame to GPU node and wait for reply.""" with self._lock: self._seq += 1 seq = self._seq event = threading.Event() self._pending[seq] = event try: frame = pack_frame(tag, seq, payload) mkv_data = encode_frame_to_mkv(frame, seq) with self._lock: self._sock.sendall(struct.pack("