565 lines
23 KiB
HTML
565 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MPR — Detection Pipeline Architecture</title>
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
background: #0a0e17;
|
|
color: #e8eaf0;
|
|
font-family: 'Inter', sans-serif;
|
|
line-height: 1.6;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
header {
|
|
padding: 16px 24px;
|
|
border-bottom: 1px solid #1e2a4a;
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
header h1 {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 22px;
|
|
font-weight: 600;
|
|
letter-spacing: 3px;
|
|
color: #0066ff;
|
|
}
|
|
|
|
header .subtitle {
|
|
font-size: 13px;
|
|
color: #4a5568;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.layout {
|
|
display: flex;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
nav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0;
|
|
width: 200px;
|
|
flex-shrink: 0;
|
|
background: #121829;
|
|
border-right: 1px solid #1e2a4a;
|
|
padding: 8px 0;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
nav a {
|
|
padding: 10px 20px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
color: #8892a8;
|
|
text-decoration: none;
|
|
border-left: 2px solid transparent;
|
|
transition: all 0.15s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
nav a:hover { color: #e8eaf0; background: #1a2340; }
|
|
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
|
|
|
|
main {
|
|
flex: 1;
|
|
overflow: auto;
|
|
padding: 32px 48px;
|
|
}
|
|
|
|
.graph-section {
|
|
display: none;
|
|
animation: fadeIn 0.2s ease;
|
|
}
|
|
|
|
.graph-section:target,
|
|
.graph-section.active { display: block; }
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.graph-section h2 {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 15px;
|
|
font-weight: 500;
|
|
color: #8892a8;
|
|
margin-bottom: 8px;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.graph-section > p {
|
|
font-size: 13px;
|
|
color: #4a5568;
|
|
margin-bottom: 24px;
|
|
max-width: 800px;
|
|
}
|
|
|
|
.graph-container {
|
|
background: #0a0e17;
|
|
border: 1px solid #1e2a4a;
|
|
padding: 24px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.graph-container a { display: block; }
|
|
.graph-container img { max-width: 100%; height: auto; }
|
|
|
|
.legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 24px;
|
|
margin-top: 16px;
|
|
font-size: 11px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
color: #4a5568;
|
|
}
|
|
|
|
.legend span::before {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 8px;
|
|
height: 8px;
|
|
margin-right: 6px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.legend .browser::before { background: #e8eaf0; }
|
|
.legend .cluster::before { background: #0066ff; }
|
|
.legend .data::before { background: #b4bccf; }
|
|
.legend .gpu::before { background: #00c853; }
|
|
.legend .cloud::before { background: #ffc107; }
|
|
|
|
/* Prose */
|
|
.graph-section h3 {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #e8eaf0;
|
|
letter-spacing: 1px;
|
|
margin: 32px 0 10px;
|
|
text-transform: uppercase;
|
|
}
|
|
.prose { max-width: 820px; }
|
|
.prose p {
|
|
font-size: 14px;
|
|
color: #b4bccf;
|
|
margin-bottom: 14px;
|
|
line-height: 1.7;
|
|
}
|
|
.prose p b { color: #e8eaf0; font-weight: 600; }
|
|
.prose code {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
color: #7ab0ff;
|
|
background: #121829;
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
}
|
|
.prose a { color: #0066ff; text-decoration: none; cursor: pointer; }
|
|
.prose a:hover { text-decoration: underline; }
|
|
.prose ul {
|
|
margin: 8px 0 16px 20px;
|
|
font-size: 14px;
|
|
color: #b4bccf;
|
|
line-height: 1.7;
|
|
}
|
|
.prose ul li { margin-bottom: 8px; }
|
|
|
|
pre.codeblock {
|
|
background: #121829;
|
|
border: 1px solid #1e2a4a;
|
|
color: #b4bccf;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 12px;
|
|
padding: 14px 18px;
|
|
margin: 8px 0 20px;
|
|
overflow-x: auto;
|
|
line-height: 1.55;
|
|
}
|
|
pre.codeblock .c { color: #4a5568; }
|
|
pre.codeblock .k { color: #0066ff; }
|
|
|
|
/* Mobile menu toggle */
|
|
.menu-toggle {
|
|
display: none;
|
|
background: transparent;
|
|
border: 1px solid #1e2a4a;
|
|
color: #e8eaf0;
|
|
padding: 6px 10px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
margin-left: auto;
|
|
}
|
|
.menu-toggle:hover { background: #1a2340; }
|
|
|
|
.nav-backdrop {
|
|
display: none;
|
|
position: absolute;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 10;
|
|
}
|
|
.layout.nav-open .nav-backdrop { display: block; }
|
|
|
|
@media (max-width: 720px) {
|
|
header { padding: 10px 12px; gap: 8px; }
|
|
header h1 { font-size: 16px; letter-spacing: 1px; }
|
|
header .subtitle { display: none; }
|
|
.menu-toggle { display: inline-block; }
|
|
|
|
.layout { position: relative; }
|
|
nav {
|
|
position: absolute;
|
|
left: 0; top: 0; bottom: 0;
|
|
width: 220px;
|
|
z-index: 20;
|
|
transform: translateX(-100%);
|
|
transition: transform 0.2s ease;
|
|
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.5);
|
|
}
|
|
.layout.nav-open nav { transform: translateX(0); }
|
|
|
|
main { padding: 16px; }
|
|
.graph-section h2 { font-size: 13px; }
|
|
.prose p, .prose ul { font-size: 13px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>MPR</h1>
|
|
<span class="subtitle">Media Processing & Detection Pipeline — Architecture</span>
|
|
<button class="menu-toggle" aria-label="Toggle navigation">☰</button>
|
|
</header>
|
|
|
|
<div class="layout">
|
|
<div class="nav-backdrop"></div>
|
|
|
|
<nav>
|
|
<a class="active" href="#overview">Overview</a>
|
|
<a href="#system">System</a>
|
|
<a href="#pipeline">Pipeline</a>
|
|
<a href="#profiles">Profiles</a>
|
|
<a href="#topology">Inference</a>
|
|
<a href="#data">Data Model</a>
|
|
<a href="#api">API</a>
|
|
<a href="#storage">Storage</a>
|
|
<a href="#modelgen">Codegen</a>
|
|
<a href="#dev">Dev Env</a>
|
|
<a href="#reference">Reference</a>
|
|
</nav>
|
|
|
|
<main>
|
|
|
|
<section id="overview" class="graph-section active">
|
|
<h2>OVERVIEW</h2>
|
|
<p>A guided tour of the platform — start here for narrative context before the diagrams.</p>
|
|
<div class="prose">
|
|
|
|
<h3>What MPR is</h3>
|
|
<p>MPR is a brand / logo / text detection pipeline for video. A user picks chunks of source material into a <b>Timeline</b>, then runs a <b>Profile</b> (pipeline topology + per-stage config) against it. The pipeline extracts frames, filters scenes, runs CV (field segmentation, edge detection) and detection (YOLO, OCR), resolves text to a session brand list, and escalates anything still unresolved to a local VLM and then to cloud VLM providers. Output is a brand timeline and per-brand stats.</p>
|
|
|
|
<h3>Where things run</h3>
|
|
<p>The architecture spans four boxes: the <b>browser</b> (Vue 3 detection-app + OpenCV WASM worker for fast CV iteration), the <b>K8s cluster</b> (Envoy Gateway, FastAPI, detection-ui, Postgres, Redis, MinIO — Kind in dev via Tilt), a separate <b>GPU host</b> on the LAN running the inference server (YOLO, OCR, local VLM), and <b>cloud VLM providers</b> (Anthropic, Gemini, OpenAI, Groq) for last-resort escalation. See <a href="#system">System</a>.</p>
|
|
|
|
<h3>Replay loop</h3>
|
|
<p>The system is built around iteration. <b>Checkpoint</b> rows form a tree of "what configs did we try at this stage" (no blobs); <b>StageOutput</b> is a flat upsert table holding each stage's output dict. A single stage can be re-run in place using upstream <code>StageOutput</code> rows, so the UI loop is "tweak config → replay one stage → look at the overlay" without rerunning the whole pipeline. Frame caches keyed by <code>timeline_id</code> are reused across replays.</p>
|
|
|
|
<h3>Profiles, not overrides</h3>
|
|
<p>Profiles live in Postgres as two JSONB blobs — <code>pipeline</code> (stages + edges + routing) and <code>configs</code> (per-stage parameters). The convention is to <i>duplicate a profile and tweak it</i>, not to layer overrides at the call site. Job-level <code>config_overrides</code> exist but are merged on top of the resolved profile in <code>core/detect/graph/nodes.py</code>.</p>
|
|
|
|
<h3>Inference indirection</h3>
|
|
<p>Every CV/ML stage takes an <code>INFERENCE_URL</code> argument. Empty (the dev default) runs CV in-process; set, the stage POSTs to <code>core/gpu/server.py</code> on the GPU host. Heavy ML deps (<code>torch</code>, <code>transformers</code>, <code>paddleocr</code>) live only in <code>core/gpu/pyproject.toml</code> — the API host doesn't need them.</p>
|
|
|
|
<h3>API and SSE</h3>
|
|
<p>FastAPI under <code>/detect/*</code> (<code>core/api/detect/</code>): sources, run/stop/pause/resume/step, status, replay, checkpoints, overlays, config. Pipeline events fan out through Redis to <code>GET /detect/stream/{job_id}</code> as SSE. Envoy keeps the SSE connection open for up to 3600s.</p>
|
|
|
|
<h3>Codegen</h3>
|
|
<p>Source-of-truth dataclasses live in <code>core/schema/models/</code>. The standalone <code>modelgen</code> tool emits SQLModel ORM (<code>core/db/models.py</code>), Pydantic schemas, TypeScript types, and Protobuf definitions. Regenerate everything with <code>bash ctrl/generate.sh</code>.</p>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<section id="system" class="graph-section">
|
|
<h2>SYSTEM ARCHITECTURE</h2>
|
|
<p>Browser ↔ Envoy Gateway ↔ FastAPI / detection-ui ↔ data plane (Postgres / Redis / MinIO) ↔ LAN GPU host ↔ cloud VLM providers.</p>
|
|
<div class="graph-container">
|
|
<a href="viewer.html?src=architecture/01-architecture.svg"><img src="architecture/01-architecture.svg" alt="System Architecture"></a>
|
|
</div>
|
|
<div class="legend">
|
|
<span class="browser">Browser</span>
|
|
<span class="cluster">K8s cluster</span>
|
|
<span class="gpu">GPU host (LAN)</span>
|
|
<span class="cloud">Cloud VLM</span>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="pipeline" class="graph-section">
|
|
<h2>DETECTION PIPELINE</h2>
|
|
<p>11 named stages from <code>core/detect/graph/nodes.py</code>. The runner flattens the profile's <code>PipelineConfig</code> graph into a linear sequence and runs each stage with cancel / pause / resume / step control.</p>
|
|
<div class="graph-container">
|
|
<a href="viewer.html?src=architecture/03-detection-pipeline.svg"><img src="architecture/03-detection-pipeline.svg" alt="Detection Pipeline"></a>
|
|
</div>
|
|
<div class="legend">
|
|
<span class="cluster">Browser / WASM-eligible</span>
|
|
<span class="gpu">GPU inference</span>
|
|
<span class="cloud">Cloud VLM</span>
|
|
</div>
|
|
<div class="prose" style="margin-top: 24px;">
|
|
<p><b>Control flow.</b> Each stage runs inside <code>trace_node()</code>, emits <code>running</code> → <code>done</code>/<code>skipped</code> via <code>core/detect/emit.py</code>, and writes its result to a <code>StageOutput</code> row keyed by <code>(job_id, stage_name)</code>. Between stages the runner checks three job-keyed flags: cancel (<code>set_cancel_check</code>), pause/resume (<code>threading.Event</code>), and pause-after-stage / step.</p>
|
|
<p><b>Skip flags.</b> <code>SKIP_VLM=1</code> emits <code>skipped</code> for <code>escalate_vlm</code>; <code>SKIP_CLOUD=1</code> for <code>escalate_cloud</code>. Useful in CI and dev when you don't want to burn provider credits.</p>
|
|
<p><a href="architecture/05-detection-pipeline.md" target="_blank" rel="noopener">Full pipeline reference →</a></p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="profiles" class="graph-section">
|
|
<h2>PROFILES & CHECKPOINTS</h2>
|
|
<p>Profiles are the config mechanism; checkpoints + StageOutput power the replay loop.</p>
|
|
<div class="prose">
|
|
|
|
<h3>Profile shape</h3>
|
|
<p>One <code>Profile</code> row per content type (e.g. <code>soccer_broadcast</code>) holds two JSONB blobs:</p>
|
|
<ul>
|
|
<li><code>pipeline</code> — a <code>PipelineConfig</code>: stages + edges + routing rules. The runner topologically sorts the edges, falling back to stage order when no edges are defined.</li>
|
|
<li><code>configs</code> — <code>{stage_name: {...}}</code> per-stage parameters: fps, thresholds, prompts, etc. Each stage parses its slice into a typed config (<code>FrameExtractionConfig</code>, <code>OCRConfig</code>, ...).</li>
|
|
</ul>
|
|
<p>Convention: <b>duplicate a profile and tweak it</b> rather than patching defaults at the call site. Job-level <code>config_overrides</code> exist for one-off experiments but the resolved profile is the durable artifact.</p>
|
|
|
|
<h3>Checkpoint tree</h3>
|
|
<p>A <code>Checkpoint</code> row is a tree node: <code>(parent_id, stage_name, config_overrides, stats)</code>. <b>No blobs.</b> Lets the UI show a branching history of "what configs did we try at this stage" without dragging frame data around.</p>
|
|
|
|
<h3>StageOutput (flat upsert)</h3>
|
|
<p>One row per <code>(job_id, stage_name)</code> holding the stage's output dict. Single-stage replay reads upstream outputs from here, so re-running <code>match_brands</code> with a tweaked threshold doesn't redo OCR. <code>POST /replay-stage</code> is the entry point.</p>
|
|
|
|
<h3>Replay loop</h3>
|
|
<p>The detection-app UI is the test surface: change a config, replay one stage, see the overlay rendered from the cached frame plus the new <code>StageOutput</code>. Frame caches keyed by <code>timeline_id</code> survive across replays — <code>extract_frames</code> only fires on the first run for a timeline.</p>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<section id="topology" class="graph-section">
|
|
<h2>INFERENCE TOPOLOGY</h2>
|
|
<p>Stages can run in three places. The split is what keeps the dev box light and lets one GPU host serve the whole team.</p>
|
|
<div class="prose">
|
|
|
|
<h3>Browser (OpenCV WASM)</h3>
|
|
<p>Field and edge stages can run in a Web Worker via <code>ui/detection-app/src/cv/wasmBridge.ts</code> using OpenCV WASM directly — no TypeScript ports of the algorithms. This is the fast-iteration path for the replay loop: tweak a kernel size, rerun the stage on the cached frames, see the overlay update without touching a server.</p>
|
|
|
|
<h3>API host (in-process)</h3>
|
|
<p>With <code>INFERENCE_URL=""</code> (the dev default in <code>ctrl/k8s/base/configmap.yaml</code>) every CV/ML stage calls its routine in-process. Useful when there's no GPU host wired up; works for everything except heavy YOLO/VLM workloads.</p>
|
|
|
|
<h3>GPU host (LAN)</h3>
|
|
<p>Set <code>INFERENCE_URL=http://gpu-host:8000</code> and the same stages POST to <code>core/gpu/server.py</code>. The GPU server exposes <code>/detect</code>, <code>/ocr</code>, <code>/preprocess</code>, <code>/vlm</code>, <code>/detect_edges</code>, <code>/segment_field</code> — each with a <code>/debug</code> variant that returns intermediate masks for the overlay viewer. Heavy ML deps live only in <code>core/gpu/pyproject.toml</code>; the API host doesn't import torch.</p>
|
|
|
|
<h3>Cloud VLM providers</h3>
|
|
<p>Last-resort escalation for unresolved candidates. <code>core/detect/providers/</code> wraps Anthropic, Gemini, OpenAI, and Groq. Selection is per-profile config; <code>SKIP_CLOUD=1</code> bypasses the stage entirely.</p>
|
|
|
|
</div>
|
|
</section>
|
|
|
|
<section id="data" class="graph-section">
|
|
<h2>DATA MODEL</h2>
|
|
<p>Tables generated by modelgen from <code>core/schema/models/</code> into <code>core/db/models.py</code> (SQLModel).</p>
|
|
<div class="graph-container">
|
|
<a href="viewer.html?src=architecture/02-data-model.svg"><img src="architecture/02-data-model.svg" alt="Data Model"></a>
|
|
</div>
|
|
<div class="prose" style="margin-top: 24px;">
|
|
<ul>
|
|
<li><code>MediaAsset</code> — source video file with probe metadata (duration, fps, codec).</li>
|
|
<li><code>Profile</code> — pipeline topology + per-stage config (JSONB).</li>
|
|
<li><code>Timeline</code> — user-created selection of chunks from a source asset.</li>
|
|
<li><code>Job</code> — one pipeline run on a timeline; <code>parent_id</code> chains replays into a tree.</li>
|
|
<li><code>Checkpoint</code> — tree node of stage state, no blobs.</li>
|
|
<li><code>StageOutput</code> — flat upsert per <code>(job, stage)</code>, holds output JSONB and an optional <code>checkpoint_id</code>.</li>
|
|
<li><code>Brand</code> — canonical name, aliases, source (ocr/local_vlm/cloud_llm/manual), airing history.</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="api" class="graph-section">
|
|
<h2>API</h2>
|
|
<p>FastAPI under <code>/detect/*</code> (mounted from <code>core/api/detect/</code>). Through Envoy Gateway in dev the public path is <code>/api/detect/...</code>; <code>/api/detect/stream/*</code> gets an extended idle timeout for SSE.</p>
|
|
<pre class="codeblock"><span class="c"># Sources / timelines</span>
|
|
<span class="k">GET</span> /sources
|
|
<span class="k">GET</span> /sources/{job_id}/chunks
|
|
<span class="k">POST</span> /timeline
|
|
<span class="k">GET</span> /timeline
|
|
<span class="k">GET</span> /timeline/{id}
|
|
<span class="k">DELETE</span> /timeline/{id}/cache
|
|
|
|
<span class="c"># Run control</span>
|
|
<span class="k">POST</span> /run
|
|
<span class="k">POST</span> /stop/{job_id}
|
|
<span class="k">POST</span> /pause/{job_id}
|
|
<span class="k">POST</span> /resume/{job_id}
|
|
<span class="k">POST</span> /step/{job_id}
|
|
<span class="k">POST</span> /pause-after-stage/{job_id}
|
|
<span class="k">GET</span> /status/{job_id}
|
|
<span class="k">POST</span> /clear/{job_id}
|
|
|
|
<span class="c"># Live events</span>
|
|
<span class="k">GET</span> /stream/{job_id} <span class="c"># SSE</span>
|
|
|
|
<span class="c"># Replay / checkpoints / overlays</span>
|
|
<span class="k">GET</span> /checkpoints/{timeline_id}
|
|
<span class="k">GET</span> /checkpoints/{timeline_id}/{stage}
|
|
<span class="k">GET</span> /scenarios
|
|
<span class="k">POST</span> /replay
|
|
<span class="k">POST</span> /replay-stage
|
|
<span class="k">POST</span> /overlays
|
|
<span class="k">GET</span> /overlays/{timeline_id}/{job_id}/{stage}/{seq}
|
|
|
|
<span class="c"># Config</span>
|
|
<span class="k">GET</span> /config
|
|
<span class="k">PUT</span> /config
|
|
<span class="k">GET</span> /config/profiles
|
|
<span class="k">GET</span> /config/profiles/{name}/pipeline
|
|
<span class="k">PUT</span> /config/edge-transform
|
|
<span class="k">GET</span> /config/stages
|
|
<span class="k">GET</span> /config/stages/{stage_name}
|
|
|
|
<span class="c"># Jobs</span>
|
|
<span class="k">GET</span> /jobs
|
|
<span class="k">GET</span> /jobs/{id}</pre>
|
|
</section>
|
|
|
|
<section id="storage" class="graph-section">
|
|
<h2>STORAGE</h2>
|
|
<p>S3-compatible everywhere — MinIO locally, real S3 / GCS / R2 in cloud targets. The same boto3 code path serves both; only <code>S3_ENDPOINT_URL</code> and credentials change.</p>
|
|
<div class="prose">
|
|
<ul>
|
|
<li><code>mpr-media-in</code> — source video files (chunks).</li>
|
|
<li><code>mpr-media-out</code> — per-job artifacts: extracted frame caches, debug overlays.</li>
|
|
</ul>
|
|
<p>Heavy artifacts (frames, masks, overlays) live in object storage. <code>Checkpoint</code> and <code>StageOutput</code> rows in Postgres hold structured outputs and references to S3 keys, never blobs.</p>
|
|
<p><a href="architecture/04-media-storage.md" target="_blank" rel="noopener">Full storage reference →</a></p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="modelgen" class="graph-section">
|
|
<h2>CODE GENERATION</h2>
|
|
<p>Source-of-truth dataclasses in <code>core/schema/models/</code> → typed code in four targets.</p>
|
|
<div class="prose">
|
|
<ul>
|
|
<li>SQLModel ORM tables → <code>core/db/models.py</code></li>
|
|
<li>Pydantic schemas (API request / response models)</li>
|
|
<li>TypeScript types (UI)</li>
|
|
<li>Protobuf definitions (gRPC stubs in <code>core/rpc/</code>)</li>
|
|
</ul>
|
|
</div>
|
|
<pre class="codeblock"><span class="c"># regenerate everything</span>
|
|
bash ctrl/generate.sh</pre>
|
|
</section>
|
|
|
|
<section id="dev" class="graph-section">
|
|
<h2>DEV ENVIRONMENT</h2>
|
|
<p>Tilt + Kind for local dev. Routing via Envoy Gateway on port 8080 — no nginx-ingress.</p>
|
|
<div class="prose">
|
|
<p>The Tiltfile lives at <code>ctrl/Tiltfile</code> and applies the kustomize overlay <code>ctrl/k8s/overlays/dev/</code>. Cluster name: <code>kind-mpr</code>. Tilt port-forwards Envoy (8080) and MinIO (9000 API, 9001 console).</p>
|
|
<ul>
|
|
<li><code>/api/detect/stream/*</code> → FastAPI SSE (3600s idle timeout)</li>
|
|
<li><code>/api/*</code> → FastAPI</li>
|
|
<li><code>/</code>, <code>/detection/*</code> → detection-ui (with WS upgrade for Vite HMR)</li>
|
|
</ul>
|
|
</div>
|
|
<pre class="codeblock"><span class="c"># Add to /etc/hosts</span>
|
|
127.0.0.1 mpr.local.ar k8s.mpr.local.ar
|
|
|
|
<span class="c"># Bring the cluster up</span>
|
|
cd ctrl
|
|
./kind-create.sh <span class="c"># one-time</span>
|
|
tilt up <span class="c"># builds + applies + port-forwards</span>
|
|
|
|
<span class="c"># UI: http://k8s.mpr.local.ar:8080/</span>
|
|
<span class="c"># API: http://k8s.mpr.local.ar:8080/api/</span>
|
|
<span class="c"># MinIO: http://localhost:9001 (console; admin / minioadmin)</span>
|
|
|
|
<span class="c"># Force a UI rebuild</span>
|
|
tilt trigger detection-ui</pre>
|
|
</section>
|
|
|
|
<section id="reference" class="graph-section">
|
|
<h2>QUICK REFERENCE</h2>
|
|
<p>Common commands and switches for working in MPR.</p>
|
|
<pre class="codeblock"><span class="c"># Render SVGs from DOT files</span>
|
|
for f in docs/architecture/*.dot; do dot -Tsvg "$f" -o "${f%.dot}.svg"; done
|
|
|
|
<span class="c"># Regenerate models from core/schema/models/</span>
|
|
bash ctrl/generate.sh
|
|
|
|
<span class="c"># Switch inference between local and GPU host</span>
|
|
INFERENCE_URL= <span class="c"># local (CV runs in API process)</span>
|
|
INFERENCE_URL=http://gpu-host:8000 <span class="c"># remote (core/gpu/server.py)</span>
|
|
|
|
<span class="c"># Skip VLM escalation paths</span>
|
|
SKIP_VLM=1
|
|
SKIP_CLOUD=1
|
|
|
|
<span class="c"># Tilt</span>
|
|
cd ctrl && tilt up
|
|
tilt trigger detection-ui</pre>
|
|
<div class="prose">
|
|
<p>Reference docs:</p>
|
|
<ul>
|
|
<li><a href="architecture/05-detection-pipeline.md" target="_blank" rel="noopener">Detection pipeline reference</a></li>
|
|
<li><a href="architecture/04-media-storage.md" target="_blank" rel="noopener">Media & artifact storage</a></li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var layout = document.querySelector('.layout');
|
|
var main = document.querySelector('main');
|
|
|
|
function syncActive() {
|
|
var hash = location.hash.slice(1) || 'overview';
|
|
document.querySelectorAll('.graph-section').forEach(function(s) { s.classList.remove('active'); });
|
|
document.querySelectorAll('nav a').forEach(function(a) { a.classList.remove('active'); });
|
|
var section = document.getElementById(hash);
|
|
if (section) section.classList.add('active');
|
|
var link = document.querySelector('nav a[href="#' + hash + '"]');
|
|
if (link) link.classList.add('active');
|
|
if (main) main.scrollTop = 0;
|
|
layout.classList.remove('nav-open');
|
|
}
|
|
|
|
window.addEventListener('hashchange', syncActive);
|
|
window.addEventListener('DOMContentLoaded', syncActive);
|
|
syncActive();
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target.closest('.menu-toggle') || e.target.closest('.nav-backdrop')) {
|
|
layout.classList.toggle('nav-open');
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|