schema definitions
This commit is contained in:
49
schema/__init__.py
Normal file
49
schema/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
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/schemas/*.py)
|
||||||
|
- TypeScript types (ui/timeline/src/types.ts)
|
||||||
|
- Protobuf definitions (grpc/protos/worker.proto)
|
||||||
|
|
||||||
|
Run `python schema/generate.py` to regenerate all targets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Media
|
||||||
|
"MediaAsset",
|
||||||
|
"AssetStatus",
|
||||||
|
# Presets
|
||||||
|
"TranscodePreset",
|
||||||
|
"BUILTIN_PRESETS",
|
||||||
|
# Jobs
|
||||||
|
"TranscodeJob",
|
||||||
|
"JobStatus",
|
||||||
|
# gRPC
|
||||||
|
"JobRequest",
|
||||||
|
"JobResponse",
|
||||||
|
"ProgressRequest",
|
||||||
|
"ProgressUpdate",
|
||||||
|
"CancelRequest",
|
||||||
|
"CancelResponse",
|
||||||
|
"WorkerStatus",
|
||||||
|
"Empty",
|
||||||
|
"GRPC_SERVICE",
|
||||||
|
]
|
||||||
130
schema/grpc.py
Normal file
130
schema/grpc.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
"""
|
||||||
|
gRPC message definitions for MPR worker communication.
|
||||||
|
|
||||||
|
This is the source of truth for gRPC messages. The generator creates:
|
||||||
|
- grpc/protos/worker.proto (protobuf definition)
|
||||||
|
- grpc/worker_pb2.py (generated Python classes)
|
||||||
|
- grpc/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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
78
schema/jobs.py
Normal file
78
schema/jobs.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
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
|
||||||
|
)
|
||||||
59
schema/media.py
Normal file
59
schema/media.py
Normal 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
|
||||||
128
schema/presets.py
Normal file
128
schema/presets.py
Normal 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
|
||||||
|
},
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user