update docs
This commit is contained in:
884
docs/index.html
884
docs/index.html
@@ -1,380 +1,564 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MPR - Architecture</title>
|
||||
<link rel="stylesheet" href="architecture/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<nav class="sidebar">
|
||||
<h2>MPR</h2>
|
||||
<ul>
|
||||
<li><a href="#overview">System Overview</a></li>
|
||||
<li><a href="#data-model">Data Model</a></li>
|
||||
<li><a href="#job-flow">Job Flow</a></li>
|
||||
<li><a href="#media-storage">Media Storage</a></li>
|
||||
<li><a href="#chunker-pipeline">Chunker Pipeline</a></li>
|
||||
<li><a href="#api">API (GraphQL)</a></li>
|
||||
<li><a href="#access-points">Access Points</a></li>
|
||||
<li><a href="#quick-reference">Quick Reference</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<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');
|
||||
|
||||
<main class="content">
|
||||
<h1>MPR - Media Processor</h1>
|
||||
<p>
|
||||
Media transcoding platform with three execution modes: local (Celery
|
||||
+ MinIO), AWS (Step Functions + Lambda + S3), and GCP (Cloud Run
|
||||
Jobs + GCS). Storage is S3-compatible across all environments.
|
||||
</p>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
<h2 id="overview">System Overview</h2>
|
||||
<div class="diagram-container">
|
||||
<div class="diagram">
|
||||
<h3>Local Architecture (Development)</h3>
|
||||
<object
|
||||
type="image/svg+xml"
|
||||
data="architecture/01a-local-architecture.svg"
|
||||
>
|
||||
<img
|
||||
src="architecture/01a-local-architecture.svg"
|
||||
alt="Local Architecture"
|
||||
/>
|
||||
</object>
|
||||
<a
|
||||
href="architecture/01a-local-architecture.svg"
|
||||
target="_blank"
|
||||
>Open full size</a
|
||||
>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<h3>AWS Architecture (Production)</h3>
|
||||
<object
|
||||
type="image/svg+xml"
|
||||
data="architecture/01b-aws-architecture.svg"
|
||||
>
|
||||
<img
|
||||
src="architecture/01b-aws-architecture.svg"
|
||||
alt="AWS Architecture"
|
||||
/>
|
||||
</object>
|
||||
<a href="architecture/01b-aws-architecture.svg" target="_blank"
|
||||
>Open full size</a
|
||||
>
|
||||
</div>
|
||||
<div class="diagram">
|
||||
<h3>GCP Architecture (Production)</h3>
|
||||
<object
|
||||
type="image/svg+xml"
|
||||
data="architecture/01c-gcp-architecture.svg"
|
||||
>
|
||||
<img
|
||||
src="architecture/01c-gcp-architecture.svg"
|
||||
alt="GCP Architecture"
|
||||
/>
|
||||
</object>
|
||||
<a href="architecture/01c-gcp-architecture.svg" target="_blank"
|
||||
>Open full size</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
body {
|
||||
background: #0a0e17;
|
||||
color: #e8eaf0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
line-height: 1.6;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Components</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="color-box" style="background: #e8f4f8"></span>
|
||||
Reverse Proxy (nginx)
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #f0f8e8"></span>
|
||||
Application Layer (Django Admin, GraphQL API, Timeline UI)
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #fff8e8"></span>
|
||||
Worker Layer (Celery local mode)
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #fde8d0"></span>
|
||||
AWS (Step Functions, Lambda)
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #e8f0fd"></span>
|
||||
GCP (Cloud Run Jobs + GCS)
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #f8e8f0"></span>
|
||||
Data Layer (PostgreSQL, Redis)
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #f0f0f0"></span>
|
||||
S3-compatible Storage (MinIO / AWS S3 / GCS)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #1e2a4a;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
<h2 id="data-model">Data Model</h2>
|
||||
<div class="diagram-container">
|
||||
<div class="diagram">
|
||||
<h3>Entity Relationships</h3>
|
||||
<object
|
||||
type="image/svg+xml"
|
||||
data="architecture/02-data-model.svg"
|
||||
>
|
||||
<img
|
||||
src="architecture/02-data-model.svg"
|
||||
alt="Data Model"
|
||||
/>
|
||||
</object>
|
||||
<a href="architecture/02-data-model.svg" target="_blank"
|
||||
>Open full size</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
header h1 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 3px;
|
||||
color: #0066ff;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Entities</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="color-box" style="background: #4a90d9"></span>
|
||||
MediaAsset - Video/audio files with metadata
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #50b050"></span>
|
||||
TranscodePreset - Encoding configurations
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #d9534f"></span>
|
||||
TranscodeJob - Processing queue items
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
header .subtitle {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
<h2 id="job-flow">Job Flow</h2>
|
||||
<div class="diagram-container">
|
||||
<div class="diagram">
|
||||
<h3>Job Lifecycle</h3>
|
||||
<object
|
||||
type="image/svg+xml"
|
||||
data="architecture/03-job-flow.svg"
|
||||
>
|
||||
<img src="architecture/03-job-flow.svg" alt="Job Flow" />
|
||||
</object>
|
||||
<a href="architecture/03-job-flow.svg" target="_blank"
|
||||
>Open full size</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
.layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Job States</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="color-box" style="background: #ffc107"></span>
|
||||
PENDING - Waiting in queue
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #17a2b8"></span>
|
||||
PROCESSING - Worker executing
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #28a745"></span>
|
||||
COMPLETED - Success
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #dc3545"></span>
|
||||
FAILED - Error occurred
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #6c757d"></span>
|
||||
CANCELLED - User cancelled
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Execution Modes</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="color-box" style="background: #e8f4e8"></span>
|
||||
Local: Celery + MinIO (S3 API) + FFmpeg
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #fde8d0"></span>
|
||||
Lambda: Step Functions + Lambda + AWS S3
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #e8f0fd"></span>
|
||||
GCP: Cloud Run Jobs + GCS (S3 compat)
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
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;
|
||||
}
|
||||
|
||||
<h2 id="media-storage">Media Storage</h2>
|
||||
<div class="diagram-container">
|
||||
<p>
|
||||
MPR separates media into <strong>input</strong> and
|
||||
<strong>output</strong> paths, each independently configurable.
|
||||
File paths are stored
|
||||
<strong>relative to their respective root</strong> to ensure
|
||||
portability between local development and cloud deployments.
|
||||
</p>
|
||||
</div>
|
||||
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;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Input / Output Separation</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="color-box" style="background: #4a90d9"></span>
|
||||
<code>MEDIA_IN</code> - Source media files to process
|
||||
</li>
|
||||
<li>
|
||||
<span class="color-box" style="background: #50b050"></span>
|
||||
<code>MEDIA_OUT</code> - Transcoded/trimmed output files
|
||||
</li>
|
||||
</ul>
|
||||
<p><strong>Why Relative Paths?</strong></p>
|
||||
<ul>
|
||||
<li>Portability: Same database works locally and in cloud</li>
|
||||
<li>Flexibility: Easy to switch between storage backends</li>
|
||||
<li>Simplicity: No need to update paths when migrating</li>
|
||||
</ul>
|
||||
</div>
|
||||
nav a:hover { color: #e8eaf0; background: #1a2340; }
|
||||
nav a.active { color: #0066ff; border-left-color: #0066ff; background: #0d1a33; }
|
||||
|
||||
<div class="legend">
|
||||
<h3>Local Development</h3>
|
||||
<pre><code>MEDIA_IN=/app/media/in
|
||||
MEDIA_OUT=/app/media/out
|
||||
main {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 32px 48px;
|
||||
}
|
||||
|
||||
/app/media/
|
||||
├── in/ # Source files
|
||||
│ ├── video1.mp4
|
||||
│ └── subfolder/video3.mp4
|
||||
└── out/ # Transcoded output
|
||||
└── video1_h264.mp4</code></pre>
|
||||
</div>
|
||||
.graph-section {
|
||||
display: none;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>AWS/Cloud Deployment</h3>
|
||||
<pre><code>MEDIA_IN=s3://source-bucket/media/
|
||||
MEDIA_OUT=s3://output-bucket/transcoded/
|
||||
MEDIA_BASE_URL=https://source-bucket.s3.amazonaws.com/media/</code></pre>
|
||||
<p>
|
||||
Database paths remain unchanged (already relative). Just upload
|
||||
files to S3 and update environment variables.
|
||||
</p>
|
||||
</div>
|
||||
.graph-section:target,
|
||||
.graph-section.active { display: block; }
|
||||
|
||||
<p>
|
||||
<a href="architecture/04-media-storage.md" target="_blank"
|
||||
>Full Media Storage Documentation →</a
|
||||
>
|
||||
</p>
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
<h2 id="chunker-pipeline">Chunker Pipeline</h2>
|
||||
<div class="diagram-container">
|
||||
<p>
|
||||
The chunker pipeline splits media into time-based segments,
|
||||
streaming real-time events from worker threads through Redis
|
||||
and gRPC-Web to the browser UI. 7 hops from worker thread to pixel.
|
||||
</p>
|
||||
</div>
|
||||
.graph-section h2 {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #8892a8;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Event Path</h3>
|
||||
<pre><code>Worker thread → Pipeline._emit() → event_bridge() → Redis RPUSH
|
||||
→ [50ms poll] gRPC server LRANGE → yield protobuf
|
||||
→ HTTP/2 frame → Envoy (grpc-web filter)
|
||||
→ HTTP/1.1 chunk → nginx (proxy_buffering off)
|
||||
→ fetch ReadableStream → protobuf-ts decode
|
||||
→ setEvents([...prev, evt]) → React re-render</code></pre>
|
||||
</div>
|
||||
.graph-section > p {
|
||||
font-size: 13px;
|
||||
color: #4a5568;
|
||||
margin-bottom: 24px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Thread Model (inside Celery worker)</h3>
|
||||
<pre><code>Celery worker process
|
||||
└─ run_job task thread
|
||||
└─ Pipeline.run()
|
||||
├─ Producer thread — enqueues chunks
|
||||
├─ Monitor thread — emits progress every 500ms
|
||||
├─ Worker thread 0 — pulls from queue, processes
|
||||
├─ Worker thread 1 — pulls from queue, processes
|
||||
├─ Worker thread 2 — pulls from queue, processes
|
||||
└─ Worker thread 3 — pulls from queue, processes</code></pre>
|
||||
</div>
|
||||
.graph-container {
|
||||
background: #0a0e17;
|
||||
border: 1px solid #1e2a4a;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
<div class="legend">
|
||||
<h3>Infrastructure</h3>
|
||||
<ul>
|
||||
<li><code>nginx :80</code> - Reverse proxy, static file serving</li>
|
||||
<li><code>fastapi :8702</code> - GraphQL API (Strawberry)</li>
|
||||
<li><code>celery</code> - Task worker (runs pipeline)</li>
|
||||
<li><code>redis :6379</code> - Event bus + Celery broker</li>
|
||||
<li><code>grpc :50051</code> - gRPC server (StreamChunkPipeline)</li>
|
||||
<li><code>envoy :8090</code> - gRPC-Web ↔ native gRPC translation</li>
|
||||
<li><code>minio :9000</code> - S3-compatible source media storage</li>
|
||||
<li><code>postgres :5432</code> - Job/asset metadata</li>
|
||||
</ul>
|
||||
</div>
|
||||
.graph-container a { display: block; }
|
||||
.graph-container img { max-width: 100%; height: auto; }
|
||||
|
||||
<p>
|
||||
<a href="architecture/05-chunker-pipeline.md" target="_blank"
|
||||
>Full Chunker Pipeline Documentation →</a
|
||||
>
|
||||
</p>
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 24px;
|
||||
margin-top: 16px;
|
||||
font-size: 11px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
<h2 id="api">API (GraphQL)</h2>
|
||||
<div class="legend">
|
||||
<p>
|
||||
All client interactions go through GraphQL at
|
||||
<code>/graphql</code>.
|
||||
</p>
|
||||
<pre><code># GraphiQL IDE
|
||||
http://mpr.local.ar/graphql
|
||||
.legend span::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
# Queries
|
||||
query { assets(status: "ready") { id filename duration } }
|
||||
query { jobs(status: "processing") { id status progress } }
|
||||
query { presets { id name container videoCodec } }
|
||||
query { systemStatus { status version } }
|
||||
.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; }
|
||||
|
||||
# Mutations
|
||||
mutation { scanMediaFolder { found registered skipped } }
|
||||
mutation { createJob(input: { sourceAssetId: "...", presetId: "..." }) { id status } }
|
||||
mutation { cancelJob(id: "...") { id status } }
|
||||
mutation { retryJob(id: "...") { id status } }
|
||||
mutation { updateAsset(id: "...", input: { comments: "..." }) { id comments } }
|
||||
mutation { deleteAsset(id: "...") { ok } }
|
||||
/* 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; }
|
||||
|
||||
# Lambda callback (REST)
|
||||
POST /api/jobs/{id}/callback - Lambda completion webhook</code></pre>
|
||||
<p><strong>Supported File Types:</strong></p>
|
||||
<p>
|
||||
Video: mp4, mkv, avi, mov, webm, flv, wmv, m4v<br />
|
||||
Audio: mp3, wav, flac, aac, ogg, m4a
|
||||
</p>
|
||||
</div>
|
||||
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; }
|
||||
|
||||
<h2 id="access-points">Access Points</h2>
|
||||
<pre><code># Add to /etc/hosts
|
||||
127.0.0.1 mpr.local.ar
|
||||
/* 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; }
|
||||
|
||||
# URLs
|
||||
http://mpr.local.ar/admin - Django Admin
|
||||
http://mpr.local.ar/graphql - GraphiQL IDE
|
||||
http://mpr.local.ar/ - Timeline UI
|
||||
http://mpr.local.ar/chunker/ - Chunker UI
|
||||
http://localhost:9001 - MinIO Console
|
||||
.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; }
|
||||
|
||||
# AWS deployment
|
||||
https://mpr.mcrn.ar/ - Production</code></pre>
|
||||
@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; }
|
||||
|
||||
<h2 id="quick-reference">Quick Reference</h2>
|
||||
<pre><code># Render SVGs from DOT files
|
||||
.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
|
||||
|
||||
# Switch executor mode
|
||||
MPR_EXECUTOR=local # Celery + MinIO
|
||||
MPR_EXECUTOR=lambda # Step Functions + Lambda + S3
|
||||
MPR_EXECUTOR=gcp # Cloud Run Jobs + GCS</code></pre>
|
||||
</main>
|
||||
</body>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user