Remove REST API, keep GraphQL as sole API

- Add missing GraphQL mutations: retryJob, updateAsset, deleteAsset
- Add UpdateAssetRequest and DeleteResult to schema source of truth
- Move Lambda callback endpoint to main.py (only REST endpoint)
- Remove REST routes, pydantic schemas, and deps
- Remove pydantic target from modelgen.json
- Update architecture diagrams and documentation
This commit is contained in:
2026-02-12 20:07:51 -03:00
parent dbbaad5b94
commit 4e9d731cff
24 changed files with 393 additions and 1031 deletions

View File

@@ -1,54 +0,0 @@
"""
FastAPI dependencies.
Provides database sessions, settings, and common dependencies.
"""
import os
from functools import lru_cache
from typing import Generator
import django
from django.conf import settings as django_settings
# Initialize Django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mpr.settings")
django.setup()
from mpr.media_assets.models import MediaAsset, TranscodeJob, TranscodePreset
@lru_cache
def get_settings():
"""Get Django settings."""
return django_settings
def get_asset(asset_id: str) -> MediaAsset:
"""Get asset by ID or raise 404."""
from fastapi import HTTPException
try:
return MediaAsset.objects.get(id=asset_id)
except MediaAsset.DoesNotExist:
raise HTTPException(status_code=404, detail="Asset not found")
def get_preset(preset_id: str) -> TranscodePreset:
"""Get preset by ID or raise 404."""
from fastapi import HTTPException
try:
return TranscodePreset.objects.get(id=preset_id)
except TranscodePreset.DoesNotExist:
raise HTTPException(status_code=404, detail="Preset not found")
def get_job(job_id: str) -> TranscodeJob:
"""Get job by ID or raise 404."""
from fastapi import HTTPException
try:
return TranscodeJob.objects.get(id=job_id)
except TranscodeJob.DoesNotExist:
raise HTTPException(status_code=404, detail="Job not found")

View File

