375 lines
14 KiB
Python
375 lines
14 KiB
Python
from pydantic import BaseModel, Field, validator
|
|
from typing import Optional, List, Dict
|
|
from datetime import date, datetime
|
|
from enum import Enum
|
|
import re
|
|
|
|
from models.project import ProjectStatus, ProjectType
|
|
from models.user import DepartmentRole
|
|
|
|
|
|
# Technical Specifications Schemas
|
|
class DeliveryMovieSpec(BaseModel):
|
|
"""Delivery movie specifications for a department"""
|
|
resolution: str = Field(..., description="Movie resolution (e.g., '1920x1080', '2048x1080')")
|
|
format: str = Field(..., description="Movie format (e.g., 'mov', 'mp4', 'exr')")
|
|
codec: Optional[str] = Field(None, description="Video codec (e.g., 'h264', 'prores', 'dnxhd')")
|
|
quality: Optional[str] = Field(None, description="Quality setting (e.g., 'high', 'medium', 'low')")
|
|
|
|
@validator('resolution')
|
|
def validate_resolution(cls, v):
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
@validator('format')
|
|
def validate_format(cls, v):
|
|
allowed_formats = ['mov', 'mp4', 'exr', 'dpx', 'tiff']
|
|
if v.lower() not in allowed_formats:
|
|
raise ValueError(f'Format must be one of: {", ".join(allowed_formats)}')
|
|
return v.lower()
|
|
|
|
@validator('codec')
|
|
def validate_codec(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_codecs = ['h264', 'h265', 'prores', 'dnxhd', 'dnxhr', 'uncompressed']
|
|
if v.lower() not in allowed_codecs:
|
|
raise ValueError(f'Codec must be one of: {", ".join(allowed_codecs)}')
|
|
return v.lower()
|
|
|
|
@validator('quality')
|
|
def validate_quality(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_qualities = ['low', 'medium', 'high', 'lossless']
|
|
if v.lower() not in allowed_qualities:
|
|
raise ValueError(f'Quality must be one of: {", ".join(allowed_qualities)}')
|
|
return v.lower()
|
|
|
|
|
|
class ProjectTechnicalSpecs(BaseModel):
|
|
"""Technical specifications for a project"""
|
|
frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)")
|
|
data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage")
|
|
publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery")
|
|
delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution (e.g., '1920x1080', '4096x2160')")
|
|
delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field(
|
|
default_factory=dict,
|
|
description="Delivery movie resolution and format specifications per department"
|
|
)
|
|
|
|
@validator('delivery_image_resolution')
|
|
def validate_delivery_image_resolution(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
@validator('delivery_movie_specs_by_department')
|
|
def validate_delivery_movie_specs_by_department(cls, v):
|
|
if v is None:
|
|
return {}
|
|
allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing']
|
|
for dept in v.keys():
|
|
if dept not in allowed_departments:
|
|
raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}')
|
|
return v
|
|
|
|
|
|
# Default delivery movie specifications per department
|
|
DEFAULT_DELIVERY_MOVIE_SPECS = {
|
|
"layout": DeliveryMovieSpec(
|
|
resolution="1920x1080",
|
|
format="mov",
|
|
codec="h264",
|
|
quality="medium"
|
|
),
|
|
"animation": DeliveryMovieSpec(
|
|
resolution="1920x1080",
|
|
format="mov",
|
|
codec="h264",
|
|
quality="high"
|
|
),
|
|
"lighting": DeliveryMovieSpec(
|
|
resolution="2048x1080",
|
|
format="exr",
|
|
codec=None,
|
|
quality="high"
|
|
),
|
|
"composite": DeliveryMovieSpec(
|
|
resolution="2048x1080",
|
|
format="mov",
|
|
codec="prores",
|
|
quality="high"
|
|
)
|
|
}
|
|
|
|
|
|
class ProjectBase(BaseModel):
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
code_name: str = Field(..., min_length=1, max_length=50, description="Unique project code identifier")
|
|
client_name: str = Field(..., min_length=1, max_length=255, description="Client or studio name")
|
|
project_type: ProjectType = Field(..., description="Project type: TV, Cinema, or Game")
|
|
description: Optional[str] = None
|
|
status: ProjectStatus = ProjectStatus.PLANNING
|
|
start_date: Optional[date] = None
|
|
end_date: Optional[date] = None
|
|
|
|
# Technical specifications
|
|
frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)")
|
|
data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage")
|
|
publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery")
|
|
delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution")
|
|
delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field(
|
|
default_factory=dict,
|
|
description="Delivery movie specifications per department"
|
|
)
|
|
|
|
@validator('delivery_image_resolution')
|
|
def validate_delivery_image_resolution(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
@validator('delivery_movie_specs_by_department')
|
|
def validate_delivery_movie_specs_by_department(cls, v):
|
|
if v is None:
|
|
return {}
|
|
allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing']
|
|
for dept in v.keys():
|
|
if dept not in allowed_departments:
|
|
raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}')
|
|
return v
|
|
|
|
|
|
class ProjectCreate(ProjectBase):
|
|
pass
|
|
|
|
|
|
class ProjectUpdate(BaseModel):
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
code_name: Optional[str] = Field(None, min_length=1, max_length=50, description="Unique project code identifier")
|
|
client_name: Optional[str] = Field(None, min_length=1, max_length=255, description="Client or studio name")
|
|
project_type: Optional[ProjectType] = Field(None, description="Project type: TV, Cinema, or Game")
|
|
description: Optional[str] = None
|
|
status: Optional[ProjectStatus] = None
|
|
start_date: Optional[date] = None
|
|
end_date: Optional[date] = None
|
|
|
|
# Technical specifications
|
|
frame_rate: Optional[float] = Field(None, ge=1.0, le=120.0, description="Frames per second (1-120 fps)")
|
|
data_drive_path: Optional[str] = Field(None, description="Physical path for project data storage")
|
|
publish_storage_path: Optional[str] = Field(None, description="Path for approved work delivery")
|
|
delivery_image_resolution: Optional[str] = Field(None, description="Required image resolution")
|
|
delivery_movie_specs_by_department: Optional[Dict[str, DeliveryMovieSpec]] = Field(
|
|
None,
|
|
description="Delivery movie specifications per department"
|
|
)
|
|
|
|
@validator('delivery_image_resolution')
|
|
def validate_delivery_image_resolution(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not re.match(r'^\d+x\d+$', v):
|
|
raise ValueError('Delivery image resolution must be in format "WIDTHxHEIGHT" (e.g., "1920x1080")')
|
|
return v
|
|
|
|
@validator('delivery_movie_specs_by_department')
|
|
def validate_delivery_movie_specs_by_department(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_departments = ['layout', 'animation', 'lighting', 'composite', 'modeling', 'rigging', 'surfacing']
|
|
for dept in v.keys():
|
|
if dept not in allowed_departments:
|
|
raise ValueError(f'Department must be one of: {", ".join(allowed_departments)}')
|
|
return v
|
|
|
|
|
|
class ProjectMemberBase(BaseModel):
|
|
user_id: int
|
|
department_role: Optional[DepartmentRole] = None
|
|
|
|
|
|
class ProjectMemberCreate(ProjectMemberBase):
|
|
pass
|
|
|
|
|
|
class ProjectMemberUpdate(BaseModel):
|
|
department_role: Optional[DepartmentRole] = None
|
|
|
|
|
|
class ProjectMemberResponse(ProjectMemberBase):
|
|
id: int
|
|
project_id: int
|
|
joined_at: datetime
|
|
|
|
# User information
|
|
user_email: str
|
|
user_first_name: str
|
|
user_last_name: str
|
|
user_avatar_url: Optional[str] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ProjectResponse(ProjectBase):
|
|
id: int
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
thumbnail_url: Optional[str] = None
|
|
|
|
# Optional member list
|
|
project_members: Optional[List[ProjectMemberResponse]] = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ProjectListResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
code_name: str
|
|
client_name: str
|
|
project_type: ProjectType
|
|
description: Optional[str] = None
|
|
status: ProjectStatus
|
|
start_date: Optional[date] = None
|
|
end_date: Optional[date] = None
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
thumbnail_url: Optional[str] = None
|
|
|
|
# Summary information
|
|
member_count: int = 0
|
|
episode_count: int = 0
|
|
asset_count: int = 0
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Default asset tasks by category
|
|
DEFAULT_ASSET_TASKS = {
|
|
"characters": ["modeling", "surfacing", "rigging"],
|
|
"props": ["modeling", "surfacing"],
|
|
"sets": ["modeling", "surfacing"],
|
|
"vehicles": ["modeling", "surfacing", "rigging"]
|
|
}
|
|
|
|
# Default shot tasks
|
|
DEFAULT_SHOT_TASKS = ["layout", "animation", "simulation", "lighting", "compositing"]
|
|
|
|
|
|
class ProjectSettings(BaseModel):
|
|
"""Project-specific settings for upload location and task templates"""
|
|
upload_data_location: Optional[str] = Field(None, description="Custom upload storage path for project files")
|
|
asset_task_templates: Optional[Dict[str, List[str]]] = Field(
|
|
default_factory=lambda: DEFAULT_ASSET_TASKS.copy(),
|
|
description="Custom default tasks per asset category"
|
|
)
|
|
shot_task_templates: Optional[List[str]] = Field(
|
|
default_factory=lambda: DEFAULT_SHOT_TASKS.copy(),
|
|
description="Custom default tasks for shots"
|
|
)
|
|
enabled_asset_tasks: Optional[Dict[str, List[str]]] = Field(
|
|
default_factory=dict,
|
|
description="Enabled/disabled status for asset tasks per category"
|
|
)
|
|
enabled_shot_tasks: Optional[List[str]] = Field(
|
|
default_factory=lambda: DEFAULT_SHOT_TASKS.copy(),
|
|
description="Enabled/disabled shot tasks"
|
|
)
|
|
|
|
@validator('asset_task_templates')
|
|
def validate_asset_task_templates(cls, v):
|
|
if v is None:
|
|
return DEFAULT_ASSET_TASKS.copy()
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Task templates for {category} must be a list')
|
|
return v
|
|
|
|
@validator('shot_task_templates')
|
|
def validate_shot_task_templates(cls, v):
|
|
if v is None:
|
|
return DEFAULT_SHOT_TASKS.copy()
|
|
if not isinstance(v, list):
|
|
raise ValueError('Shot task templates must be a list')
|
|
return v
|
|
|
|
@validator('enabled_asset_tasks')
|
|
def validate_enabled_asset_tasks(cls, v):
|
|
if v is None:
|
|
return {}
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Enabled tasks for {category} must be a list')
|
|
return v
|
|
|
|
@validator('enabled_shot_tasks')
|
|
def validate_enabled_shot_tasks(cls, v):
|
|
if v is None:
|
|
return DEFAULT_SHOT_TASKS.copy()
|
|
if not isinstance(v, list):
|
|
raise ValueError('Enabled shot tasks must be a list')
|
|
return v
|
|
|
|
|
|
class ProjectSettingsUpdate(BaseModel):
|
|
"""Update project settings"""
|
|
upload_data_location: Optional[str] = Field(None, description="Custom upload storage path for project files")
|
|
asset_task_templates: Optional[Dict[str, List[str]]] = Field(None, description="Custom default tasks per asset category")
|
|
shot_task_templates: Optional[List[str]] = Field(None, description="Custom default tasks for shots")
|
|
enabled_asset_tasks: Optional[Dict[str, List[str]]] = Field(None, description="Enabled/disabled asset tasks per category")
|
|
enabled_shot_tasks: Optional[List[str]] = Field(None, description="Enabled/disabled shot tasks")
|
|
|
|
@validator('asset_task_templates')
|
|
def validate_asset_task_templates(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Task templates for {category} must be a list')
|
|
return v
|
|
|
|
@validator('shot_task_templates')
|
|
def validate_shot_task_templates(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not isinstance(v, list):
|
|
raise ValueError('Shot task templates must be a list')
|
|
return v
|
|
|
|
@validator('enabled_asset_tasks')
|
|
def validate_enabled_asset_tasks(cls, v):
|
|
if v is None:
|
|
return v
|
|
allowed_categories = ['characters', 'props', 'sets', 'vehicles']
|
|
for category in v.keys():
|
|
if category not in allowed_categories:
|
|
raise ValueError(f'Asset category must be one of: {", ".join(allowed_categories)}')
|
|
if not isinstance(v[category], list):
|
|
raise ValueError(f'Enabled tasks for {category} must be a list')
|
|
return v
|
|
|
|
@validator('enabled_shot_tasks')
|
|
def validate_enabled_shot_tasks(cls, v):
|
|
if v is None:
|
|
return v
|
|
if not isinstance(v, list):
|
|
raise ValueError('Enabled shot tasks must be a list')
|
|
return v |