major refactor

This commit is contained in:
2026-03-13 01:07:02 -03:00
parent eaaf2ad60c
commit 3eeedebb15
61 changed files with 441 additions and 242 deletions

58
core/schema/__init__.py Normal file
View File

@@ -0,0 +1,58 @@
"""
MPR Schema Definitions - Source of Truth
This package defines the core data models as Python dataclasses.
These definitions are used to generate:
- Django ORM models (mpr/media_assets/models.py)
- Pydantic schemas (api/schema/*.py)
- TypeScript types (ui/timeline/src/types.ts)
- Protobuf definitions (grpc/protos/worker.proto)
Run `python schema/generate.py` to regenerate all targets.
"""
from .models import (
BUILTIN_PRESETS,
# For generator
DATACLASSES,
ENUMS,
GRPC_MESSAGES,
# gRPC
GRPC_SERVICE,
# Enums
AssetStatus,
CancelRequest,
CancelResponse,
Empty,
JobRequest,
JobResponse,
JobStatus,
# Models
MediaAsset,
ProgressRequest,
ProgressUpdate,
TranscodeJob,
TranscodePreset,
WorkerStatus,
)
__all__ = [
"MediaAsset",
"TranscodePreset",
"TranscodeJob",
"AssetStatus",
"JobStatus",
"GRPC_SERVICE",
"JobRequest",
"JobResponse",
"ProgressRequest",
"ProgressUpdate",
"CancelRequest",
"CancelResponse",
"WorkerStatus",
"Empty",
"DATACLASSES",
"ENUMS",
"GRPC_MESSAGES",
"BUILTIN_PRESETS",
]

25
core/schema/modelgen.json Normal file
View File

@@ -0,0 +1,25 @@
{
"schema": "core/schema/models",
"targets": [
{
"target": "django",
"output": "admin/mpr/media_assets/models.py",
"include": ["dataclasses", "enums"]
},
{
"target": "graphene",
"output": "core/api/schema/graphql.py",
"include": ["dataclasses", "enums", "api"]
},
{
"target": "typescript",
"output": "ui/timeline/src/types.ts",
"include": ["dataclasses", "enums", "api"]
},
{
"target": "protobuf",
"output": "core/rpc/protos/worker.proto",
"include": ["grpc"]
}
]
}

View File