@@ -1,7 +1,7 @@
"""
GraphQL API using graphene, mounted on FastAPI/Starlette.
Provides the same data as the REST API but via GraphQL queries and mutations.
Primary API for MPR — all client interactions go through GraphQL.
Uses Django ORM directly for data access.
Types are generated from schema/ via modelgen — see api/schema/graphql.py.
"""
@@ -12,11 +12,13 @@ import graphene
from api.schema.graphql import (
CreateJobInput,
DeleteResultType,
MediaAssetType,
ScanResultType,
SystemStatusType,
TranscodeJobType,
TranscodePresetType,
UpdateAssetInput,
)
from core.storage import BUCKET_IN, list_objects
@@ -238,10 +240,83 @@ class CancelJob(graphene.Mutation):
return job
class RetryJob(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
Output = TranscodeJobType
def mutate(self, info, id):
from mpr.media_assets.models import TranscodeJob
try:
job = TranscodeJob.objects.get(id=id)
except TranscodeJob.DoesNotExist:
raise Exception("Job not found")
if job.status != "failed":
raise Exception("Only failed jobs can be retried")
job.status = "pending"
job.progress = 0
job.error_message = None
job.save(update_fields=["status", "progress", "error_message"])
return job
class UpdateAsset(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
input = UpdateAssetInput(required=True)
Output = MediaAssetType
def mutate(self, info, id, input):
from mpr.media_assets.models import MediaAsset
try:
asset = MediaAsset.objects.get(id=id)
except MediaAsset.DoesNotExist:
raise Exception("Asset not found")
update_fields = []
if input.comments is not None:
asset.comments = input.comments
update_fields.append("comments")
if input.tags is not None:
asset.tags = input.tags
update_fields.append("tags")
if update_fields:
asset.save(update_fields=update_fields)
return asset
class DeleteAsset(graphene.Mutation):
class Arguments:
id = graphene.UUID(required=True)
Output = DeleteResultType
def mutate(self, info, id):
from mpr.media_assets.models import MediaAsset
try:
asset = MediaAsset.objects.get(id=id)
asset.delete()
return DeleteResultType(ok=True)
except MediaAsset.DoesNotExist:
raise Exception("Asset not found")
class Mutation(graphene.ObjectType):
scan_media_folder = ScanMediaFolder.Field()
create_job = CreateJob.Field()
cancel_job = CancelJob.Field()
retry_job = RetryJob.Field()
update_asset = UpdateAsset.Field()
delete_asset = DeleteAsset.Field()
# ---------------------------------------------------------------------------

View File

@@ -1,11 +1,13 @@
"""
MPR FastAPI Application
Main entry point for the REST API.
Serves GraphQL API and Lambda callback endpoint.
"""
import os
import sys
from typing import Optional
from uuid import UUID
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -17,16 +19,17 @@ import django
django.setup()
from fastapi import FastAPI
from fastapi import FastAPI, Header, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from starlette_graphene3 import GraphQLApp, make_graphiql_handler
from api.graphql import schema as graphql_schema
from api.routes import assets_router, jobs_router, presets_router, system_router
from starlette_graphene3 import GraphQLApp, make_graphiql_handler
CALLBACK_API_KEY = os.environ.get("CALLBACK_API_KEY", "")
app = FastAPI(
title="MPR API",
description="Media Processor REST API",
description="Media Processor — GraphQL API",
version="0.1.0",
docs_url="/docs",
redoc_url="/redoc",
@@ -41,12 +44,6 @@ app.add_middleware(
allow_headers=["*"],
)
# Routes - all under /api prefix
app.include_router(system_router, prefix="/api")
app.include_router(assets_router, prefix="/api")
app.include_router(presets_router, prefix="/api")
app.include_router(jobs_router, prefix="/api")
# GraphQL
app.mount("/graphql", GraphQLApp(schema=graphql_schema, on_get=make_graphiql_handler()))
@@ -57,5 +54,45 @@ def root():
return {
"name": "MPR API",
"version": "0.1.0",
"docs": "/docs",
"graphql": "/graphql",
}
@app.post("/api/jobs/{job_id}/callback")
def job_callback(
job_id: UUID,
payload: dict,
x_api_key: Optional[str] = Header(None),
):
"""
Callback endpoint for Lambda to report job completion.
Protected by API key.
"""
if CALLBACK_API_KEY and x_api_key != CALLBACK_API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
from django.utils import timezone
from mpr.media_assets.models import TranscodeJob
try:
job = TranscodeJob.objects.get(id=job_id)
except TranscodeJob.DoesNotExist:
raise HTTPException(status_code=404, detail="Job not found")
status = payload.get("status", "failed")
job.status = status
job.progress = 100.0 if status == "completed" else job.progress
update_fields = ["status", "progress"]
if payload.get("error"):
job.error_message = payload["error"]
update_fields.append("error_message")
if status in ("completed", "failed"):
job.completed_at = timezone.now()
update_fields.append("completed_at")
job.save(update_fields=update_fields)
return {"ok": True}

View File

@@ -1,8 +0,0 @@
"""API Routes."""
from .assets import router as assets_router
from .jobs import router as jobs_router
from .presets import router as presets_router
from .system import router as system_router
__all__ = ["assets_router", "jobs_router", "presets_router", "system_router"]

View File

@@ -1,117 +0,0 @@
"""
Asset endpoints - media file registration and metadata.
"""
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from api.deps import get_asset
from api.schema import AssetCreate, AssetResponse, AssetUpdate
from core.storage import BUCKET_IN, list_objects
router = APIRouter(prefix="/assets", tags=["assets"])
# Supported media extensions
VIDEO_EXTS = {".mp4", ".mkv", ".avi", ".mov", ".webm", ".flv", ".wmv", ".m4v"}
AUDIO_EXTS = {".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"}
MEDIA_EXTS = VIDEO_EXTS | AUDIO_EXTS
@router.post("/", response_model=AssetResponse, status_code=201)
def create_asset(data: AssetCreate):
"""Register a media file as an asset."""
from mpr.media_assets.models import MediaAsset
asset = MediaAsset.objects.create(
filename=data.filename or data.file_path.split("/")[-1],
file_path=data.file_path,
file_size=data.file_size,
)
return asset
@router.get("/", response_model=list[AssetResponse])
def list_assets(
status: Optional[str] = Query(None, description="Filter by status"),
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
):
"""List assets with optional filtering."""
from mpr.media_assets.models import MediaAsset
qs = MediaAsset.objects.all()
if status:
qs = qs.filter(status=status)
return list(qs[offset : offset + limit])
@router.get("/{asset_id}", response_model=AssetResponse)
def get_asset_detail(asset_id: UUID, asset=Depends(get_asset)):
"""Get asset details."""
return asset
@router.patch("/{asset_id}", response_model=AssetResponse)
def update_asset(asset_id: UUID, data: AssetUpdate, asset=Depends(get_asset)):
"""Update asset metadata (comments, tags)."""
update_fields = []
if data.comments is not None:
asset.comments = data.comments
update_fields.append("comments")
if data.tags is not None:
asset.tags = data.tags
update_fields.append("tags")
if update_fields:
asset.save(update_fields=update_fields)
return asset
@router.delete("/{asset_id}", status_code=204)
def delete_asset(asset_id: UUID, asset=Depends(get_asset)):
"""Delete an asset."""
asset.delete()
@router.post("/scan", response_model=dict)
def scan_media_folder():
"""
Scan the S3 media-in bucket for new video/audio files and register them as assets.
"""
from mpr.media_assets.models import MediaAsset
# List objects from S3 bucket
objects = list_objects(BUCKET_IN, extensions=MEDIA_EXTS)
# Get existing filenames to avoid duplicates
existing_filenames = set(MediaAsset.objects.values_list("filename", flat=True))
registered_files = []
skipped_files = []
for obj in objects:
if obj["filename"] in existing_filenames:
skipped_files.append(obj["filename"])
continue
try:
MediaAsset.objects.create(
filename=obj["filename"],
file_path=obj["key"],
file_size=obj["size"],
)
registered_files.append(obj["filename"])
except Exception as e:
print(f"Error registering {obj['filename']}: {e}")
return {
"found": len(objects),
"registered": len(registered_files),
"skipped": len(skipped_files),
"files": registered_files,
}

View File

@@ -1,233 +0,0 @@
"""
Job endpoints - transcode/trim job management.
"""
import os
from pathlib import Path
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, Header, HTTPException, Query
from api.deps import get_asset, get_job, get_preset
from api.schema import JobCreate, JobResponse
router = APIRouter(prefix="/jobs", tags=["jobs"])
CALLBACK_API_KEY = os.environ.get("CALLBACK_API_KEY", "")
@router.post("/", response_model=JobResponse, status_code=201)
def create_job(data: JobCreate):
"""
Create a transcode or trim job.
- With preset_id: Full transcode using preset settings
- Without preset_id but with trim_start/end: Trim only (stream copy)
"""
from mpr.media_assets.models import MediaAsset, TranscodeJob, TranscodePreset
# Get source asset
try:
source = MediaAsset.objects.get(id=data.source_asset_id)
except MediaAsset.DoesNotExist:
raise HTTPException(status_code=404, detail="Source asset not found")
# Get preset if specified
preset = None
preset_snapshot = {}
if data.preset_id:
try:
preset = TranscodePreset.objects.get(id=data.preset_id)
preset_snapshot = {
"name": preset.name,
"container": preset.container,
"video_codec": preset.video_codec,
"video_bitrate": preset.video_bitrate,
"video_crf": preset.video_crf,
"video_preset": preset.video_preset,
"resolution": preset.resolution,
"framerate": preset.framerate,
"audio_codec": preset.audio_codec,
"audio_bitrate": preset.audio_bitrate,
"audio_channels": preset.audio_channels,
"audio_samplerate": preset.audio_samplerate,
"extra_args": preset.extra_args,
}
except TranscodePreset.DoesNotExist:
raise HTTPException(status_code=404, detail="Preset not found")
# Validate trim-only job
if not preset and not data.trim_start and not data.trim_end:
raise HTTPException(
status_code=400, detail="Must specify preset_id or trim_start/trim_end"
)
# Generate output filename - stored as S3 key in output bucket
output_filename = data.output_filename
if not output_filename:
stem = Path(source.filename).stem
ext = preset_snapshot.get("container", "mp4") if preset else "mp4"
output_filename = f"{stem}_output.{ext}"
# Create job
job = TranscodeJob.objects.create(
source_asset_id=source.id,
preset_id=preset.id if preset else None,
preset_snapshot=preset_snapshot,
trim_start=data.trim_start,
trim_end=data.trim_end,
output_filename=output_filename,
output_path=output_filename, # S3 key in output bucket
priority=data.priority or 0,
)
# Dispatch based on executor mode
executor_mode = os.environ.get("MPR_EXECUTOR", "local")
if executor_mode == "lambda":
_dispatch_lambda(job, source, preset_snapshot)
else:
_dispatch_celery(job, source, preset_snapshot)
return job
def _dispatch_celery(job, source, preset_snapshot):
"""Dispatch job to Celery worker."""
from task.tasks import run_transcode_job
result = run_transcode_job.delay(
job_id=str(job.id),
source_key=source.file_path,
output_key=job.output_filename,
preset=preset_snapshot or None,
trim_start=job.trim_start,
trim_end=job.trim_end,
duration=source.duration,
)
job.celery_task_id = result.id
job.save(update_fields=["celery_task_id"])
def _dispatch_lambda(job, source, preset_snapshot):
"""Dispatch job to AWS Step Functions."""
from task.executor import get_executor
executor = get_executor()
executor.run(
job_id=str(job.id),
source_path=source.file_path,
output_path=job.output_filename,
preset=preset_snapshot or None,
trim_start=job.trim_start,
trim_end=job.trim_end,
duration=source.duration,
)
@router.post("/{job_id}/callback")
def job_callback(
job_id: UUID,
payload: dict,
x_api_key: Optional[str] = Header(None),
):
"""
Callback endpoint for Lambda to report job completion.
Protected by API key.
"""
if CALLBACK_API_KEY and x_api_key != CALLBACK_API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
from django.utils import timezone
from mpr.media_assets.models import TranscodeJob
try:
job = TranscodeJob.objects.get(id=job_id)
except TranscodeJob.DoesNotExist:
raise HTTPException(status_code=404, detail="Job not found")
status = payload.get("status", "failed")
job.status = status
job.progress = 100.0 if status == "completed" else job.progress
update_fields = ["status", "progress"]
if payload.get("error"):
job.error_message = payload["error"]
update_fields.append("error_message")
if status == "completed":
job.completed_at = timezone.now()
update_fields.append("completed_at")
elif status == "failed":
job.completed_at = timezone.now()
update_fields.append("completed_at")
job.save(update_fields=update_fields)
return {"ok": True}
@router.get("/", response_model=list[JobResponse])
def list_jobs(
status: Optional[str] = Query(None, description="Filter by status"),
source_asset_id: Optional[UUID] = Query(None),
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
):
"""List jobs with optional filtering."""
from mpr.media_assets.models import TranscodeJob
qs = TranscodeJob.objects.all()
if status:
qs = qs.filter(status=status)
if source_asset_id:
qs = qs.filter(source_asset_id=source_asset_id)
return list(qs[offset : offset + limit])
@router.get("/{job_id}", response_model=JobResponse)
def get_job_detail(job_id: UUID, job=Depends(get_job)):
"""Get job details including progress."""
return job
@router.get("/{job_id}/progress")
def get_job_progress(job_id: UUID, job=Depends(get_job)):
"""Get real-time job progress."""
return {
"job_id": str(job.id),
"status": job.status,
"progress": job.progress,
"current_frame": job.current_frame,
"current_time": job.current_time,
"speed": job.speed,
}
@router.post("/{job_id}/cancel", response_model=JobResponse)
def cancel_job(job_id: UUID, job=Depends(get_job)):
"""Cancel a pending or processing job."""
if job.status not in ("pending", "processing"):
raise HTTPException(
status_code=400, detail=f"Cannot cancel job with status: {job.status}"
)
job.status = "cancelled"
job.save(update_fields=["status"])
return job
@router.post("/{job_id}/retry", response_model=JobResponse)
def retry_job(job_id: UUID, job=Depends(get_job)):
"""Retry a failed job."""
if job.status != "failed":
raise HTTPException(status_code=400, detail="Only failed jobs can be retried")
job.status = "pending"
job.progress = 0
job.error_message = None
job.save(update_fields=["status", "progress", "error_message"])
return job

View File

@@ -1,100 +0,0 @@
"""
Preset endpoints - transcode configuration templates.
"""
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException
from api.deps import get_preset
from api.schema import PresetCreate, PresetResponse, PresetUpdate
router = APIRouter(prefix="/presets", tags=["presets"])
@router.post("/", response_model=PresetResponse, status_code=201)
def create_preset(data: PresetCreate):
"""Create a custom preset."""
from mpr.media_assets.models import TranscodePreset
preset = TranscodePreset.objects.create(
name=data.name,
description=data.description or "",
container=data.container or "mp4",
video_codec=data.video_codec or "libx264",
video_bitrate=data.video_bitrate,
video_crf=data.video_crf,
video_preset=data.video_preset,
resolution=data.resolution,
framerate=data.framerate,
audio_codec=data.audio_codec or "aac",
audio_bitrate=data.audio_bitrate,
audio_channels=data.audio_channels,
audio_samplerate=data.audio_samplerate,
extra_args=data.extra_args or [],
is_builtin=False,
)
return preset
@router.get("/", response_model=list[PresetResponse])
def list_presets(include_builtin: bool = True):
"""List all presets."""
from mpr.media_assets.models import TranscodePreset
qs = TranscodePreset.objects.all()
if not include_builtin:
qs = qs.filter(is_builtin=False)
return list(qs)
@router.get("/{preset_id}", response_model=PresetResponse)
def get_preset_detail(preset_id: UUID, preset=Depends(get_preset)):
"""Get preset details."""
return preset
@router.patch("/{preset_id}", response_model=PresetResponse)
def update_preset(preset_id: UUID, data: PresetUpdate, preset=Depends(get_preset)):
"""Update a custom preset. Builtin presets cannot be modified."""
if preset.is_builtin:
raise HTTPException(status_code=403, detail="Cannot modify builtin preset")
update_fields = []
for field in [
"name",
"description",
"container",
"video_codec",
"video_bitrate",
"video_crf",
"video_preset",
"resolution",
"framerate",
"audio_codec",
"audio_bitrate",
"audio_channels",
"audio_samplerate",
"extra_args",
]:
value = getattr(data, field, None)
if value is not None:
setattr(preset, field, value)
update_fields.append(field)
if update_fields:
preset.save(update_fields=update_fields)
return preset
@router.delete("/{preset_id}", status_code=204)
def delete_preset(preset_id: UUID, preset=Depends(get_preset)):
"""Delete a custom preset. Builtin presets cannot be deleted."""
if preset.is_builtin:
raise HTTPException(status_code=403, detail="Cannot delete builtin preset")
preset.delete()

View File

@@ -1,51 +0,0 @@
"""
System endpoints - health checks and FFmpeg capabilities.
"""
from fastapi import APIRouter
from core.ffmpeg import get_decoders, get_encoders, get_formats
router = APIRouter(prefix="/system", tags=["system"])
@router.get("/health")
def health_check():
"""Health check endpoint."""
return {"status": "healthy"}
@router.get("/status")
def system_status():
"""System status for UI."""
return {"status": "ok", "version": "0.1.0"}
@router.get("/worker")
def worker_status():
"""Worker status from gRPC."""
try:
from rpc.client import get_client
client = get_client()
status = client.get_worker_status()
if status:
return status
return {"available": False, "error": "No response from worker"}
except Exception as e:
return {"available": False, "error": str(e)}
@router.get("/ffmpeg/codecs")
def ffmpeg_codecs():
"""Get available FFmpeg encoders and decoders."""
return {
"encoders": get_encoders(),
"decoders": get_decoders(),
}
@router.get("/ffmpeg/formats")
def ffmpeg_formats():
"""Get available FFmpeg muxers and demuxers."""
return get_formats()

View File

@@ -1,10 +0,0 @@
"""API Schemas - GENERATED FILE"""
from .base import BaseSchema
from .asset import AssetCreate, AssetUpdate, AssetResponse
from .asset import AssetStatus
from .preset import PresetCreate, PresetUpdate, PresetResponse
from .job import JobCreate, JobUpdate, JobResponse
from .job import JobStatus
__all__ = ["BaseSchema", "AssetCreate", "AssetUpdate", "AssetResponse", "AssetStatus", "PresetCreate", "PresetUpdate", "PresetResponse", "JobCreate", "JobUpdate", "JobResponse", "JobStatus"]

View File

@@ -1,70 +0,0 @@
"""MediaAsset Schemas - GENERATED FILE"""
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID
from .base import BaseSchema
class AssetStatus(str, Enum):
PENDING = "pending"
READY = "ready"
ERROR = "error"
class AssetCreate(BaseSchema):
"""AssetCreate schema."""
filename: str
file_path: str
file_size: Optional[int] = None
duration: Optional[float] = None
video_codec: Optional[str] = None
audio_codec: Optional[str] = None
width: Optional[int] = None
height: Optional[int] = None
framerate: Optional[float] = None
bitrate: Optional[int] = None
properties: Dict[str, Any]
comments: str = ""
tags: List[str] = Field(default_factory=list)
class AssetUpdate(BaseSchema):
"""AssetUpdate schema."""
filename: Optional[str] = None
file_path: Optional[str] = None
status: Optional[AssetStatus] = None
error_message: Optional[str] = None
file_size: Optional[int] = None
duration: Optional[float] = None
video_codec: Optional[str] = None
audio_codec: Optional[str] = None
width: Optional[int] = None
height: Optional[int] = None
framerate: Optional[float] = None
bitrate: Optional[int] = None
properties: Optional[Dict[str, Any]] = None
comments: Optional[str] = None
tags: Optional[List[str]] = None
class AssetResponse(BaseSchema):
"""AssetResponse schema."""
id: UUID
filename: str
file_path: str
status: AssetStatus = "AssetStatus.PENDING"
error_message: Optional[str] = None
file_size: Optional[int] = None
duration: Optional[float] = None
video_codec: Optional[str] = None
audio_codec: Optional[str] = None
width: Optional[int] = None
height: Optional[int] = None
framerate: Optional[float] = None
bitrate: Optional[int] = None
properties: Dict[str, Any]
comments: str = ""
tags: List[str] = Field(default_factory=list)
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None

View File

@@ -1,8 +0,0 @@
"""Pydantic Base Schema - GENERATED FILE"""
from pydantic import BaseModel, ConfigDict
class BaseSchema(BaseModel):
"""Base schema with ORM mode."""
model_config = ConfigDict(from_attributes=True)

View File

@@ -104,6 +104,13 @@ class CreateJobInput(graphene.InputObjectType):
priority = graphene.Int(default_value=0)
class UpdateAssetInput(graphene.InputObjectType):
"""Request body for updating asset metadata."""
comments = graphene.String()
tags = graphene.List(graphene.String)
class SystemStatusType(graphene.ObjectType):
"""System status response."""
@@ -120,6 +127,12 @@ class ScanResultType(graphene.ObjectType):
files = graphene.List(graphene.String)
class DeleteResultType(graphene.ObjectType):
"""Result of a delete operation."""
ok = graphene.Boolean()
class WorkerStatusType(graphene.ObjectType):
"""Worker health and capabilities."""

View File

@@ -1,83 +0,0 @@
"""TranscodeJob Schemas - GENERATED FILE"""
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID
from .base import BaseSchema
class JobStatus(str, Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
class JobCreate(BaseSchema):
"""JobCreate schema."""
source_asset_id: UUID
preset_id: Optional[UUID] = None
preset_snapshot: Dict[str, Any]
trim_start: Optional[float] = None
trim_end: Optional[float] = None
output_filename: str = ""
output_path: Optional[str] = None
output_asset_id: Optional[UUID] = None
progress: float = 0.0
current_frame: Optional[int] = None
current_time: Optional[float] = None
speed: Optional[str] = None
celery_task_id: Optional[str] = None
execution_arn: Optional[str] = None
priority: int = 0
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
class JobUpdate(BaseSchema):
"""JobUpdate schema."""
source_asset_id: Optional[UUID] = None
preset_id: Optional[UUID] = None
preset_snapshot: Optional[Dict[str, Any]] = None
trim_start: Optional[float] = None
trim_end: Optional[float] = None
output_filename: Optional[str] = None
output_path: Optional[str] = None
output_asset_id: Optional[UUID] = None
status: Optional[JobStatus] = None
progress: Optional[float] = None
current_frame: Optional[int] = None
current_time: Optional[float] = None
speed: Optional[str] = None
error_message: Optional[str] = None
celery_task_id: Optional[str] = None
execution_arn: Optional[str] = None
priority: Optional[int] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
class JobResponse(BaseSchema):
"""JobResponse schema."""
id: UUID
source_asset_id: UUID
preset_id: Optional[UUID] = None
preset_snapshot: Dict[str, Any]
trim_start: Optional[float] = None
trim_end: Optional[float] = None
output_filename: str = ""
output_path: Optional[str] = None
output_asset_id: Optional[UUID] = None
status: JobStatus = "JobStatus.PENDING"
progress: float = 0.0
current_frame: Optional[int] = None
current_time: Optional[float] = None
speed: Optional[str] = None
error_message: Optional[str] = None
celery_task_id: Optional[str] = None
execution_arn: Optional[str] = None
priority: int = 0
created_at: Optional[datetime] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None

View File

@@ -1,66 +0,0 @@
"""TranscodePreset Schemas - GENERATED FILE"""
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID
from .base import BaseSchema
class PresetCreate(BaseSchema):
"""PresetCreate schema."""
name: str
description: str = ""
is_builtin: bool = False
container: str = "mp4"
video_codec: str = "libx264"
video_bitrate: Optional[str] = None
video_crf: Optional[int] = None
video_preset: Optional[str] = None
resolution: Optional[str] = None
framerate: Optional[float] = None
audio_codec: str = "aac"
audio_bitrate: Optional[str] = None
audio_channels: Optional[int] = None
audio_samplerate: Optional[int] = None
extra_args: List[str] = Field(default_factory=list)
class PresetUpdate(BaseSchema):
"""PresetUpdate schema."""
name: Optional[str] = None
description: Optional[str] = None
is_builtin: Optional[bool] = None
container: Optional[str] = None
video_codec: Optional[str] = None
video_bitrate: Optional[str] = None
video_crf: Optional[int] = None
video_preset: Optional[str] = None
resolution: Optional[str] = None
framerate: Optional[float] = None
audio_codec: Optional[str] = None
audio_bitrate: Optional[str] = None
audio_channels: Optional[int] = None
audio_samplerate: Optional[int] = None
extra_args: Optional[List[str]] = None
class PresetResponse(BaseSchema):
"""PresetResponse schema."""
id: UUID
name: str
description: str = ""
is_builtin: bool = False
container: str = "mp4"
video_codec: str = "libx264"
video_bitrate: Optional[str] = None
video_crf: Optional[int] = None
video_preset: Optional[str] = None
resolution: Optional[str] = None
framerate: Optional[float] = None
audio_codec: str = "aac"
audio_bitrate: Optional[str] = None
audio_channels: Optional[int] = None
audio_samplerate: Optional[int] = None
extra_args: List[str] = Field(default_factory=list)
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None

View File

@@ -35,7 +35,7 @@ digraph local_architecture {
fillcolor="#f0f8e8"
django [label="Django Admin\n/admin\nport 8701"]
fastapi [label="FastAPI + GraphQL\n/api + /graphql\nport 8702"]
fastapi [label="GraphQL API\n/graphql\nport 8702"]
timeline [label="Timeline UI\n/\nport 5173"]
}
@@ -74,11 +74,11 @@ digraph local_architecture {
browser -> nginx [label="HTTP"]
nginx -> django [xlabel="/admin"]
nginx -> fastapi [xlabel="/api, /graphql"]
nginx -> fastapi [xlabel="/graphql"]
nginx -> timeline [xlabel="/"]
nginx -> minio [xlabel="/media/*"]
timeline -> fastapi [label="REST API\nGraphQL"]
timeline -> fastapi [label="GraphQL"]
django -> postgres
fastapi -> postgres [label="read/write jobs"]

View File

@@ -4,31 +4,31 @@
<!-- Generated by graphviz version 14.1.2 (0)
-->
<!-- Title: local_architecture Pages: 1 -->
<svg width="667pt" height="1108pt"
viewBox="0.00 0.00 667.00 1108.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1103.51)">
<svg width="667pt" height="1095pt"
viewBox="0.00 0.00 667.00 1095.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1090.76)">
<title>local_architecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1103.51 663,-1103.51 663,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="329.5" y="-1080.31" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR &#45; Local Architecture (Celery + MinIO)</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-1090.76 663,-1090.76 663,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="329.5" y="-1067.56" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR &#45; Local Architecture (Celery + MinIO)</text>
<g id="clust1" class="cluster">
<title>cluster_external</title>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="270,-960.41 270,-1064.01 424,-1064.01 424,-960.41 270,-960.41"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-1044.81" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="270,-947.66 270,-1051.26 424,-1051.26 424,-947.66 270,-947.66"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-1032.06" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_proxy</title>
<polygon fill="#e8f4f8" stroke="black" points="274,-832.66 274,-918.66 420,-918.66 420,-832.66 274,-832.66"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-899.46" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
<polygon fill="#e8f4f8" stroke="black" points="274,-819.91 274,-905.91 420,-905.91 420,-819.91 274,-819.91"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-886.71" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_apps</title>
<polygon fill="#f0f8e8" stroke="black" points="19,-556.16 19,-802.66 301,-802.66 301,-556.16 19,-556.16"/>
<text xml:space="preserve" text-anchor="middle" x="160" y="-783.46" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
<polygon fill="#f0f8e8" stroke="black" points="19,-556.16 19,-789.91 301,-789.91 301,-556.16 19,-556.16"/>
<text xml:space="preserve" text-anchor="middle" x="160" y="-770.71" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_workers</title>
<polygon fill="#fff8e8" stroke="black" points="188,-302.41 188,-501.66 364,-501.66 364,-302.41 188,-302.41"/>
<text xml:space="preserve" text-anchor="middle" x="276" y="-482.46" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Worker Layer</text>
<polygon fill="#fff8e8" stroke="black" points="193,-302.41 193,-501.66 369,-501.66 369,-302.41 193,-302.41"/>
<text xml:space="preserve" text-anchor="middle" x="281" y="-482.46" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Worker Layer</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_data</title>
@@ -43,68 +43,68 @@
<!-- browser -->
<g id="node1" class="node">
<title>browser</title>
<ellipse fill="none" stroke="black" cx="347" cy="-998.46" rx="69.12" ry="30.05"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-1002.41" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="347" y="-985.16" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.local.ar</text>
<ellipse fill="none" stroke="black" cx="347" cy="-985.71" rx="69.12" ry="30.05"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-989.66" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="347" y="-972.41" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.local.ar</text>
</g>
<!-- nginx -->
<g id="node2" class="node">
<title>nginx</title>
<path fill="none" stroke="black" d="M368.5,-883.16C368.5,-883.16 325.5,-883.16 325.5,-883.16 319.5,-883.16 313.5,-877.16 313.5,-871.16 313.5,-871.16 313.5,-852.66 313.5,-852.66 313.5,-846.66 319.5,-840.66 325.5,-840.66 325.5,-840.66 368.5,-840.66 368.5,-840.66 374.5,-840.66 380.5,-846.66 380.5,-852.66 380.5,-852.66 380.5,-871.16 380.5,-871.16 380.5,-877.16 374.5,-883.16 368.5,-883.16"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-865.86" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
<text xml:space="preserve" text-anchor="middle" x="347" y="-848.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
<path fill="none" stroke="black" d="M368.5,-870.41C368.5,-870.41 325.5,-870.41 325.5,-870.41 319.5,-870.41 313.5,-864.41 313.5,-858.41 313.5,-858.41 313.5,-839.91 313.5,-839.91 313.5,-833.91 319.5,-827.91 325.5,-827.91 325.5,-827.91 368.5,-827.91 368.5,-827.91 374.5,-827.91 380.5,-833.91 380.5,-839.91 380.5,-839.91 380.5,-858.41 380.5,-858.41 380.5,-864.41 374.5,-870.41 368.5,-870.41"/>
<text xml:space="preserve" text-anchor="middle" x="347" y="-853.11" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
<text xml:space="preserve" text-anchor="middle" x="347" y="-835.86" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
</g>
<!-- browser&#45;&gt;nginx -->
<g id="edge1" class="edge">
<title>browser&#45;&gt;nginx</title>
<path fill="none" stroke="black" d="M347,-968.15C347,-968.15 347,-895.16 347,-895.16"/>
<polygon fill="black" stroke="black" points="350.5,-895.16 347,-885.16 343.5,-895.16 350.5,-895.16"/>
<text xml:space="preserve" text-anchor="middle" x="359.75" y="-929.91" font-family="Helvetica,sans-Serif" font-size="10.00">HTTP</text>
<path fill="none" stroke="black" d="M347,-955.4C347,-955.4 347,-882.41 347,-882.41"/>
<polygon fill="black" stroke="black" points="350.5,-882.41 347,-872.41 343.5,-882.41 350.5,-882.41"/>
<text xml:space="preserve" text-anchor="middle" x="359.75" y="-917.16" font-family="Helvetica,sans-Serif" font-size="10.00">HTTP</text>
</g>
<!-- django -->
<g id="node3" class="node">
<title>django</title>
<path fill="none" stroke="black" d="M128.75,-767.16C128.75,-767.16 39.25,-767.16 39.25,-767.16 33.25,-767.16 27.25,-761.16 27.25,-755.16 27.25,-755.16 27.25,-719.41 27.25,-719.41 27.25,-713.41 33.25,-707.41 39.25,-707.41 39.25,-707.41 128.75,-707.41 128.75,-707.41 134.75,-707.41 140.75,-713.41 140.75,-719.41 140.75,-719.41 140.75,-755.16 140.75,-755.16 140.75,-761.16 134.75,-767.16 128.75,-767.16"/>
<text xml:space="preserve" text-anchor="middle" x="84" y="-749.86" font-family="Helvetica,sans-Serif" font-size="14.00">Django Admin</text>
<text xml:space="preserve" text-anchor="middle" x="84" y="-732.61" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
<text xml:space="preserve" text-anchor="middle" x="84" y="-715.36" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
<path fill="none" stroke="black" d="M128.75,-754.41C128.75,-754.41 39.25,-754.41 39.25,-754.41 33.25,-754.41 27.25,-748.41 27.25,-742.41 27.25,-742.41 27.25,-706.66 27.25,-706.66 27.25,-700.66 33.25,-694.66 39.25,-694.66 39.25,-694.66 128.75,-694.66 128.75,-694.66 134.75,-694.66 140.75,-700.66 140.75,-706.66 140.75,-706.66 140.75,-742.41 140.75,-742.41 140.75,-748.41 134.75,-754.41 128.75,-754.41"/>
<text xml:space="preserve" text-anchor="middle" x="84" y="-737.11" font-family="Helvetica,sans-Serif" font-size="14.00">Django Admin</text>
<text xml:space="preserve" text-anchor="middle" x="84" y="-719.86" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
<text xml:space="preserve" text-anchor="middle" x="84" y="-702.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
</g>
<!-- nginx&#45;&gt;django -->
<g id="edge2" class="edge">
<title>nginx&#45;&gt;django</title>
<path fill="none" stroke="black" d="M313.16,-869C242.12,-869 84,-869 84,-869 84,-869 84,-779.01 84,-779.01"/>
<polygon fill="black" stroke="black" points="87.5,-779.01 84,-769.01 80.5,-779.01 87.5,-779.01"/>
<text xml:space="preserve" text-anchor="middle" x="136.71" y="-872.25" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
<path fill="none" stroke="black" d="M313.16,-856C242.12,-856 84,-856 84,-856 84,-856 84,-766.21 84,-766.21"/>
<polygon fill="black" stroke="black" points="87.5,-766.21 84,-756.21 80.5,-766.21 87.5,-766.21"/>
<text xml:space="preserve" text-anchor="middle" x="136.81" y="-859.25" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
</g>
<!-- fastapi -->
<g id="node4" class="node">
<title>fastapi</title>
<path fill="none" stroke="black" d="M281.38,-623.91C281.38,-623.91 156.62,-623.91 156.62,-623.91 150.62,-623.91 144.62,-617.91 144.62,-611.91 144.62,-611.91 144.62,-576.16 144.62,-576.16 144.62,-570.16 150.62,-564.16 156.62,-564.16 156.62,-564.16 281.38,-564.16 281.38,-564.16 287.38,-564.16 293.38,-570.16 293.38,-576.16 293.38,-576.16 293.38,-611.91 293.38,-611.91 293.38,-617.91 287.38,-623.91 281.38,-623.91"/>
<text xml:space="preserve" text-anchor="middle" x="219" y="-606.61" font-family="Helvetica,sans-Serif" font-size="14.00">FastAPI + GraphQL</text>
<text xml:space="preserve" text-anchor="middle" x="219" y="-589.36" font-family="Helvetica,sans-Serif" font-size="14.00">/api + /graphql</text>
<text xml:space="preserve" text-anchor="middle" x="219" y="-572.11" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
<path fill="none" stroke="black" d="M281.25,-623.91C281.25,-623.91 200.75,-623.91 200.75,-623.91 194.75,-623.91 188.75,-617.91 188.75,-611.91 188.75,-611.91 188.75,-576.16 188.75,-576.16 188.75,-570.16 194.75,-564.16 200.75,-564.16 200.75,-564.16 281.25,-564.16 281.25,-564.16 287.25,-564.16 293.25,-570.16 293.25,-576.16 293.25,-576.16 293.25,-611.91 293.25,-611.91 293.25,-617.91 287.25,-623.91 281.25,-623.91"/>
<text xml:space="preserve" text-anchor="middle" x="241" y="-606.61" font-family="Helvetica,sans-Serif" font-size="14.00">GraphQL API</text>
<text xml:space="preserve" text-anchor="middle" x="241" y="-589.36" font-family="Helvetica,sans-Serif" font-size="14.00">/graphql</text>
<text xml:space="preserve" text-anchor="middle" x="241" y="-572.11" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
</g>
<!-- nginx&#45;&gt;fastapi -->
<g id="edge3" class="edge">
<title>nginx&#45;&gt;fastapi</title>
<path fill="none" stroke="black" d="M334.56,-840.44C334.56,-776.86 334.56,-594 334.56,-594 334.56,-594 305.33,-594 305.33,-594"/>
<polygon fill="black" stroke="black" points="305.33,-590.5 295.33,-594 305.33,-597.5 305.33,-590.5"/>
<text xml:space="preserve" text-anchor="middle" x="302.69" y="-693.1" font-family="Helvetica,sans-Serif" font-size="10.00">/api, /graphql</text>
<path fill="none" stroke="black" d="M337.06,-827.84C337.06,-766.52 337.06,-594 337.06,-594 337.06,-594 305.04,-594 305.04,-594"/>
<polygon fill="black" stroke="black" points="305.04,-590.5 295.04,-594 305.04,-597.5 305.04,-590.5"/>
<text xml:space="preserve" text-anchor="middle" x="317.19" y="-698.16" font-family="Helvetica,sans-Serif" font-size="10.00">/graphql</text>
</g>
<!-- timeline -->
<g id="node5" class="node">
<title>timeline</title>
<path fill="none" stroke="black" d="M281,-767.16C281,-767.16 211,-767.16 211,-767.16 205,-767.16 199,-761.16 199,-755.16 199,-755.16 199,-719.41 199,-719.41 199,-713.41 205,-707.41 211,-707.41 211,-707.41 281,-707.41 281,-707.41 287,-707.41 293,-713.41 293,-719.41 293,-719.41 293,-755.16 293,-755.16 293,-761.16 287,-767.16 281,-767.16"/>
<text xml:space="preserve" text-anchor="middle" x="246" y="-749.86" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
<text xml:space="preserve" text-anchor="middle" x="246" y="-732.61" font-family="Helvetica,sans-Serif" font-size="14.00">/</text>
<text xml:space="preserve" text-anchor="middle" x="246" y="-715.36" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
<path fill="none" stroke="black" d="M281,-754.41C281,-754.41 211,-754.41 211,-754.41 205,-754.41 199,-748.41 199,-742.41 199,-742.41 199,-706.66 199,-706.66 199,-700.66 205,-694.66 211,-694.66 211,-694.66 281,-694.66 281,-694.66 287,-694.66 293,-700.66 293,-706.66 293,-706.66 293,-742.41 293,-742.41 293,-748.41 287,-754.41 281,-754.41"/>
<text xml:space="preserve" text-anchor="middle" x="246" y="-737.11" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
<text xml:space="preserve" text-anchor="middle" x="246" y="-719.86" font-family="Helvetica,sans-Serif" font-size="14.00">/</text>
<text xml:space="preserve" text-anchor="middle" x="246" y="-702.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
</g>
<!-- nginx&#45;&gt;timeline -->
<g id="edge4" class="edge">
<title>nginx&#45;&gt;timeline</title>
<path fill="none" stroke="black" d="M313.34,-855C298.97,-855 285.44,-855 285.44,-855 285.44,-855 285.44,-779.11 285.44,-779.11"/>
<polygon fill="black" stroke="black" points="288.94,-779.11 285.44,-769.11 281.94,-779.11 288.94,-779.11"/>
<text xml:space="preserve" text-anchor="middle" x="283.94" y="-834.25" font-family="Helvetica,sans-Serif" font-size="10.00">/</text>
<path fill="none" stroke="black" d="M313.34,-842C298.97,-842 285.44,-842 285.44,-842 285.44,-842 285.44,-766.3 285.44,-766.3"/>
<polygon fill="black" stroke="black" points="288.94,-766.3 285.44,-756.3 281.94,-766.3 288.94,-766.3"/>
<text xml:space="preserve" text-anchor="middle" x="283.94" y="-821.35" font-family="Helvetica,sans-Serif" font-size="10.00">/</text>
</g>
<!-- minio -->
<g id="node10" class="node">
@@ -117,9 +117,9 @@
<!-- nginx&#45;&gt;minio -->
<g id="edge5" class="edge">
<title>nginx&#45;&gt;minio</title>
<path fill="none" stroke="black" d="M368.06,-840.47C368.06,-840.47 368.06,-200.34 368.06,-200.34"/>
<polygon fill="black" stroke="black" points="371.56,-200.34 368.06,-190.34 364.56,-200.34 371.56,-200.34"/>
<text xml:space="preserve" text-anchor="middle" x="347.06" y="-523.66" font-family="Helvetica,sans-Serif" font-size="10.00">/media/*</text>
<path fill="none" stroke="black" d="M370.56,-827.73C370.56,-827.73 370.56,-200.13 370.56,-200.13"/>
<polygon fill="black" stroke="black" points="374.06,-200.13 370.56,-190.13 367.06,-200.13 374.06,-200.13"/>
<text xml:space="preserve" text-anchor="middle" x="391.56" y="-517.18" font-family="Helvetica,sans-Serif" font-size="10.00">/media/*</text>
</g>
<!-- postgres -->
<g id="node8" class="node">
@@ -132,58 +132,57 @@
<!-- django&#45;&gt;postgres -->
<g id="edge7" class="edge">
<title>django&#45;&gt;postgres</title>
<path fill="none" stroke="black" d="M55.42,-706.98C55.42,-706.98 55.42,-199.74 55.42,-199.74"/>
<polygon fill="black" stroke="black" points="58.92,-199.74 55.42,-189.74 51.92,-199.74 58.92,-199.74"/>
<path fill="none" stroke="black" d="M48.38,-694.5C48.38,-694.5 48.38,-199.71 48.38,-199.71"/>
<polygon fill="black" stroke="black" points="51.88,-199.71 48.38,-189.71 44.88,-199.71 51.88,-199.71"/>
</g>
<!-- grpc_server -->
<g id="node6" class="node">
<title>grpc_server</title>
<path fill="none" stroke="black" d="M296.5,-466.16C296.5,-466.16 217.5,-466.16 217.5,-466.16 211.5,-466.16 205.5,-460.16 205.5,-454.16 205.5,-454.16 205.5,-435.66 205.5,-435.66 205.5,-429.66 211.5,-423.66 217.5,-423.66 217.5,-423.66 296.5,-423.66 296.5,-423.66 302.5,-423.66 308.5,-429.66 308.5,-435.66 308.5,-435.66 308.5,-454.16 308.5,-454.16 308.5,-460.16 302.5,-466.16 296.5,-466.16"/>
<text xml:space="preserve" text-anchor="middle" x="257" y="-448.86" font-family="Helvetica,sans-Serif" font-size="14.00">gRPC Server</text>
<text xml:space="preserve" text-anchor="middle" x="257" y="-431.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 50051</text>
<path fill="none" stroke="black" d="M301.5,-466.16C301.5,-466.16 222.5,-466.16 222.5,-466.16 216.5,-466.16 210.5,-460.16 210.5,-454.16 210.5,-454.16 210.5,-435.66 210.5,-435.66 210.5,-429.66 216.5,-423.66 222.5,-423.66 222.5,-423.66 301.5,-423.66 301.5,-423.66 307.5,-423.66 313.5,-429.66 313.5,-435.66 313.5,-435.66 313.5,-454.16 313.5,-454.16 313.5,-460.16 307.5,-466.16 301.5,-466.16"/>
<text xml:space="preserve" text-anchor="middle" x="262" y="-448.86" font-family="Helvetica,sans-Serif" font-size="14.00">gRPC Server</text>
<text xml:space="preserve" text-anchor="middle" x="262" y="-431.61" font-family="Helvetica,sans-Serif" font-size="14.00">port 50051</text>
</g>
<!-- fastapi&#45;&gt;grpc_server -->
<g id="edge9" class="edge">
<title>fastapi&#45;&gt;grpc_server</title>
<path fill="none" stroke="black" d="M249.44,-563.85C249.44,-563.85 249.44,-477.88 249.44,-477.88"/>
<polygon fill="black" stroke="black" points="252.94,-477.88 249.44,-467.88 245.94,-477.88 252.94,-477.88"/>
<text xml:space="preserve" text-anchor="middle" x="276" y="-525.66" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="276" y="-512.91" font-family="Helvetica,sans-Serif" font-size="10.00">progress updates</text>
<path fill="none" stroke="black" d="M251.88,-563.85C251.88,-563.85 251.88,-477.88 251.88,-477.88"/>
<polygon fill="black" stroke="black" points="255.38,-477.88 251.88,-467.88 248.38,-477.88 255.38,-477.88"/>
<text xml:space="preserve" text-anchor="middle" x="292" y="-525.66" font-family="Helvetica,sans-Serif" font-size="10.00">gRPC</text>
<text xml:space="preserve" text-anchor="middle" x="292" y="-512.91" font-family="Helvetica,sans-Serif" font-size="10.00">progress updates</text>
</g>
<!-- fastapi&#45;&gt;postgres -->
<g id="edge8" class="edge">
<title>fastapi&#45;&gt;postgres</title>
<path fill="none" stroke="black" d="M157.25,-563.79C157.25,-465.13 157.25,-159 157.25,-159 157.25,-159 123.5,-159 123.5,-159"/>
<polygon fill="black" stroke="black" points="123.5,-155.5 113.5,-159 123.5,-162.5 123.5,-155.5"/>
<path fill="none" stroke="black" d="M188.61,-594C138.18,-594 69.5,-594 69.5,-594 69.5,-594 69.5,-199.68 69.5,-199.68"/>
<polygon fill="black" stroke="black" points="73,-199.68 69.5,-189.68 66,-199.68 73,-199.68"/>
<text xml:space="preserve" text-anchor="middle" x="82.38" y="-385.16" font-family="Helvetica,sans-Serif" font-size="10.00">read/write jobs</text>
</g>
<!-- timeline&#45;&gt;fastapi -->
<g id="edge6" class="edge">
<title>timeline&#45;&gt;fastapi</title>
<path fill="none" stroke="black" d="M246,-707.25C246,-707.25 246,-635.54 246,-635.54"/>
<polygon fill="black" stroke="black" points="249.5,-635.54 246,-625.54 242.5,-635.54 249.5,-635.54"/>
<text xml:space="preserve" text-anchor="middle" x="253.75" y="-668.91" font-family="Helvetica,sans-Serif" font-size="10.00">REST API</text>
<text xml:space="preserve" text-anchor="middle" x="253.75" y="-656.16" font-family="Helvetica,sans-Serif" font-size="10.00">GraphQL</text>
<path fill="none" stroke="black" d="M246,-694.26C246,-694.26 246,-635.65 246,-635.65"/>
<polygon fill="black" stroke="black" points="249.5,-635.65 246,-625.65 242.5,-635.65 249.5,-635.65"/>
<text xml:space="preserve" text-anchor="middle" x="264" y="-656.16" font-family="Helvetica,sans-Serif" font-size="10.00">GraphQL</text>
</g>
<!-- celery -->
<g id="node7" class="node">
<title>celery</title>
<path fill="none" stroke="black" d="M343.62,-352.91C343.62,-352.91 208.38,-352.91 208.38,-352.91 202.38,-352.91 196.38,-346.91 196.38,-340.91 196.38,-340.91 196.38,-322.41 196.38,-322.41 196.38,-316.41 202.38,-310.41 208.38,-310.41 208.38,-310.41 343.62,-310.41 343.62,-310.41 349.62,-310.41 355.62,-316.41 355.62,-322.41 355.62,-322.41 355.62,-340.91 355.62,-340.91 355.62,-346.91 349.62,-352.91 343.62,-352.91"/>
<text xml:space="preserve" text-anchor="middle" x="276" y="-335.61" font-family="Helvetica,sans-Serif" font-size="14.00">Celery Worker</text>
<text xml:space="preserve" text-anchor="middle" x="276" y="-318.36" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg transcoding</text>
<path fill="none" stroke="black" d="M348.62,-352.91C348.62,-352.91 213.38,-352.91 213.38,-352.91 207.38,-352.91 201.38,-346.91 201.38,-340.91 201.38,-340.91 201.38,-322.41 201.38,-322.41 201.38,-316.41 207.38,-310.41 213.38,-310.41 213.38,-310.41 348.62,-310.41 348.62,-310.41 354.62,-310.41 360.62,-316.41 360.62,-322.41 360.62,-322.41 360.62,-340.91 360.62,-340.91 360.62,-346.91 354.62,-352.91 348.62,-352.91"/>
<text xml:space="preserve" text-anchor="middle" x="281" y="-335.61" font-family="Helvetica,sans-Serif" font-size="14.00">Celery Worker</text>
<text xml:space="preserve" text-anchor="middle" x="281" y="-318.36" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg transcoding</text>
</g>
<!-- grpc_server&#45;&gt;celery -->
<g id="edge10" class="edge">
<title>grpc_server&#45;&gt;celery</title>
<path fill="none" stroke="black" d="M257,-423.34C257,-423.34 257,-364.66 257,-364.66"/>
<polygon fill="black" stroke="black" points="260.5,-364.66 257,-354.66 253.5,-364.66 260.5,-364.66"/>
<text xml:space="preserve" text-anchor="middle" x="300.25" y="-385.16" font-family="Helvetica,sans-Serif" font-size="10.00">dispatch tasks</text>
<path fill="none" stroke="black" d="M262,-423.34C262,-423.34 262,-364.66 262,-364.66"/>
<polygon fill="black" stroke="black" points="265.5,-364.66 262,-354.66 258.5,-364.66 265.5,-364.66"/>
<text xml:space="preserve" text-anchor="middle" x="305.25" y="-385.16" font-family="Helvetica,sans-Serif" font-size="10.00">dispatch tasks</text>
</g>
<!-- celery&#45;&gt;postgres -->
<g id="edge12" class="edge">
<title>celery&#45;&gt;postgres</title>
<path fill="none" stroke="black" d="M196.07,-332C143.2,-332 83.58,-332 83.58,-332 83.58,-332 83.58,-199.51 83.58,-199.51"/>
<polygon fill="black" stroke="black" points="87.08,-199.51 83.58,-189.51 80.08,-199.51 87.08,-199.51"/>
<path fill="none" stroke="black" d="M201.09,-332C148.99,-332 90.62,-332 90.62,-332 90.62,-332 90.62,-199.51 90.62,-199.51"/>
<polygon fill="black" stroke="black" points="94.13,-199.51 90.63,-189.51 87.13,-199.51 94.13,-199.51"/>
<text xml:space="preserve" text-anchor="middle" x="181.38" y="-259.16" font-family="Helvetica,sans-Serif" font-size="10.00">update job status</text>
</g>
<!-- redis -->
@@ -198,15 +197,15 @@
<!-- celery&#45;&gt;redis -->
<g id="edge11" class="edge">
<title>celery&#45;&gt;redis</title>
<path fill="none" stroke="black" d="M237.25,-310.09C237.25,-310.09 237.25,-211.49 237.25,-211.49"/>
<polygon fill="black" stroke="black" points="240.75,-211.49 237.25,-201.49 233.75,-211.49 240.75,-211.49"/>
<path fill="none" stroke="black" d="M239.75,-310.09C239.75,-310.09 239.75,-211.49 239.75,-211.49"/>
<polygon fill="black" stroke="black" points="243.25,-211.49 239.75,-201.49 236.25,-211.49 243.25,-211.49"/>
<text xml:space="preserve" text-anchor="middle" x="314" y="-259.16" font-family="Helvetica,sans-Serif" font-size="10.00">task queue</text>
</g>
<!-- celery&#45;&gt;minio -->
<g id="edge13" class="edge">
<title>celery&#45;&gt;minio</title>
<path fill="none" stroke="black" d="M349.62,-310.09C349.62,-310.09 349.62,-200.39 349.62,-200.39"/>
<polygon fill="black" stroke="black" points="353.13,-200.39 349.63,-190.39 346.13,-200.39 353.13,-200.39"/>
<path fill="none" stroke="black" d="M352.12,-310.09C352.12,-310.09 352.12,-200.39 352.12,-200.39"/>
<polygon fill="black" stroke="black" points="355.63,-200.39 352.13,-190.39 348.63,-200.39 355.63,-200.39"/>
<text xml:space="preserve" text-anchor="middle" x="441.5" y="-271.91" font-family="Helvetica,sans-Serif" font-size="10.00">S3 API</text>
<text xml:space="preserve" text-anchor="middle" x="441.5" y="-259.16" font-family="Helvetica,sans-Serif" font-size="10.00">download input</text>
<text xml:space="preserve" text-anchor="middle" x="441.5" y="-246.41" font-family="Helvetica,sans-Serif" font-size="10.00">upload output</text>

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -35,7 +35,7 @@ digraph aws_architecture {
fillcolor="#f0f8e8"
django [label="Django Admin\n/admin\nport 8701"]
fastapi [label="FastAPI + GraphQL\n/api + /graphql\nport 8702"]
fastapi [label="GraphQL API\n/graphql\nport 8702"]
timeline [label="Timeline UI\n/\nport 5173"]
}
@@ -65,10 +65,10 @@ digraph aws_architecture {
browser -> nginx [label="HTTP"]
nginx -> django [xlabel="/admin"]
nginx -> fastapi [xlabel="/api, /graphql"]
nginx -> fastapi [xlabel="/graphql"]
nginx -> timeline [xlabel="/"]
timeline -> fastapi [label="REST API\nGraphQL"]
timeline -> fastapi [label="GraphQL"]
django -> postgres
fastapi -> postgres [label="read/write jobs"]

View File

@@ -4,222 +4,221 @@
<!-- Generated by graphviz version 14.1.2 (0)
-->
<!-- Title: aws_architecture Pages: 1 -->
<svg width="620pt" height="1094pt"
viewBox="0.00 0.00 620.00 1094.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1090.1)">
<svg width="639pt" height="1081pt"
viewBox="0.00 0.00 639.00 1081.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1077.35)">
<title>aws_architecture</title>
<polygon fill="white" stroke="none" points="-4,4 -4,-1090.1 616.25,-1090.1 616.25,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="306.12" y="-1066.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR &#45; AWS Architecture (Lambda + Step Functions)</text>
<polygon fill="white" stroke="none" points="-4,4 -4,-1077.35 635.25,-1077.35 635.25,4 -4,4"/>
<text xml:space="preserve" text-anchor="middle" x="315.62" y="-1054.15" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">MPR &#45; AWS Architecture (Lambda + Step Functions)</text>
<g id="clust1" class="cluster">
<title>cluster_external</title>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="158,-947 158,-1050.6 318,-1050.6 318,-947 158,-947"/>
<text xml:space="preserve" text-anchor="middle" x="238" y="-1031.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
<polygon fill="none" stroke="gray" stroke-dasharray="5,2" points="155,-934.25 155,-1037.85 315,-1037.85 315,-934.25 155,-934.25"/>
<text xml:space="preserve" text-anchor="middle" x="235" y="-1018.65" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">External</text>
</g>
<g id="clust2" class="cluster">
<title>cluster_proxy</title>
<polygon fill="#e8f4f8" stroke="black" points="165,-819.25 165,-905.25 311,-905.25 311,-819.25 165,-819.25"/>
<text xml:space="preserve" text-anchor="middle" x="238" y="-886.05" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
<polygon fill="#e8f4f8" stroke="black" points="162,-806.5 162,-892.5 308,-892.5 308,-806.5 162,-806.5"/>
<text xml:space="preserve" text-anchor="middle" x="235" y="-873.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Reverse Proxy</text>
</g>
<g id="clust3" class="cluster">
<title>cluster_apps</title>
<polygon fill="#f0f8e8" stroke="black" points="11,-542.75 11,-789.25 293,-789.25 293,-542.75 11,-542.75"/>
<text xml:space="preserve" text-anchor="middle" x="152" y="-770.05" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
<polygon fill="#f0f8e8" stroke="black" points="8,-542.75 8,-776.5 290,-776.5 290,-542.75 8,-542.75"/>
<text xml:space="preserve" text-anchor="middle" x="149" y="-757.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Application Layer</text>
</g>
<g id="clust4" class="cluster">
<title>cluster_data</title>
<polygon fill="#f8e8f0" stroke="black" points="8,-372.91 8,-474.84 122,-474.84 122,-372.91 8,-372.91"/>
<text xml:space="preserve" text-anchor="middle" x="65" y="-455.64" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Data Layer</text>
<polygon fill="#f8e8f0" stroke="black" points="27,-372.91 27,-474.84 141,-474.84 141,-372.91 27,-372.91"/>
<text xml:space="preserve" text-anchor="middle" x="84" y="-455.64" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">Data Layer</text>
</g>
<g id="clust5" class="cluster">
<title>cluster_aws</title>
<polygon fill="#fde8d0" stroke="black" points="245,-8 245,-475.5 577,-475.5 577,-8 245,-8"/>
<text xml:space="preserve" text-anchor="middle" x="411" y="-456.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AWS Cloud</text>
<polygon fill="#fde8d0" stroke="black" points="264,-8 264,-475.5 596,-475.5 596,-8 264,-8"/>
<text xml:space="preserve" text-anchor="middle" x="430" y="-456.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="16.00">AWS Cloud</text>
</g>
<!-- browser -->
<g id="node1" class="node">
<title>browser</title>
<ellipse fill="none" stroke="black" cx="238" cy="-985.05" rx="71.77" ry="30.05"/>
<text xml:space="preserve" text-anchor="middle" x="238" y="-989" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="238" y="-971.75" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.mcrn.ar</text>
<ellipse fill="none" stroke="black" cx="235" cy="-972.3" rx="71.77" ry="30.05"/>
<text xml:space="preserve" text-anchor="middle" x="235" y="-976.25" font-family="Helvetica,sans-Serif" font-size="14.00">Browser</text>
<text xml:space="preserve" text-anchor="middle" x="235" y="-959" font-family="Helvetica,sans-Serif" font-size="14.00">mpr.mcrn.ar</text>
</g>
<!-- nginx -->
<g id="node2" class="node">
<title>nginx</title>
<path fill="none" stroke="black" d="M259.5,-869.75C259.5,-869.75 216.5,-869.75 216.5,-869.75 210.5,-869.75 204.5,-863.75 204.5,-857.75 204.5,-857.75 204.5,-839.25 204.5,-839.25 204.5,-833.25 210.5,-827.25 216.5,-827.25 216.5,-827.25 259.5,-827.25 259.5,-827.25 265.5,-827.25 271.5,-833.25 271.5,-839.25 271.5,-839.25 271.5,-857.75 271.5,-857.75 271.5,-863.75 265.5,-869.75 259.5,-869.75"/>
<text xml:space="preserve" text-anchor="middle" x="238" y="-852.45" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
<text xml:space="preserve" text-anchor="middle" x="238" y="-835.2" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
<path fill="none" stroke="black" d="M256.5,-857C256.5,-857 213.5,-857 213.5,-857 207.5,-857 201.5,-851 201.5,-845 201.5,-845 201.5,-826.5 201.5,-826.5 201.5,-820.5 207.5,-814.5 213.5,-814.5 213.5,-814.5 256.5,-814.5 256.5,-814.5 262.5,-814.5 268.5,-820.5 268.5,-826.5 268.5,-826.5 268.5,-845 268.5,-845 268.5,-851 262.5,-857 256.5,-857"/>
<text xml:space="preserve" text-anchor="middle" x="235" y="-839.7" font-family="Helvetica,sans-Serif" font-size="14.00">nginx</text>
<text xml:space="preserve" text-anchor="middle" x="235" y="-822.45" font-family="Helvetica,sans-Serif" font-size="14.00">port 80</text>
</g>
<!-- browser&#45;&gt;nginx -->
<g id="edge1" class="edge">
<title>browser&#45;&gt;nginx</title>
<path fill="none" stroke="black" d="M238,-954.75C238,-954.75 238,-881.75 238,-881.75"/>
<polygon fill="black" stroke="black" points="241.5,-881.75 238,-871.75 234.5,-881.75 241.5,-881.75"/>
<text xml:space="preserve" text-anchor="middle" x="250.75" y="-916.5" font-family="Helvetica,sans-Serif" font-size="10.00">HTTP</text>
<path fill="none" stroke="black" d="M235,-942C235,-942 235,-869 235,-869"/>
<polygon fill="black" stroke="black" points="238.5,-869 235,-859 231.5,-869 238.5,-869"/>
<text xml:space="preserve" text-anchor="middle" x="247.75" y="-903.75" font-family="Helvetica,sans-Serif" font-size="10.00">HTTP</text>
</g>
<!-- django -->
<g id="node3" class="node">
<title>django</title>
<path fill="none" stroke="black" d="M120.75,-753.75C120.75,-753.75 31.25,-753.75 31.25,-753.75 25.25,-753.75 19.25,-747.75 19.25,-741.75 19.25,-741.75 19.25,-706 19.25,-706 19.25,-700 25.25,-694 31.25,-694 31.25,-694 120.75,-694 120.75,-694 126.75,-694 132.75,-700 132.75,-706 132.75,-706 132.75,-741.75 132.75,-741.75 132.75,-747.75 126.75,-753.75 120.75,-753.75"/>
<text xml:space="preserve" text-anchor="middle" x="76" y="-736.45" font-family="Helvetica,sans-Serif" font-size="14.00">Django Admin</text>
<text xml:space="preserve" text-anchor="middle" x="76" y="-719.2" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
<text xml:space="preserve" text-anchor="middle" x="76" y="-701.95" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
<path fill="none" stroke="black" d="M117.75,-741C117.75,-741 28.25,-741 28.25,-741 22.25,-741 16.25,-735 16.25,-729 16.25,-729 16.25,-693.25 16.25,-693.25 16.25,-687.25 22.25,-681.25 28.25,-681.25 28.25,-681.25 117.75,-681.25 117.75,-681.25 123.75,-681.25 129.75,-687.25 129.75,-693.25 129.75,-693.25 129.75,-729 129.75,-729 129.75,-735 123.75,-741 117.75,-741"/>
<text xml:space="preserve" text-anchor="middle" x="73" y="-723.7" font-family="Helvetica,sans-Serif" font-size="14.00">Django Admin</text>
<text xml:space="preserve" text-anchor="middle" x="73" y="-706.45" font-family="Helvetica,sans-Serif" font-size="14.00">/admin</text>
<text xml:space="preserve" text-anchor="middle" x="73" y="-689.2" font-family="Helvetica,sans-Serif" font-size="14.00">port 8701</text>
</g>
<!-- nginx&#45;&gt;django -->
<g id="edge2" class="edge">
<title>nginx&#45;&gt;django</title>
<path fill="none" stroke="black" d="M204.04,-856C156.54,-856 76,-856 76,-856 76,-856 76,-765.7 76,-765.7"/>
<polygon fill="black" stroke="black" points="79.5,-765.7 76,-755.7 72.5,-765.7 79.5,-765.7"/>
<text xml:space="preserve" text-anchor="middle" x="78" y="-859.25" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
<path fill="none" stroke="black" d="M201.04,-843C153.54,-843 73,-843 73,-843 73,-843 73,-752.89 73,-752.89"/>
<polygon fill="black" stroke="black" points="76.5,-752.89 73,-742.89 69.5,-752.89 76.5,-752.89"/>
<text xml:space="preserve" text-anchor="middle" x="75.09" y="-846.25" font-family="Helvetica,sans-Serif" font-size="10.00">/admin</text>
</g>
<!-- fastapi -->
<g id="node4" class="node">
<title>fastapi</title>
<path fill="none" stroke="black" d="M273.38,-610.5C273.38,-610.5 148.62,-610.5 148.62,-610.5 142.62,-610.5 136.62,-604.5 136.62,-598.5 136.62,-598.5 136.62,-562.75 136.62,-562.75 136.62,-556.75 142.62,-550.75 148.62,-550.75 148.62,-550.75 273.38,-550.75 273.38,-550.75 279.38,-550.75 285.38,-556.75 285.38,-562.75 285.38,-562.75 285.38,-598.5 285.38,-598.5 285.38,-604.5 279.38,-610.5 273.38,-610.5"/>
<text xml:space="preserve" text-anchor="middle" x="211" y="-593.2" font-family="Helvetica,sans-Serif" font-size="14.00">FastAPI + GraphQL</text>
<text xml:space="preserve" text-anchor="middle" x="211" y="-575.95" font-family="Helvetica,sans-Serif" font-size="14.00">/api + /graphql</text>
<text xml:space="preserve" text-anchor="middle" x="211" y="-558.7" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
<path fill="none" stroke="black" d="M270.25,-610.5C270.25,-610.5 189.75,-610.5 189.75,-610.5 183.75,-610.5 177.75,-604.5 177.75,-598.5 177.75,-598.5 177.75,-562.75 177.75,-562.75 177.75,-556.75 183.75,-550.75 189.75,-550.75 189.75,-550.75 270.25,-550.75 270.25,-550.75 276.25,-550.75 282.25,-556.75 282.25,-562.75 282.25,-562.75 282.25,-598.5 282.25,-598.5 282.25,-604.5 276.25,-610.5 270.25,-610.5"/>
<text xml:space="preserve" text-anchor="middle" x="230" y="-593.2" font-family="Helvetica,sans-Serif" font-size="14.00">GraphQL API</text>
<text xml:space="preserve" text-anchor="middle" x="230" y="-575.95" font-family="Helvetica,sans-Serif" font-size="14.00">/graphql</text>
<text xml:space="preserve" text-anchor="middle" x="230" y="-558.7" font-family="Helvetica,sans-Serif" font-size="14.00">port 8702</text>
</g>
<!-- nginx&#45;&gt;fastapi -->
<g id="edge3" class="edge">
<title>nginx&#45;&gt;fastapi</title>
<path fill="none" stroke="black" d="M204.14,-841C190.81,-841 178.61,-841 178.61,-841 178.61,-841 178.61,-622.26 178.61,-622.26"/>
<polygon fill="black" stroke="black" points="182.11,-622.26 178.61,-612.26 175.11,-622.26 182.11,-622.26"/>
<text xml:space="preserve" text-anchor="middle" x="162.68" y="-734.9" font-family="Helvetica,sans-Serif" font-size="10.00">/api, /graphql</text>
<path fill="none" stroke="black" d="M201.11,-829C191.15,-829 182.88,-829 182.88,-829 182.88,-829 182.88,-622.1 182.88,-622.1"/>
<polygon fill="black" stroke="black" points="186.38,-622.1 182.88,-612.1 179.38,-622.1 186.38,-622.1"/>
<text xml:space="preserve" text-anchor="middle" x="163" y="-737.91" font-family="Helvetica,sans-Serif" font-size="10.00">/graphql</text>
</g>
<!-- timeline -->
<g id="node5" class="node">
<title>timeline</title>
<path fill="none" stroke="black" d="M273,-753.75C273,-753.75 203,-753.75 203,-753.75 197,-753.75 191,-747.75 191,-741.75 191,-741.75 191,-706 191,-706 191,-700 197,-694 203,-694 203,-694 273,-694 273,-694 279,-694 285,-700 285,-706 285,-706 285,-741.75 285,-741.75 285,-747.75 279,-753.75 273,-753.75"/>
<text xml:space="preserve" text-anchor="middle" x="238" y="-736.45" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
<text xml:space="preserve" text-anchor="middle" x="238" y="-719.2" font-family="Helvetica,sans-Serif" font-size="14.00">/</text>
<text xml:space="preserve" text-anchor="middle" x="238" y="-701.95" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
<path fill="none" stroke="black" d="M270,-741C270,-741 200,-741 200,-741 194,-741 188,-735 188,-729 188,-729 188,-693.25 188,-693.25 188,-687.25 194,-681.25 200,-681.25 200,-681.25 270,-681.25 270,-681.25 276,-681.25 282,-687.25 282,-693.25 282,-693.25 282,-729 282,-729 282,-735 276,-741 270,-741"/>
<text xml:space="preserve" text-anchor="middle" x="235" y="-723.7" font-family="Helvetica,sans-Serif" font-size="14.00">Timeline UI</text>
<text xml:space="preserve" text-anchor="middle" x="235" y="-706.45" font-family="Helvetica,sans-Serif" font-size="14.00">/</text>
<text xml:space="preserve" text-anchor="middle" x="235" y="-689.2" font-family="Helvetica,sans-Serif" font-size="14.00">port 5173</text>
</g>
<!-- nginx&#45;&gt;timeline -->
<g id="edge4" class="edge">
<title>nginx&#45;&gt;timeline</title>
<path fill="none" stroke="black" d="M238,-826.79C238,-826.79 238,-765.72 238,-765.72"/>
<polygon fill="black" stroke="black" points="241.5,-765.72 238,-755.72 234.5,-765.72 241.5,-765.72"/>
<text xml:space="preserve" text-anchor="middle" x="236.5" y="-799.5" font-family="Helvetica,sans-Serif" font-size="10.00">/</text>
<path fill="none" stroke="black" d="M235,-814.04C235,-814.04 235,-752.97 235,-752.97"/>
<polygon fill="black" stroke="black" points="238.5,-752.97 235,-742.97 231.5,-752.97 238.5,-752.97"/>
<text xml:space="preserve" text-anchor="middle" x="233.5" y="-786.75" font-family="Helvetica,sans-Serif" font-size="10.00">/</text>
</g>
<!-- postgres -->
<g id="node6" class="node">
<title>postgres</title>
<path fill="none" stroke="black" d="M112.75,-434.03C112.75,-436.96 91.35,-439.34 65,-439.34 38.65,-439.34 17.25,-436.96 17.25,-434.03 17.25,-434.03 17.25,-386.22 17.25,-386.22 17.25,-383.29 38.65,-380.91 65,-380.91 91.35,-380.91 112.75,-383.29 112.75,-386.22 112.75,-386.22 112.75,-434.03 112.75,-434.03"/>
<path fill="none" stroke="black" d="M112.75,-434.03C112.75,-431.1 91.35,-428.72 65,-428.72 38.65,-428.72 17.25,-431.1 17.25,-434.03"/>
<text xml:space="preserve" text-anchor="middle" x="65" y="-414.07" font-family="Helvetica,sans-Serif" font-size="14.00">PostgreSQL</text>
<text xml:space="preserve" text-anchor="middle" x="65" y="-396.82" font-family="Helvetica,sans-Serif" font-size="14.00">port 5436</text>
<path fill="none" stroke="black" d="M131.75,-434.03C131.75,-436.96 110.35,-439.34 84,-439.34 57.65,-439.34 36.25,-436.96 36.25,-434.03 36.25,-434.03 36.25,-386.22 36.25,-386.22 36.25,-383.29 57.65,-380.91 84,-380.91 110.35,-380.91 131.75,-383.29 131.75,-386.22 131.75,-386.22 131.75,-434.03 131.75,-434.03"/>
<path fill="none" stroke="black" d="M131.75,-434.03C131.75,-431.1 110.35,-428.72 84,-428.72 57.65,-428.72 36.25,-431.1 36.25,-434.03"/>
<text xml:space="preserve" text-anchor="middle" x="84" y="-414.07" font-family="Helvetica,sans-Serif" font-size="14.00">PostgreSQL</text>
<text xml:space="preserve" text-anchor="middle" x="84" y="-396.82" font-family="Helvetica,sans-Serif" font-size="14.00">port 5436</text>
</g>
<!-- django&#45;&gt;postgres -->
<g id="edge6" class="edge">
<title>django&#45;&gt;postgres</title>
<path fill="none" stroke="black" d="M42.62,-693.51C42.62,-693.51 42.62,-451.11 42.62,-451.11"/>
<polygon fill="black" stroke="black" points="46.13,-451.11 42.63,-441.11 39.13,-451.11 46.13,-451.11"/>
<path fill="none" stroke="black" d="M83,-680.89C83,-680.89 83,-450.97 83,-450.97"/>
<polygon fill="black" stroke="black" points="86.5,-450.97 83,-440.97 79.5,-450.97 86.5,-450.97"/>
</g>
<!-- fastapi&#45;&gt;postgres -->
<g id="edge7" class="edge">
<title>fastapi&#45;&gt;postgres</title>
<path fill="none" stroke="black" d="M136.38,-591C100.85,-591 66,-591 66,-591 66,-591 66,-451.1 66,-451.1"/>
<polygon fill="black" stroke="black" points="69.5,-451.1 66,-441.1 62.5,-451.1 69.5,-451.1"/>
<text xml:space="preserve" text-anchor="middle" x="247.38" y="-499.5" font-family="Helvetica,sans-Serif" font-size="10.00">read/write jobs</text>
<path fill="none" stroke="black" d="M201.38,-550.41C201.38,-503.88 201.38,-420 201.38,-420 201.38,-420 143.59,-420 143.59,-420"/>
<polygon fill="black" stroke="black" points="143.59,-416.5 133.59,-420 143.59,-423.5 143.59,-416.5"/>
<text xml:space="preserve" text-anchor="middle" x="266.38" y="-499.5" font-family="Helvetica,sans-Serif" font-size="10.00">read/write jobs</text>
</g>
<!-- fastapi&#45;&gt;postgres -->
<g id="edge12" class="edge">
<title>fastapi&#45;&gt;postgres</title>
<path fill="none" stroke="black" d="M136.4,-571C111.22,-571 89.38,-571 89.38,-571 89.38,-571 89.38,-451.15 89.38,-451.15"/>
<polygon fill="black" stroke="black" points="92.88,-451.15 89.38,-441.15 85.88,-451.15 92.88,-451.15"/>
<text xml:space="preserve" text-anchor="middle" x="106.25" y="-505.88" font-family="Helvetica,sans-Serif" font-size="10.00">callback updates</text>
<text xml:space="preserve" text-anchor="middle" x="106.25" y="-493.12" font-family="Helvetica,sans-Serif" font-size="10.00">job status</text>
<path fill="none" stroke="black" d="M225,-550.39C225,-498.97 225,-400 225,-400 225,-400 143.64,-400 143.64,-400"/>
<polygon fill="black" stroke="black" points="143.64,-396.5 133.64,-400 143.64,-403.5 143.64,-396.5"/>
<text xml:space="preserve" text-anchor="middle" x="125.25" y="-505.88" font-family="Helvetica,sans-Serif" font-size="10.00">callback updates</text>
<text xml:space="preserve" text-anchor="middle" x="125.25" y="-493.12" font-family="Helvetica,sans-Serif" font-size="10.00">job status</text>
</g>
<!-- step_functions -->
<g id="node7" class="node">
<title>step_functions</title>
<path fill="none" stroke="black" d="M365.38,-440C365.38,-440 270.62,-440 270.62,-440 264.62,-440 258.62,-434 258.62,-428 258.62,-428 258.62,-392.25 258.62,-392.25 258.62,-386.25 264.62,-380.25 270.62,-380.25 270.62,-380.25 365.38,-380.25 365.38,-380.25 371.38,-380.25 377.38,-386.25 377.38,-392.25 377.38,-392.25 377.38,-428 377.38,-428 377.38,-434 371.38,-440 365.38,-440"/>
<text xml:space="preserve" text-anchor="middle" x="318" y="-422.7" font-family="Helvetica,sans-Serif" font-size="14.00">Step Functions</text>
<text xml:space="preserve" text-anchor="middle" x="318" y="-405.45" font-family="Helvetica,sans-Serif" font-size="14.00">Orchestration</text>
<text xml:space="preserve" text-anchor="middle" x="318" y="-388.2" font-family="Helvetica,sans-Serif" font-size="14.00">state machine</text>
<path fill="none" stroke="black" d="M384.38,-440C384.38,-440 289.62,-440 289.62,-440 283.62,-440 277.62,-434 277.62,-428 277.62,-428 277.62,-392.25 277.62,-392.25 277.62,-386.25 283.62,-380.25 289.62,-380.25 289.62,-380.25 384.38,-380.25 384.38,-380.25 390.38,-380.25 396.38,-386.25 396.38,-392.25 396.38,-392.25 396.38,-428 396.38,-428 396.38,-434 390.38,-440 384.38,-440"/>
<text xml:space="preserve" text-anchor="middle" x="337" y="-422.7" font-family="Helvetica,sans-Serif" font-size="14.00">Step Functions</text>
<text xml:space="preserve" text-anchor="middle" x="337" y="-405.45" font-family="Helvetica,sans-Serif" font-size="14.00">Orchestration</text>
<text xml:space="preserve" text-anchor="middle" x="337" y="-388.2" font-family="Helvetica,sans-Serif" font-size="14.00">state machine</text>
</g>
<!-- fastapi&#45;&gt;step_functions -->
<g id="edge8" class="edge">
<title>fastapi&#45;&gt;step_functions</title>
<path fill="none" stroke="black" d="M272,-550.54C272,-550.54 272,-452 272,-452"/>
<polygon fill="black" stroke="black" points="275.5,-452 272,-442 268.5,-452 275.5,-452"/>
<text xml:space="preserve" text-anchor="middle" x="388.25" y="-512.25" font-family="Helvetica,sans-Serif" font-size="10.00">boto3</text>
<text xml:space="preserve" text-anchor="middle" x="388.25" y="-499.5" font-family="Helvetica,sans-Serif" font-size="10.00">start_execution()</text>
<text xml:space="preserve" text-anchor="middle" x="388.25" y="-486.75" font-family="Helvetica,sans-Serif" font-size="10.00">execution_arn</text>
<path fill="none" stroke="black" d="M282.68,-581C289.69,-581 294.51,-581 294.51,-581 294.51,-581 294.51,-451.79 294.51,-451.79"/>
<polygon fill="black" stroke="black" points="298.01,-451.79 294.51,-441.79 291.01,-451.79 298.01,-451.79"/>
<text xml:space="preserve" text-anchor="middle" x="407.25" y="-512.25" font-family="Helvetica,sans-Serif" font-size="10.00">boto3</text>
<text xml:space="preserve" text-anchor="middle" x="407.25" y="-499.5" font-family="Helvetica,sans-Serif" font-size="10.00">start_execution()</text>
<text xml:space="preserve" text-anchor="middle" x="407.25" y="-486.75" font-family="Helvetica,sans-Serif" font-size="10.00">execution_arn</text>
</g>
<!-- timeline&#45;&gt;fastapi -->
<g id="edge5" class="edge">
<title>timeline&#45;&gt;fastapi</title>
<path fill="none" stroke="black" d="M238,-693.85C238,-693.85 238,-622.13 238,-622.13"/>
<polygon fill="black" stroke="black" points="241.5,-622.13 238,-612.13 234.5,-622.13 241.5,-622.13"/>
<text xml:space="preserve" text-anchor="middle" x="245.75" y="-655.5" font-family="Helvetica,sans-Serif" font-size="10.00">REST API</text>
<text xml:space="preserve" text-anchor="middle" x="245.75" y="-642.75" font-family="Helvetica,sans-Serif" font-size="10.00">GraphQL</text>
<path fill="none" stroke="black" d="M235,-680.86C235,-680.86 235,-622.24 235,-622.24"/>
<polygon fill="black" stroke="black" points="238.5,-622.24 235,-612.24 231.5,-622.24 238.5,-622.24"/>
<text xml:space="preserve" text-anchor="middle" x="253" y="-642.75" font-family="Helvetica,sans-Serif" font-size="10.00">GraphQL</text>
</g>
<!-- lambda -->
<g id="node8" class="node">
<title>lambda</title>
<path fill="none" stroke="black" d="M467,-296.75C467,-296.75 349,-296.75 349,-296.75 343,-296.75 337,-290.75 337,-284.75 337,-284.75 337,-249 337,-249 337,-243 343,-237 349,-237 349,-237 467,-237 467,-237 473,-237 479,-243 479,-249 479,-249 479,-284.75 479,-284.75 479,-290.75 473,-296.75 467,-296.75"/>
<text xml:space="preserve" text-anchor="middle" x="408" y="-279.45" font-family="Helvetica,sans-Serif" font-size="14.00">Lambda Function</text>
<text xml:space="preserve" text-anchor="middle" x="408" y="-262.2" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg container</text>
<text xml:space="preserve" text-anchor="middle" x="408" y="-244.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoding</text>
<path fill="none" stroke="black" d="M486,-296.75C486,-296.75 368,-296.75 368,-296.75 362,-296.75 356,-290.75 356,-284.75 356,-284.75 356,-249 356,-249 356,-243 362,-237 368,-237 368,-237 486,-237 486,-237 492,-237 498,-243 498,-249 498,-249 498,-284.75 498,-284.75 498,-290.75 492,-296.75 486,-296.75"/>
<text xml:space="preserve" text-anchor="middle" x="427" y="-279.45" font-family="Helvetica,sans-Serif" font-size="14.00">Lambda Function</text>
<text xml:space="preserve" text-anchor="middle" x="427" y="-262.2" font-family="Helvetica,sans-Serif" font-size="14.00">FFmpeg container</text>
<text xml:space="preserve" text-anchor="middle" x="427" y="-244.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoding</text>
</g>
<!-- step_functions&#45;&gt;lambda -->
<g id="edge9" class="edge">
<title>step_functions&#45;&gt;lambda</title>
<path fill="none" stroke="black" d="M357.19,-380.1C357.19,-380.1 357.19,-308.38 357.19,-308.38"/>
<polygon fill="black" stroke="black" points="360.69,-308.38 357.19,-298.38 353.69,-308.38 360.69,-308.38"/>
<text xml:space="preserve" text-anchor="middle" x="388.12" y="-341.75" font-family="Helvetica,sans-Serif" font-size="10.00">invoke with</text>
<text xml:space="preserve" text-anchor="middle" x="388.12" y="-329" font-family="Helvetica,sans-Serif" font-size="10.00">job parameters</text>
<path fill="none" stroke="black" d="M376.19,-380.1C376.19,-380.1 376.19,-308.38 376.19,-308.38"/>
<polygon fill="black" stroke="black" points="379.69,-308.38 376.19,-298.38 372.69,-308.38 379.69,-308.38"/>
<text xml:space="preserve" text-anchor="middle" x="407.12" y="-341.75" font-family="Helvetica,sans-Serif" font-size="10.00">invoke with</text>
<text xml:space="preserve" text-anchor="middle" x="407.12" y="-329" font-family="Helvetica,sans-Serif" font-size="10.00">job parameters</text>
</g>
<!-- lambda&#45;&gt;fastapi -->
<g id="edge11" class="edge">
<title>lambda&#45;&gt;fastapi</title>
<path fill="none" stroke="black" d="M336.76,-267C274.82,-267 194.94,-267 194.94,-267 194.94,-267 194.94,-538.75 194.94,-538.75"/>
<polygon fill="black" stroke="black" points="191.44,-538.75 194.94,-548.75 198.44,-538.75 191.44,-538.75"/>
<text xml:space="preserve" text-anchor="middle" x="552.62" y="-413.38" font-family="Helvetica,sans-Serif" font-size="10.00">POST /jobs/{id}/callback</text>
<text xml:space="preserve" text-anchor="middle" x="552.62" y="-400.62" font-family="Helvetica,sans-Serif" font-size="10.00">update status</text>
<path fill="none" stroke="black" d="M355.73,-267C306.05,-267 248.62,-267 248.62,-267 248.62,-267 248.62,-538.75 248.62,-538.75"/>
<polygon fill="black" stroke="black" points="245.13,-538.75 248.63,-548.75 252.13,-538.75 245.13,-538.75"/>
<text xml:space="preserve" text-anchor="middle" x="571.62" y="-413.38" font-family="Helvetica,sans-Serif" font-size="10.00">POST /jobs/{id}/callback</text>
<text xml:space="preserve" text-anchor="middle" x="571.62" y="-400.62" font-family="Helvetica,sans-Serif" font-size="10.00">update status</text>
</g>
<!-- s3 -->
<g id="node9" class="node">
<title>s3</title>
<polygon fill="none" stroke="black" points="454.62,-153.5 451.62,-157.5 430.62,-157.5 427.62,-153.5 361.38,-153.5 361.38,-117.5 454.62,-117.5 454.62,-153.5"/>
<text xml:space="preserve" text-anchor="middle" x="408" y="-130.82" font-family="Helvetica,sans-Serif" font-size="14.00">S3 Buckets</text>
<polygon fill="none" stroke="black" points="473.62,-153.5 470.62,-157.5 449.62,-157.5 446.62,-153.5 380.38,-153.5 380.38,-117.5 473.62,-117.5 473.62,-153.5"/>
<text xml:space="preserve" text-anchor="middle" x="427" y="-130.82" font-family="Helvetica,sans-Serif" font-size="14.00">S3 Buckets</text>
</g>
<!-- lambda&#45;&gt;s3 -->
<g id="edge10" class="edge">
<title>lambda&#45;&gt;s3</title>
<path fill="none" stroke="black" d="M408,-236.73C408,-236.73 408,-165.27 408,-165.27"/>
<polygon fill="black" stroke="black" points="411.5,-165.27 408,-155.27 404.5,-165.27 411.5,-165.27"/>
<text xml:space="preserve" text-anchor="middle" x="445.5" y="-198.5" font-family="Helvetica,sans-Serif" font-size="10.00">download input</text>
<text xml:space="preserve" text-anchor="middle" x="445.5" y="-185.75" font-family="Helvetica,sans-Serif" font-size="10.00">upload output</text>
<path fill="none" stroke="black" d="M427,-236.73C427,-236.73 427,-165.27 427,-165.27"/>
<polygon fill="black" stroke="black" points="430.5,-165.27 427,-155.27 423.5,-165.27 430.5,-165.27"/>
<text xml:space="preserve" text-anchor="middle" x="464.5" y="-198.5" font-family="Helvetica,sans-Serif" font-size="10.00">download input</text>
<text xml:space="preserve" text-anchor="middle" x="464.5" y="-185.75" font-family="Helvetica,sans-Serif" font-size="10.00">upload output</text>
</g>
<!-- bucket_in -->
<g id="node10" class="node">
<title>bucket_in</title>
<polygon fill="none" stroke="black" points="360.75,-58.5 253.25,-58.5 253.25,-16 366.75,-16 366.75,-52.5 360.75,-58.5"/>
<polyline fill="none" stroke="black" points="360.75,-58.5 360.75,-52.5"/>
<polyline fill="none" stroke="black" points="366.75,-52.5 360.75,-52.5"/>
<text xml:space="preserve" text-anchor="middle" x="310" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr&#45;media&#45;in/</text>
<text xml:space="preserve" text-anchor="middle" x="310" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">input videos</text>
<polygon fill="none" stroke="black" points="379.75,-58.5 272.25,-58.5 272.25,-16 385.75,-16 385.75,-52.5 379.75,-58.5"/>
<polyline fill="none" stroke="black" points="379.75,-58.5 379.75,-52.5"/>
<polyline fill="none" stroke="black" points="385.75,-52.5 379.75,-52.5"/>
<text xml:space="preserve" text-anchor="middle" x="329" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr&#45;media&#45;in/</text>
<text xml:space="preserve" text-anchor="middle" x="329" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">input videos</text>
</g>
<!-- s3&#45;&gt;bucket_in -->
<g id="edge13" class="edge">
<title>s3&#45;&gt;bucket_in</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M361.09,-136C354.1,-136 349.19,-136 349.19,-136 349.19,-136 349.19,-87.72 349.19,-58.68"/>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M380.09,-136C373.1,-136 368.19,-136 368.19,-136 368.19,-136 368.19,-87.72 368.19,-58.68"/>
</g>
<!-- bucket_out -->
<g id="node11" class="node">
<title>bucket_out</title>
<polygon fill="none" stroke="black" points="563.12,-58.5 424.88,-58.5 424.88,-16 569.12,-16 569.12,-52.5 563.12,-58.5"/>
<polyline fill="none" stroke="black" points="563.12,-58.5 563.12,-52.5"/>
<polyline fill="none" stroke="black" points="569.12,-52.5 563.12,-52.5"/>
<text xml:space="preserve" text-anchor="middle" x="497" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr&#45;media&#45;out/</text>
<text xml:space="preserve" text-anchor="middle" x="497" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoded output</text>
<polygon fill="none" stroke="black" points="582.12,-58.5 443.88,-58.5 443.88,-16 588.12,-16 588.12,-52.5 582.12,-58.5"/>
<polyline fill="none" stroke="black" points="582.12,-58.5 582.12,-52.5"/>
<polyline fill="none" stroke="black" points="588.12,-52.5 582.12,-52.5"/>
<text xml:space="preserve" text-anchor="middle" x="516" y="-41.2" font-family="Helvetica,sans-Serif" font-size="14.00">mpr&#45;media&#45;out/</text>
<text xml:space="preserve" text-anchor="middle" x="516" y="-23.95" font-family="Helvetica,sans-Serif" font-size="14.00">transcoded output</text>
</g>
<!-- s3&#45;&gt;bucket_out -->
<g id="edge14" class="edge">
<title>s3&#45;&gt;bucket_out</title>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M439.75,-117.02C439.75,-100.45 439.75,-76.15 439.75,-58.73"/>
<path fill="none" stroke="black" stroke-dasharray="1,5" d="M458.75,-117.02C458.75,-100.45 458.75,-76.15 458.75,-58.73"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -57,8 +57,7 @@
</li>
<li>
<span class="color-box" style="background: #f0f8e8"></span>
Application Layer (Django Admin, FastAPI + GraphQL, Timeline
UI)
Application Layer (Django Admin, GraphQL API, Timeline UI)
</li>
<li>
<span class="color-box" style="background: #fff8e8"></span>
@@ -170,24 +169,31 @@
</p>
</div>
<h2>API Interfaces</h2>
<pre><code># REST API
http://mpr.local.ar/api/docs - Swagger UI
POST /api/assets/scan - Scan S3 bucket for media
POST /api/jobs/ - Create transcode job
POST /api/jobs/{id}/callback - Lambda completion callback
<h2>API (GraphQL)</h2>
<pre><code># GraphiQL IDE
http://mpr.local.ar/graphql
# GraphQL (GraphiQL)
http://mpr.local.ar/graphql - GraphiQL IDE
query { assets { id filename } }
mutation { createJob(input: {...}) { id status } }
mutation { scanMediaFolder { found registered } }</code></pre>
# 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 } }
# 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 } }
# Lambda callback (REST)
POST /api/jobs/{id}/callback - Lambda completion webhook</code></pre>
<h2>Access Points</h2>
<pre><code># Local development
127.0.0.1 mpr.local.ar
http://mpr.local.ar/admin - Django Admin
http://mpr.local.ar/api/docs - FastAPI Swagger
http://mpr.local.ar/graphql - GraphiQL
http://mpr.local.ar/ - Timeline UI
http://localhost:9001 - MinIO Console

View File

@@ -65,8 +65,7 @@
</li>
<li>
<span class="color-box" style="background: #f0f8e8"></span>
Application Layer (Django Admin, FastAPI + GraphQL, Timeline
UI)
Application Layer (Django Admin, GraphQL API, Timeline UI)
</li>
<li>
<span class="color-box" style="background: #fff8e8"></span>
@@ -223,15 +222,22 @@ MEDIA_BASE_URL=https://source-bucket.s3.amazonaws.com/media/</code></pre>
</div>
<div class="legend">
<h3>API Endpoints</h3>
<h3>API (GraphQL)</h3>
<p>
All client interactions go through GraphQL at
<code>/graphql</code>.
</p>
<ul>
<li>
<code>POST /api/assets/scan</code> - Recursively scans
MEDIA_IN and registers video/audio files
<code>scanMediaFolder</code> - Scan S3 bucket for media
files
</li>
<li><code>createJob</code> - Create transcode/trim job</li>
<li>
<code>cancelJob / retryJob</code> - Job lifecycle management
</li>
<li>
<code>POST /api/jobs/</code> - Creates transcoding job with
source asset, preset, and optional trim times
<code>updateAsset / deleteAsset</code> - Asset management
</li>
</ul>
<p><strong>Supported File Types:</strong></p>
@@ -241,15 +247,13 @@ MEDIA_BASE_URL=https://source-bucket.s3.amazonaws.com/media/</code></pre>
</p>
</div>
<h2>Quick Reference</h2>
<h2>Access Points</h2>
<pre><code># Add to /etc/hosts
127.0.0.1 mpr.local.ar
# URLs
http://mpr.local.ar/admin - Django Admin
http://mpr.local.ar/api - FastAPI (docs at /api/docs)
http://mpr.local.ar/ui - Timeline UI</code></pre>
http://mpr.local.ar/admin - Django Admin
http://mpr.local.ar/graphql - GraphiQL IDE
http://mpr.local.ar/ - Timeline UI</code></pre>
</body>
</html>

View File

@@ -6,16 +6,6 @@
"output": "mpr/media_assets/models.py",
"include": ["dataclasses", "enums"]
},
{
"target": "pydantic",
"output": "api/schema/",
"include": ["dataclasses", "enums"],
"name_map": {
"TranscodeJob": "Job",
"MediaAsset": "Asset",
"TranscodePreset": "Preset"
}
},
{
"target": "graphene",
"output": "api/schema/graphql.py",

View File

@@ -5,7 +5,13 @@ This module exports all dataclasses, enums, and constants that the generator
should process. Add new models here to have them included in generation.
"""
from .api import CreateJobRequest, ScanResult, SystemStatus
from .api import (
CreateJobRequest,
DeleteResult,
ScanResult,
SystemStatus,
UpdateAssetRequest,
)
from .grpc import (
GRPC_SERVICE,
CancelRequest,
@@ -26,7 +32,14 @@ DATACLASSES = [MediaAsset, TranscodePreset, TranscodeJob]
# API request/response models - generates TypeScript only (no Django)
# WorkerStatus from grpc.py is reused here
API_MODELS = [CreateJobRequest, SystemStatus, ScanResult, WorkerStatus]
API_MODELS = [
CreateJobRequest,
UpdateAssetRequest,
SystemStatus,
ScanResult,
DeleteResult,
WorkerStatus,
]
# Status enums - included in generated code
ENUMS = [AssetStatus, JobStatus]
@@ -50,6 +63,8 @@ __all__ = [
"TranscodeJob",
# API Models
"CreateJobRequest",
"UpdateAssetRequest",
"DeleteResult",
"ScanResult",
"SystemStatus",
# Enums

View File

@@ -40,4 +40,19 @@ class ScanResult:
files: List[str] = field(default_factory=list)
@dataclass
class UpdateAssetRequest:
"""Request body for updating asset metadata."""
comments: Optional[str] = None
tags: Optional[List[str]] = None
@dataclass
class DeleteResult:
"""Result of a delete operation."""
ok: bool = False
# Note: WorkerStatus is defined in grpc.py and reused here

View File

@@ -82,6 +82,11 @@ export interface CreateJobRequest {
priority: number;
}
export interface UpdateAssetRequest {
comments: string | null;
tags: string[] | null;
}
export interface SystemStatus {
status: string;
version: string;
@@ -94,6 +99,10 @@ export interface ScanResult {
files: string[];
}
export interface DeleteResult {
ok: boolean;
}
export interface WorkerStatus {
available: boolean;
active_jobs: number;