django and fastapi apps
This commit is contained in:
64
schema/models/__init__.py
Normal file
64
schema/models/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
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 .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]
|
||||
|
||||
# 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",
|
||||
# Enums
|
||||
"AssetStatus",
|
||||
"JobStatus",
|
||||
# gRPC
|
||||
"GRPC_SERVICE",
|
||||
"JobRequest",
|
||||
"JobResponse",
|
||||
"ProgressRequest",
|
||||
"ProgressUpdate",
|
||||
"CancelRequest",
|
||||
"CancelResponse",
|
||||
"WorkerStatus",
|
||||
"Empty",
|
||||
# For generator
|
||||
"DATACLASSES",
|
||||
"ENUMS",
|
||||
"GRPC_MESSAGES",
|
||||
"BUILTIN_PRESETS",
|
||||
]
|
||||
130
schema/models/grpc.py
Normal file
130
schema/models/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/models/jobs.py
Normal file
78
schema/models/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/models/media.py
Normal file
59
schema/models/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/models/presets.py
Normal file
128
schema/models/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