@@ -0,0 +1,89 @@
"""
MPR Schema Models
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,
DeleteResult,
ScanResult,
SystemStatus,
UpdateAssetRequest,
)
from .grpc import (
GRPC_SERVICE,
CancelRequest,
CancelResponse,
Empty,
JobRequest,
JobResponse,
ProgressRequest,
ProgressUpdate,
WorkerStatus,
)
from .jobs import JobStatus, TranscodeJob
from .media import AssetStatus, MediaAsset
from .presets import BUILTIN_PRESETS, TranscodePreset
# Core domain models - generates Django, Pydantic, TypeScript
DATACLASSES = [MediaAsset, TranscodePreset, TranscodeJob]
# API request/response models - generates TypeScript only (no Django)
# WorkerStatus from grpc.py is reused here
API_MODELS = [
CreateJobRequest,
UpdateAssetRequest,
SystemStatus,
ScanResult,
DeleteResult,
WorkerStatus,
]
# Status enums - included in generated code
ENUMS = [AssetStatus, JobStatus]
# gRPC messages - generates Proto
GRPC_MESSAGES = [
JobRequest,
JobResponse,
ProgressRequest,
ProgressUpdate,
CancelRequest,
CancelResponse,
WorkerStatus,
Empty,
]
__all__ = [
# Models
"MediaAsset",
"TranscodePreset",
"TranscodeJob",
# API Models
"CreateJobRequest",
"UpdateAssetRequest",
"DeleteResult",
"ScanResult",
"SystemStatus",
# Enums
"AssetStatus",
"JobStatus",
# gRPC
"GRPC_SERVICE",
"JobRequest",
"JobResponse",
"ProgressRequest",
"ProgressUpdate",
"CancelRequest",
"CancelResponse",
"WorkerStatus",
"Empty",
# For generator
"DATACLASSES",
"API_MODELS",
"ENUMS",
"GRPC_MESSAGES",
"BUILTIN_PRESETS",
]

58
core/schema/models/api.py Normal file
View File

@@ -0,0 +1,58 @@
"""
API Request/Response Schema Definitions
These are separate from the main domain models and represent
the shape of data sent to/from the API endpoints.
"""
from dataclasses import dataclass, field
from typing import List, Optional
from uuid import UUID
@dataclass
class CreateJobRequest:
"""Request body for creating a transcode/trim job."""
source_asset_id: UUID
preset_id: Optional[UUID] = None
trim_start: Optional[float] = None # seconds
trim_end: Optional[float] = None # seconds
output_filename: Optional[str] = None
priority: int = 0
@dataclass
class SystemStatus:
"""System status response."""
status: str
version: str
@dataclass
class ScanResult:
"""Result of scanning the media input bucket."""
found: int = 0
registered: int = 0
skipped: int = 0
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

130
core/schema/models/grpc.py Normal file
View File

@@ -0,0 +1,130 @@
"""
gRPC message definitions for MPR worker communication.
This is the source of truth for gRPC messages. The generator creates:
- rpc/protos/worker.proto (protobuf definition)
- rpc/worker_pb2.py (generated Python classes)
- rpc/worker_pb2_grpc.py (generated gRPC stubs)
"""
from dataclasses import dataclass
from typing import Optional
# -----------------------------------------------------------------------------
# Request Messages
# -----------------------------------------------------------------------------
@dataclass
class JobRequest:
"""Request to submit a transcode/trim job."""
job_id: str
source_path: str
output_path: str
preset_json: str # Serialized TranscodePreset
trim_start: Optional[float] = None
trim_end: Optional[float] = None
@dataclass
class ProgressRequest:
"""Request to stream progress updates for a job."""
job_id: str
@dataclass
class CancelRequest:
"""Request to cancel a running job."""
job_id: str
@dataclass
class Empty:
"""Empty message for requests with no parameters."""
pass
# -----------------------------------------------------------------------------
# Response Messages
# -----------------------------------------------------------------------------
@dataclass
class JobResponse:
"""Response after submitting a job."""
job_id: str
accepted: bool
message: str
@dataclass
class ProgressUpdate:
"""Streaming progress update from worker."""
job_id: str
progress: int # 0-100
current_frame: int
current_time: float
speed: float # e.g., 2.5x
status: str # pending, processing, completed, failed, cancelled
error: Optional[str] = None
@dataclass
class CancelResponse:
"""Response after cancel request."""
job_id: str
cancelled: bool
message: str
@dataclass
class WorkerStatus:
"""Worker health and capabilities."""
available: bool
active_jobs: int
supported_codecs: list[str]
gpu_available: bool
# -----------------------------------------------------------------------------
# Service Definition (for documentation, generator uses this)
# -----------------------------------------------------------------------------
GRPC_SERVICE = {
"name": "WorkerService",
"package": "mpr.worker",
"methods": [
{
"name": "SubmitJob",
"request": JobRequest,
"response": JobResponse,
"stream_response": False,
},
{
"name": "StreamProgress",
"request": ProgressRequest,
"response": ProgressUpdate,
"stream_response": True, # Server streaming
},
{
"name": "CancelJob",
"request": CancelRequest,
"response": CancelResponse,
"stream_response": False,
},
{
"name": "GetWorkerStatus",
"request": Empty,
"response": WorkerStatus,
"stream_response": False,
},
],
}

View File

@@ -0,0 +1,79 @@
"""
TranscodeJob Schema Definition
Source of truth for job data model.
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Optional
from uuid import UUID
class JobStatus(str, Enum):
"""Status of a transcode/trim job."""
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
CANCELLED = "cancelled"
@dataclass
class TranscodeJob:
"""
A transcoding or trimming job in the queue.
Jobs can either:
- Transcode using a preset (full re-encode)
- Trim only (stream copy with -c:v copy -c:a copy)
A trim-only job has no preset and uses stream copy.
"""
id: UUID
# Input
source_asset_id: UUID
# Configuration
preset_id: Optional[UUID] = None
preset_snapshot: Dict[str, Any] = field(
default_factory=dict
) # Copy at creation time
# Trimming (optional)
trim_start: Optional[float] = None # seconds
trim_end: Optional[float] = None # seconds
# Output
output_filename: str = ""
output_path: Optional[str] = None
output_asset_id: Optional[UUID] = None
# Status & Progress
status: JobStatus = JobStatus.PENDING
progress: float = 0.0 # 0.0 to 100.0
current_frame: Optional[int] = None
current_time: Optional[float] = None # seconds processed
speed: Optional[str] = None # "2.5x"
error_message: Optional[str] = None
# Worker tracking
celery_task_id: Optional[str] = None
execution_arn: Optional[str] = None # AWS Step Functions execution ARN
priority: int = 0 # Lower = higher priority
# Timestamps
created_at: Optional[datetime] = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
@property
def is_trim_only(self) -> bool:
"""Check if this is a trim-only job (stream copy, no transcode)."""
return self.preset_id is None and (
self.trim_start is not None or self.trim_end is not None
)

View File

