File size: 7,930 Bytes
b84c410 b5c3309 b84c410 b5c3309 57f6065 b5c3309 b84c410 b5c3309 b84c410 0205fd4 b84c410 57f6065 b5c3309 57f6065 b5c3309 b84c410 b5c3309 57f6065 b5c3309 b84c410 b5c3309 b84c410 b5c3309 b84c410 b5c3309 b84c410 b5c3309 57f6065 b5c3309 b84c410 b5c3309 b84c410 b5c3309 57f6065 b5c3309 0205fd4 b5c3309 57f6065 b5c3309 0205fd4 b5c3309 b84c410 b5c3309 b84c410 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | """CMS API + browsing UI for the compression miner.
Reads the SQLite DB (populated by ingest.py), the calibration log, the CQ
tables' source, and the two sample libraries. Runs as its own process,
independent of the compression service and the miner -- see run_server.sh.
"""
from __future__ import annotations
import re
from pathlib import Path
import calibration
import cq_health
import db
import library
import preview
from auth import require_auth
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
REFERENCE_LIBRARY_PATH = Path("/root/vidaio-real-content-library")
COMPRESSED_LIBRARY_PATH = Path("/root/vidaio-compressed-sample-library")
_RANGE_RE = re.compile(r"bytes=(\d*)-(\d*)")
app = FastAPI(title="Vidaio Miner CMS")
Auth = Depends(require_auth)
def _find_sample(library_path: Path, task_id: str) -> Path | None:
matches = list(library_path.glob(f"{task_id}_*.mp4"))
return matches[0] if matches else None
_STREAM_CHUNK = 1024 * 1024
def _stream_video(path: Path, request: Request) -> StreamingResponse:
"""Plain FileResponse ignores the Range header and always returns 200
with the entire file -- for a 200MB+ 4K reference clip, that means the
browser has to download the whole thing before it can play anything,
which looks exactly like the "spinning, never plays" symptom this was
built to fix. Real 206 Partial Content support so the <video> element
can seek and start playing immediately.
"""
file_size = path.stat().st_size
range_header = request.headers.get("range")
start, end = 0, file_size - 1
status_code = 200
if range_header:
match = _RANGE_RE.match(range_header)
if match:
if match.group(1):
start = int(match.group(1))
if match.group(2):
end = int(match.group(2))
end = min(end, file_size - 1)
status_code = 206
def iterfile():
with open(path, "rb") as f:
f.seek(start)
remaining = end - start + 1
while remaining > 0:
chunk = f.read(min(_STREAM_CHUNK, remaining))
if not chunk:
break
remaining -= len(chunk)
yield chunk
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Accept-Ranges": "bytes",
"Content-Length": str(end - start + 1),
}
return StreamingResponse(iterfile(), status_code=status_code, media_type="video/mp4", headers=headers)
@app.on_event("startup")
def _startup() -> None:
db.init_db()
from auth import get_credentials
user, pw = get_credentials()
print(f"CMS auth -- username: {user} password: {pw}", flush=True)
print(f"(persisted at {__import__('auth').CREDENTIALS_FILE})", flush=True)
# --- Items ------------------------------------------------------------
@app.get("/api/items")
def api_list_items(codec: str | None = None, vmaf_threshold: float | None = None,
limit: int = 100, offset: int = 0, sort: str = "ts", order: str = "desc",
user: str = Auth):
return db.list_items(codec=codec, vmaf_threshold=vmaf_threshold, limit=limit,
offset=offset, sort=sort, order=order)
@app.get("/api/items/{task_id}")
def api_get_item(task_id: str, user: str = Auth):
item = db.get_item(task_id)
if item is None:
raise HTTPException(status_code=404, detail="item not found")
# The DB's has_*_sample flags are set once at ingest time, but the
# compressed-sample capture is a fire-and-forget background task in a
# separate process (neurons/miner.py) with no ordering guarantee
# against the outcome-log write this row was ingested from -- ingest
# can easily run before the file finishes copying, permanently
# freezing the flag at False even though the file shows up moments
# later. Check the filesystem live instead of trusting that snapshot.
item["has_reference_sample"] = int(_find_sample(REFERENCE_LIBRARY_PATH, task_id) is not None)
item["has_compressed_sample"] = int(_find_sample(COMPRESSED_LIBRARY_PATH, task_id) is not None)
return item
@app.get("/api/stats")
def api_stats(user: str = Auth):
return db.stats_summary()
@app.get("/api/score-trend")
def api_score_trend(days: int = 14, user: str = Auth):
return db.score_trend(days=days)
@app.get("/api/rate-distortion")
def api_rate_distortion(limit: int = 500, user: str = Auth):
return db.rate_distortion_points(limit=limit)
@app.get("/video/{task_id}/reference")
def video_reference(task_id: str, request: Request, user: str = Auth):
path = _find_sample(REFERENCE_LIBRARY_PATH, task_id)
if path is None:
raise HTTPException(status_code=404, detail="reference sample not retained for this item")
playable = preview.get_preview(path, f"{task_id}_reference")
if playable is None:
raise HTTPException(status_code=500, detail="failed to prepare a browser-playable preview")
return _stream_video(playable, request)
@app.get("/video/{task_id}/compressed")
def video_compressed(task_id: str, request: Request, user: str = Auth):
path = _find_sample(COMPRESSED_LIBRARY_PATH, task_id)
if path is None:
raise HTTPException(status_code=404, detail="compressed sample not retained for this item")
playable = preview.get_preview(path, f"{task_id}_compressed")
if playable is None:
raise HTTPException(status_code=500, detail="failed to prepare a browser-playable preview")
return _stream_video(playable, request)
# --- CQ table health ----------------------------------------------------
@app.get("/api/cq-health")
def api_cq_health(user: str = Auth):
return cq_health.cq_table_health()
# --- Calibration ----------------------------------------------------------
@app.get("/api/calibration/log")
def api_calibration_log(codec: str | None = None, vmaf_threshold: float | None = None,
limit: int = 200, user: str = Auth):
return calibration.read_calibration_log(codec=codec, vmaf_threshold=vmaf_threshold, limit=limit)
@app.post("/api/calibration/start")
def api_calibration_start(codec: str, vmaf_threshold: float, cq_grid: str = "30,33,35,38,40",
limit_clips: int = 6, user: str = Auth):
run_id = calibration.start_calibration(codec, vmaf_threshold, cq_grid, limit_clips)
return {"run_id": run_id}
@app.get("/api/calibration/runs")
def api_calibration_runs(user: str = Auth):
return db.list_calibration_runs()
@app.get("/api/calibration/runs/{run_id}")
def api_calibration_run(run_id: int, user: str = Auth):
run = calibration.poll_calibration_run(run_id)
if run is None:
raise HTTPException(status_code=404, detail="run not found")
return run
# --- Content library --------------------------------------------------
@app.get("/api/library/coverage")
def api_library_coverage(user: str = Auth):
return library.coverage_grid()
@app.get("/api/library/clips")
def api_library_clips(user: str = Auth):
return library.list_reference_clips()
@app.get("/thumbnail/{task_id}")
def get_thumbnail(task_id: str, user: str = Auth):
path = library.get_thumbnail(task_id)
if path is None:
raise HTTPException(status_code=404, detail="no thumbnail available")
return FileResponse(path, media_type="image/jpeg")
# --- Alerts -----------------------------------------------------------
@app.get("/api/alerts")
def api_alerts(limit: int = 50, user: str = Auth):
return db.recent_alerts(limit=limit)
# --- UI -----------------------------------------------------------------
_PAGE_PATH = Path(__file__).parent / "page.html"
@app.get("/", response_class=HTMLResponse)
def index(user: str = Auth):
return _PAGE_PATH.read_text()
|