x-tracker / main.py
Marnu25's picture
feat(restore): restore original high-speed Docker x-tracker with T4 CUDA GPU acceleration and zero NSFW tracking
18ef677 verified
Raw History Blame Contribute Delete
14.4 kB
import base64
import hashlib
import hmac
import json
import os
import shutil
import socket
import time
from datetime import datetime
from pathlib import Path
from typing import Optional
from pydantic import BaseModel
from fastapi import FastAPI, HTTPException, Request, Response, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, StreamingResponse
from fastapi.staticfiles import StaticFiles
from camera_manager import CameraManager
from filters import AVAILABLE_FILTERS
from recorder import VideoRecorder
BASE_DIR = Path(__file__).resolve().parent
STORAGE_DIR = BASE_DIR / "storage"
SNAPSHOT_DIR = STORAGE_DIR / "snapshots"
RECORDING_DIR = STORAGE_DIR / "recordings"
CONFIG_PATH = STORAGE_DIR / "cameras.json"
STATIC_DIR = BASE_DIR / "static"
TEMPLATES_DIR = BASE_DIR / "templates"
SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
RECORDING_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI(title="X-Tracker Studio - GPU AI Vision & Multi-Camera Tracking")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
camera = CameraManager(config_path=CONFIG_PATH)
recorder = VideoRecorder(camera=camera, storage_dir=RECORDING_DIR)
class AddCameraRequest(BaseModel):
name: str
source: str
type: str = "network"
def get_network_ips():
lan_ip = "127.0.0.1"
tailscale_ip = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
lan_ip = s.getsockname()[0]
s.close()
except Exception:
pass
try:
ts_out = os.popen("tailscale ip -4 2>/dev/null").read().strip()
if ts_out:
tailscale_ip = ts_out
except Exception:
pass
return lan_ip, tailscale_ip
@app.on_event("startup")
def startup_event():
camera.start()
@app.on_event("shutdown")
def shutdown_event():
if recorder.is_recording:
recorder.stop()
camera.stop()
@app.get("/")
def get_dashboard():
return FileResponse(str(TEMPLATES_DIR / "index.html"))
@app.get("/api/status")
def get_system_status():
cam_status = camera.get_status()
rec_status = recorder.get_status()
lan_ip, ts_ip = get_network_ips()
port = int(os.environ.get("PORT", 7860))
disk_total, disk_used, disk_free = shutil.disk_usage(str(STORAGE_DIR))
return {
"status": "online",
"camera": cam_status,
"recorder": rec_status,
"network": {
"lan_ip": lan_ip,
"tailscale_ip": ts_ip,
"port": port,
"lan_url": f"http://{lan_ip}:{port}",
"tailscale_url": f"http://{ts_ip}:{port}" if ts_ip else None,
},
"storage": {
"free_gb": round(disk_free / (1024**3), 2),
"total_gb": round(disk_total / (1024**3), 2),
}
}
# --- Filter Endpoints ---
@app.get("/api/filters")
def get_filters():
return {
"available_filters": AVAILABLE_FILTERS,
"active_filter": camera.current_filter
}
@app.post("/api/filter/{filter_name}")
def set_active_filter(filter_name: str):
ok, msg = camera.set_filter(filter_name)
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {
"status": "success",
"message": msg,
"active_filter": camera.current_filter
}
# --- Zoom & AI Detection Endpoints ---
class FocusPointRequest(BaseModel):
x: float
y: float
zoom: Optional[float] = None
@app.post("/api/zoom/reset")
def reset_zoom():
camera.set_zoom(1.0, 0.5, 0.5)
camera.set_auto_tracking_mode("off")
return {"status": "success", "message": "Zoom and focus reset to center 1.0x"}
@app.post("/api/zoom/focus")
def set_zoom_focus_point(payload: FocusPointRequest):
ok, msg = camera.set_zoom_focus_point(payload.x, payload.y, payload.zoom)
return {
"status": "success",
"message": msg,
"zoom_center_x": camera.zoom_center_x,
"zoom_center_y": camera.zoom_center_y,
"zoom_level": camera.zoom_level
}
@app.post("/api/zoom/target/{object_id}")
def set_zoom_target_object(object_id: str):
target = None if object_id in ["none", "clear", "null", "reset"] else object_id
ok, msg = camera.set_focus_target_object(target)
return {
"status": "success",
"message": msg,
"target_object_id": camera.target_object_id,
"auto_tracking_mode": camera.auto_tracking_mode
}
@app.post("/api/zoom/track/{mode}")
def set_auto_tracking_mode(mode: str):
ok, msg = camera.set_auto_tracking_mode(mode)
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {
"status": "success",
"message": msg,
"auto_tracking_mode": camera.auto_tracking_mode
}
@app.post("/api/zoom/{level}")
def set_zoom(level: float):
ok, msg, new_zoom = camera.set_zoom(level)
return {"status": "success", "message": msg, "zoom_level": new_zoom}
@app.post("/api/detection/toggle")
def toggle_ai_detection():
new_state = not camera.ai_detection_enabled
ok, msg, enabled = camera.set_detection(new_state)
return {"status": "success", "message": msg, "ai_detection_enabled": enabled}
@app.post("/api/detection/{state}")
def set_ai_detection(state: str):
enabled = (state.lower() in ["1", "true", "on", "enable"])
ok, msg, enabled = camera.set_detection(enabled)
return {"status": "success", "message": msg, "ai_detection_enabled": enabled}
@app.get("/api/detections")
def get_detections():
return {
"ai_detection_enabled": camera.ai_detection_enabled,
"detections": camera.current_detections,
"telemetry": camera.current_telemetry,
"count": len(camera.current_detections)
}
@app.post("/api/overlay/{state}")
def set_hud_overlay(state: str):
enabled = (state.lower() in ["1", "true", "on", "enable"])
ok, msg, enabled = camera.set_hud_overlay(enabled)
return {"status": "success", "message": msg, "show_hud_overlay": enabled}
# --- Multi-Camera Endpoints ---
@app.get("/api/cameras")
def get_registered_cameras():
return {
"cameras": camera.get_cameras(),
"active_camera_id": camera.active_camera_id
}
@app.post("/api/cameras/active/{cam_id}")
def switch_active_camera(cam_id: str):
if recorder.is_recording:
recorder.stop()
ok, msg = camera.switch_camera(cam_id)
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {"status": "success", "message": msg, "active_camera": camera.get_active_camera()}
@app.post("/api/cameras")
def register_new_camera(req: AddCameraRequest):
ok, msg, new_cam = camera.add_camera(name=req.name, source=req.source, cam_type=req.type)
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {"status": "success", "message": msg, "camera": new_cam}
@app.delete("/api/cameras/{cam_id}")
def delete_camera(cam_id: str):
ok, msg = camera.remove_camera(cam_id)
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {"status": "success", "message": msg}
# --- Remote Power Control ---
@app.post("/api/camera/start")
def remote_start_camera():
ok, msg = camera.start()
if not ok:
raise HTTPException(status_code=500, detail=msg)
return {"status": "success", "message": msg, "camera": camera.get_status()}
@app.post("/api/camera/stop")
def remote_stop_camera():
if recorder.is_recording:
recorder.stop()
ok, msg = camera.stop()
return {"status": "success", "message": msg, "camera": camera.get_status()}
# --- Streaming, Snapshot, Recording ---
@app.get("/api/stream")
def get_live_stream():
if not camera.running:
camera.start()
return StreamingResponse(
camera.generate_mjpeg(),
media_type="multipart/x-mixed-replace; boundary=frame"
)
@app.post("/api/snapshot")
def remote_snapshot():
if not camera.running:
ok, msg = camera.start()
if not ok:
raise HTTPException(status_code=503, detail=f"Cannot snapshot: {msg}")
jpeg = None
for _ in range(10):
jpeg = camera.get_clean_jpeg()
if jpeg:
break
time.sleep(0.05)
if not jpeg:
raise HTTPException(status_code=503, detail="No video frame available to capture")
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
filt = camera.current_filter
filename = f"snap_{filt}_{ts}.jpg"
filepath = SNAPSHOT_DIR / filename
filepath.write_bytes(jpeg)
return {
"status": "success",
"filename": filename,
"url": f"/media/snapshots/{filename}",
"size_bytes": len(jpeg),
"timestamp": ts,
"filter": filt,
"width": camera.actual_width,
"height": camera.actual_height,
}
@app.post("/api/record/start")
def remote_record_start():
ok, msg, filename = recorder.start()
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {"status": "success", "message": msg, "filename": filename, "recorder": recorder.get_status()}
@app.post("/api/record/stop")
def remote_record_stop():
ok, msg, info = recorder.stop()
if not ok:
raise HTTPException(status_code=400, detail=msg)
return {"status": "success", "message": msg, "info": info}
MEDIA_SECRET = os.getenv("SECRET_KEY", "xtracker-media-secure-key-2026")
def generate_signed_media_url(media_type: str, filename: str, expires_in: int = 3600) -> str:
exp = int(time.time()) + expires_in
msg = f"{media_type}:{filename}:{exp}".encode()
sig = hmac.new(MEDIA_SECRET.encode(), msg, hashlib.sha256).hexdigest()[:16]
return f"/media/{media_type}/{filename}?exp={exp}&sig={sig}"
# --- Media Endpoints ---
@app.get("/api/media")
def list_media():
snapshots = []
for f in sorted(SNAPSHOT_DIR.glob("*.jpg"), key=os.path.getmtime, reverse=True):
st = f.stat()
snapshots.append({
"name": f.name,
"url": generate_signed_media_url("snapshots", f.name, expires_in=3600),
"size_bytes": st.st_size,
"size_formatted": f"{round(st.st_size / 1024, 1)} KB",
"modified": datetime.fromtimestamp(st.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"type": "image",
})
recordings = []
for f in sorted(RECORDING_DIR.glob("*.mp4"), key=os.path.getmtime, reverse=True):
st = f.stat()
recordings.append({
"name": f.name,
"url": generate_signed_media_url("recordings", f.name, expires_in=3600),
"size_bytes": st.st_size,
"size_formatted": f"{round(st.st_size / (1024*1024), 2)} MB" if st.st_size >= 1048576 else f"{round(st.st_size / 1024, 1)} KB",
"modified": datetime.fromtimestamp(st.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"type": "video",
})
return {
"snapshots": snapshots,
"recordings": recordings,
"total_count": len(snapshots) + len(recordings)
}
@app.api_route("/media/snapshots/{filename}", methods=["GET", "HEAD"])
def serve_snapshot_image(filename: str):
f = SNAPSHOT_DIR / Path(filename).name
if not f.exists():
raise HTTPException(status_code=404, detail="Snapshot not found")
return FileResponse(str(f), media_type="image/jpeg")
@app.api_route("/media/recordings/{filename}", methods=["GET", "HEAD"])
def serve_recording_video(filename: str, request: Request):
file_path = RECORDING_DIR / Path(filename).name
if not file_path.exists():
raise HTTPException(status_code=404, detail="Recording not found")
file_size = file_path.stat().st_size
range_header = request.headers.get("Range")
if not range_header:
return FileResponse(str(file_path), media_type="video/mp4")
try:
h = range_header.replace("bytes=", "").split("-")
start = int(h[0]) if h[0] else 0
end = int(h[1]) if len(h) > 1 and h[1] else file_size - 1
except ValueError:
return FileResponse(str(file_path), media_type="video/mp4")
if start >= file_size or end >= file_size or start > end:
return Response(status_code=416, headers={"Content-Range": f"bytes */{file_size}"})
chunk_size = (end - start) + 1
with open(file_path, "rb") as f:
f.seek(start)
data = f.read(chunk_size)
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(chunk_size),
"Content-Type": "video/mp4",
}
return Response(content=data, status_code=206, headers=headers)
@app.post("/api/infer")
async def infer_frame(request: Request):
"""Remote GPU inference endpoint for Hugging Face Spaces offloading."""
import cv2
import numpy as np
body = await request.body()
if not body:
raise HTTPException(status_code=400, detail="Empty image payload")
arr = np.frombuffer(body, dtype=np.uint8)
frame = cv2.imdecode(arr, cv2.IMREAD_COLOR)
if frame is None:
raise HTTPException(status_code=400, detail="Invalid JPEG buffer")
dets, telem = camera.detector.detect(frame)
return {
"status": "success",
"detections": dets,
"telemetry": telem,
"keypoints": camera.detector.latest_keypoints,
"gpu_active": bool(telem.get("gpu_active", False))
}
@app.post("/api/frame/push")
async def push_camera_frame(request: Request):
"""Ingests live JPEG frames from Browser Webcam (getUserMedia) or local station relay."""
body = await request.body()
ok = camera.push_external_frame(body)
return {"status": "success" if ok else "ignored"}
@app.websocket("/ws/frame")
async def websocket_frame_ingest(websocket: WebSocket):
"""Ultra-low-latency binary WebSocket ingest for live Browser Webcam frames."""
await websocket.accept()
try:
while True:
data = await websocket.receive_bytes()
camera.push_external_frame(data)
except WebSocketDisconnect:
pass
except Exception:
pass
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 7860))
uvicorn.run(app, host="0.0.0.0", port=port)