@@ -0,0 +1,59 @@
"""
MediaAsset Schema Definition
Source of truth for media asset data model.
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, List, Optional
from uuid import UUID
class AssetStatus(str, Enum):
"""Status of a media asset after probing."""
PENDING = "pending"
READY = "ready"
ERROR = "error"
@dataclass
class MediaAsset:
"""
A video/audio file registered in the system.
Metadata is populated asynchronously via ffprobe after registration.
"""
id: UUID
filename: str
file_path: str
# Status
status: AssetStatus = AssetStatus.PENDING
error_message: Optional[str] = None
# File info
file_size: Optional[int] = None
# Media metadata (populated by ffprobe)
duration: Optional[float] = None # seconds
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 # bits per second
# Full ffprobe output and custom metadata
properties: Dict[str, Any] = field(default_factory=dict)
# User annotations
comments: str = ""
tags: List[str] = field(default_factory=list)
# Timestamps
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None

View File

@@ -0,0 +1,128 @@
"""
TranscodePreset Schema Definition
Source of truth for preset data model.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional
from uuid import UUID
@dataclass
class TranscodePreset:
"""
A reusable transcoding configuration (like Handbrake presets).
Presets can be builtin (shipped with the app) or user-created.
"""
id: UUID
name: str
description: str = ""
is_builtin: bool = False
# Output container
container: str = "mp4" # mp4, mkv, webm, mov, avi
# Video settings
video_codec: str = "libx264"
video_bitrate: Optional[str] = None # "2M", "5000k"
video_crf: Optional[int] = None # Quality-based (0-51 for x264)
video_preset: Optional[str] = None # ultrafast...veryslow
resolution: Optional[str] = None # "1920x1080", "1280x720"
framerate: Optional[float] = None
# Audio settings
audio_codec: str = "aac"
audio_bitrate: Optional[str] = None # "128k", "320k"
audio_channels: Optional[int] = None # 2 for stereo
audio_samplerate: Optional[int] = None # 44100, 48000
# Advanced: extra FFmpeg arguments
extra_args: List[str] = field(default_factory=list)
# Timestamps
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
BUILTIN_PRESETS = [
{
"name": "DaVinci Resolve",
"description": "MPEG-4 (xvid) + MP3 - Compatible with DaVinci Resolve Free",
"container": "avi",
"video_codec": "mpeg4",
"video_crf": 5,
"audio_codec": "libmp3lame",
"audio_bitrate": "320k",
"audio_samplerate": 48000,
"extra_args": ["-vtag", "xvid", "-pix_fmt", "yuv420p"],
},
{
"name": "Copy (Trim Only)",
"description": "Stream copy - No transcoding, fast trimming only",
"container": "mp4",
"video_codec": "copy",
"audio_codec": "copy",
},
{
"name": "Web H.264",
"description": "H.264 + AAC - General web playback",
"container": "mp4",
"video_codec": "libx264",
"video_crf": 23,
"video_preset": "medium",
"audio_codec": "aac",
"audio_bitrate": "128k",
},
{
"name": "Web H.265",
"description": "HEVC + AAC - Smaller files, modern browsers",
"container": "mp4",
"video_codec": "libx265",
"video_crf": 28,
"video_preset": "medium",
"audio_codec": "aac",
"audio_bitrate": "128k",
},
{
"name": "DNxHR HQ",
"description": "DNxHR High Quality - Professional editing",
"container": "mov",
"video_codec": "dnxhd",
"audio_codec": "pcm_s16le",
"audio_samplerate": 48000,
"extra_args": ["-profile:v", "dnxhr_hq", "-pix_fmt", "yuv422p"],
},
{
"name": "H.264 NVENC",
"description": "NVIDIA GPU encoding - Fast H.264",
"container": "mp4",
"video_codec": "h264_nvenc",
"video_bitrate": "10M",
"audio_codec": "aac",
"audio_bitrate": "192k",
"extra_args": ["-preset", "p4", "-rc", "vbr", "-cq", "19"],
},
{
"name": "HEVC NVENC",
"description": "NVIDIA GPU encoding - HEVC/H.265",
"container": "mp4",
"video_codec": "hevc_nvenc",
"video_bitrate": "8M",
"audio_codec": "aac",
"audio_bitrate": "192k",
"extra_args": ["-preset", "p4", "-rc", "vbr", "-cq", "23"],
},
{
"name": "Archive ProRes",
"description": "Apple ProRes 422 HQ - Archival quality",
"container": "mov",
"video_codec": "prores_ks",
"audio_codec": "pcm_s16le",
"audio_samplerate": 48000,
"extra_args": ["-profile:v", "3"], # ProRes 422 HQ
},
]