Files
soroush.asadi 4f04f6bf75
CI/CD / CI · Web (tsc) (push) Successful in 1m21s
CI/CD / Deploy · full stack (push) Failing after 20s
feat(render+templates): Remotion engine, 16 branded templates (incl. 3D), seconds pricing, coming-soon
Render engine
- Add Remotion (code-based) as a 2nd render engine alongside After Effects.
  node-agent dispatches on Job.Engine; RunRemotion maps bindings -> --props,
  renders native then ffmpeg-scales to the quality tier (aspect-preserving).
- content.projects.render_engine + render_remotion_comp (migration 32);
  render-svc claim resolves engine and routes (skips .aep for Remotion).
- Admin TemplatesAdmin gains an engine picker + Remotion composition id field.

Template pack (services/remotion)
- 16 branded, Persian (Vazirmatn), color- and text-editable templates, each in
  3 aspects (16:9 / 1:1 / 9:16): LogoMotion, Opener, InstaPromo, YouTubeIntro,
  Slideshow, HappyBirthday, SalePromo, QuoteCard, EventInvite, Countdown,
  GlitterReveal (editable logo image), NowruzGreeting (animated characters),
  and 4 cinematic 3D templates via @remotion/three (Hero3D, Nowruz3D,
  Birthday3D, Promo3D) with reflections + bloom/DOF/vignette.
- scripts/seed_remotion_templates.py seeds containers/projects/scenes/colors.

Pricing
- Rewrite /pricing to the seconds-based model (charge = length x resolution),
  data-driven from /v1/plans, Toman, broker checkout.

Coming-soon
- Persian experimental-build overlay on all pages (launch date + countdown).

Fixes
- middleware matcher bypasses all static asset paths; catalog mapping passes
  cover image + preview video so real thumbnails render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:52:52 +03:30

489 lines
20 KiB
Go

package models
import (
"time"
"github.com/google/uuid"
)
// ── Enums ────────────────────────────────────────────────────────────────────
const (
// NodeStatus
NodeStatusReady = "Ready"
NodeStatusBusy = "Busy"
NodeStatusOffline = "Offline"
NodeStatusMaintenance = "Maintenance"
NodeStatusCrashed = "Crashed"
NodeStatusUpdating = "Updating"
NodeStatusDisabled = "Disabled"
// NodeKind
NodeKindShared = "Shared"
NodeKindDedicated = "Dedicated"
NodeKindSpot = "Spot"
// RenderStep
StepQueued = "Queued"
StepPreparing = "Preparing"
StepTemplateCache = "TemplateCache"
StepJsxGen = "JsxGen"
StepMusic = "Music"
StepRendering = "Rendering"
StepValidating = "Validating"
StepRepairing = "Repairing"
StepOptimisation = "Optimisation"
StepVideo = "Video"
StepMixing = "Mixing"
StepFinal = "Final"
StepUploading = "Uploading"
StepDone = "Done"
StepFailed = "Failed"
StepCancelled = "Cancelled"
// PriceKind
PriceKindFree = "Free"
PriceKindPreview = "Preview"
PriceKindCash = "Cash"
PriceKindPlan = "Plan"
PriceKindSnapshot = "Snapshot"
PriceKindReseller = "Reseller"
// RenderQuality
QualityLow = "Low"
QualityMedium = "Medium"
QualityHigh = "High"
QualityFull = "Full"
QualityLossless = "Lossless"
// FrameJobStatus
FrameJobPending = "Pending"
FrameJobRendering = "Rendering"
FrameJobValidated = "Validated"
FrameJobRepairing = "Repairing"
FrameJobConverting = "Converting"
FrameJobDone = "Done"
FrameJobFailed = "Failed"
// PriorityQueue
QueueSnapshot = "snapshot"
QueueVIP = "vip"
QueuePaid = "paid"
QueuePreview = "preview"
QueueMockup = "mockup"
QueueVoiceover = "voiceover"
// Export types
ExportCreateRender = "Render"
ExportCreateUpload = "Upload"
ExportCreateSnapshot = "Snapshot"
ExportCreateReupload = "Reupload"
ExportFileVideo = "Video"
ExportFileImage = "Image"
ExportFileAudio = "Audio"
ExportFileGIF = "GIF"
ExportFilePDF = "PDF"
)
// ── Domain entities ──────────────────────────────────────────────────────────
type RenderNode struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Region string `json:"region"`
NodeIP string `json:"node_ip"`
WorkerPort int `json:"worker_port"`
PublicEndpoint *string `json:"public_endpoint,omitempty"`
RamGB *int `json:"ram_gb,omitempty"`
CPUCores *int `json:"cpu_cores,omitempty"`
GPUModel *string `json:"gpu_model,omitempty"`
StorageGB *int `json:"storage_gb,omitempty"`
CurrentAEVersion string `json:"current_ae_version"`
AvailableAEVersions []string `json:"available_ae_versions"`
NodeAgentVersion *string `json:"node_agent_version,omitempty"`
NodeKind string `json:"node_kind"`
OwnerUserID *uuid.UUID `json:"owner_user_id,omitempty"`
OwnerTenantID *uuid.UUID `json:"owner_tenant_id,omitempty"`
Status string `json:"status"`
CurrentJobID *uuid.UUID `json:"current_job_id,omitempty"`
CurrentFrameJobID *uuid.UUID `json:"current_frame_job_id,omitempty"`
JobStartedAt *time.Time `json:"job_started_at,omitempty"`
LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"`
LastCPUPct *int `json:"last_cpu_pct,omitempty"`
LastRAMAvailableMB *int `json:"last_ram_available_mb,omitempty"`
LastDiskPct *int `json:"last_disk_pct,omitempty"`
DiskTotalGB *int `json:"disk_total_gb,omitempty"`
AERunning bool `json:"ae_running"`
LifetimeTaskCount int64 `json:"lifetime_task_count"`
LifetimeCrashCount int `json:"lifetime_crash_count"`
ConsecutiveFailures int `json:"consecutive_failures"`
Priority int `json:"priority"`
IsActive bool `json:"is_active"`
AcceptsNewJobs bool `json:"accepts_new_jobs"`
LastMaintenanceAt *time.Time `json:"last_maintenance_at,omitempty"`
NextMaintenanceAt *time.Time `json:"next_maintenance_at,omitempty"`
MaintenanceReason *string `json:"maintenance_reason,omitempty"`
CachedTemplateMD5s []string `json:"cached_template_md5s"`
CacheUsedGB int `json:"cache_used_gb"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RenderJob struct {
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenant_id"`
UserID uuid.UUID `json:"user_id"`
SavedProjectID uuid.UUID `json:"saved_project_id"`
OriginalProjectID uuid.UUID `json:"original_project_id"`
ProjectName *string `json:"project_name,omitempty"`
Title *string `json:"title,omitempty"`
Name *string `json:"name,omitempty"`
ExternalJobID *string `json:"external_job_id,omitempty"`
PriorityQueue string `json:"priority_queue"`
PriorityScore int `json:"priority_score"`
Step string `json:"step"`
RenderProgress int `json:"render_progress"`
ConvertProgress int `json:"convert_progress"`
ImagePreviewB64 *string `json:"image_preview_b64,omitempty"`
PriceType string `json:"price_type"`
PaidPriceMinor int64 `json:"paid_price_minor"`
DiscountCode *string `json:"discount_code,omitempty"`
SupportFlatrender bool `json:"support_flatrender"`
Mode string `json:"mode"`
Quality string `json:"quality"`
Resolution string `json:"resolution"`
RHeight int `json:"r_height"`
FrameRate int `json:"frame_rate"`
Is60FPS bool `json:"is_60_fps"`
DurationSec float64 `json:"duration_sec"`
ExportDurationSec *float64 `json:"export_duration_sec,omitempty"`
HasMusic bool `json:"has_music"`
HasSFX bool `json:"has_sfx"`
HasVoiceover bool `json:"has_voiceover"`
MusicVolume *float64 `json:"music_volume,omitempty"`
SFXVolume *float64 `json:"sfx_volume,omitempty"`
VoiceoverVolume *float64 `json:"voiceover_volume,omitempty"`
RenderNodeCount int `json:"render_node_count"`
CurrentActiveNodes int `json:"current_active_nodes"`
Region *string `json:"region,omitempty"`
TellMeWhenDone bool `json:"tell_me_when_done"`
RetryCount int `json:"retry_count"`
MaxRetries int `json:"max_retries"`
RepairAttempts int `json:"repair_attempts"`
FailedMessage *string `json:"failed_message,omitempty"`
FailedAtStep *string `json:"failed_at_step,omitempty"`
ExportID *uuid.UUID `json:"export_id,omitempty"`
TaskStartDate time.Time `json:"task_start_date"`
QueuedAt time.Time `json:"queued_at"`
StartedAt *time.Time `json:"started_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Admin-list enrichment (set by the handler via a cross-schema lookup, not scanned).
UserName string `json:"user_name,omitempty"`
UserEmail string `json:"user_email,omitempty"`
}
type FrameJob struct {
ID uuid.UUID `json:"id"`
RenderJobID uuid.UUID `json:"render_job_id"`
NodeID uuid.UUID `json:"node_id"`
StartFrame int `json:"start_frame"`
EndFrame int `json:"end_frame"`
CollectFrameCount int `json:"collect_frame_count"`
OrderValue int `json:"order_value"`
FolderName string `json:"folder_name"`
ConvertURL *string `json:"convert_url,omitempty"`
Status string `json:"status"`
FramesRendered int `json:"frames_rendered"`
FramesValidated int `json:"frames_validated"`
Attempt int `json:"attempt"`
LastError *string `json:"last_error,omitempty"`
OutputMP4URL *string `json:"output_mp4_url,omitempty"`
AssignedAt time.Time `json:"assigned_at"`
StartedAt *time.Time `json:"started_at,omitempty"`
LastProgressAt *time.Time `json:"last_progress_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Snapshot struct {
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenant_id"`
UserID uuid.UUID `json:"user_id"`
SavedProjectID uuid.UUID `json:"saved_project_id"`
SceneKey string `json:"scene_key"`
FrameNumber int `json:"frame_number"`
InputsHash string `json:"inputs_hash"`
Status string `json:"status"`
RenderNodeID *uuid.UUID `json:"render_node_id,omitempty"`
ImageURL *string `json:"image_url,omitempty"`
ThumbnailURL *string `json:"thumbnail_url,omitempty"`
Width *int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
SizeBytes *int64 `json:"size_bytes,omitempty"`
RequestedAt time.Time `json:"requested_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
DurationMS *int `json:"duration_ms,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
ErrorMessage *string `json:"error_message,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type Export struct {
ID uuid.UUID `json:"id"`
TenantID uuid.UUID `json:"tenant_id"`
UserID uuid.UUID `json:"user_id"`
SavedProjectID uuid.UUID `json:"saved_project_id"`
ProjectID uuid.UUID `json:"project_id"`
RenderJobID *uuid.UUID `json:"render_job_id,omitempty"`
Image *string `json:"image,omitempty"`
Path string `json:"path"`
FileExtension string `json:"file_extension"`
FileType string `json:"file_type"`
RenderQuality string `json:"render_quality"`
CreateType string `json:"create_type"`
SizeBytes int64 `json:"size_bytes"`
DurationSec *float64 `json:"duration_sec,omitempty"`
Width *int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
ProduceDate time.Time `json:"produce_date"`
AutoDeleteDate time.Time `json:"auto_delete_date"`
DeleteNotified bool `json:"delete_notified"`
CreatedAt time.Time `json:"created_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
type ExportFile struct {
ID uuid.UUID `json:"id"`
ExportID uuid.UUID `json:"export_id"`
UserID uuid.UUID `json:"user_id"`
Name *string `json:"name,omitempty"`
Thumbnail *string `json:"thumbnail,omitempty"`
Path string `json:"path"`
SizeBytes int64 `json:"size_bytes"`
FileType string `json:"file_type"`
Width *int `json:"width,omitempty"`
Height *int `json:"height,omitempty"`
Sort int `json:"sort"`
CreatedAt time.Time `json:"created_at"`
}
type NodeHealthLog struct {
ID int64 `json:"id"`
NodeID uuid.UUID `json:"node_id"`
RecordedAt time.Time `json:"recorded_at"`
Status string `json:"status"`
CPUPct *int `json:"cpu_pct,omitempty"`
RAMAvailableMB *int `json:"ram_available_mb,omitempty"`
AERunning *bool `json:"ae_running,omitempty"`
CurrentJobID *uuid.UUID `json:"current_job_id,omitempty"`
CurrentFrame *int `json:"current_frame,omitempty"`
CacheUsedGB *int `json:"cache_used_gb,omitempty"`
}
type NodeCrash struct {
ID uuid.UUID `json:"id"`
NodeID uuid.UUID `json:"node_id"`
RenderJobID *uuid.UUID `json:"render_job_id,omitempty"`
FrameJobID *uuid.UUID `json:"frame_job_id,omitempty"`
CrashedAt time.Time `json:"crashed_at"`
LastKnownFrame *int `json:"last_known_frame,omitempty"`
CrashSignal *string `json:"crash_signal,omitempty"`
ErrorLog *string `json:"error_log,omitempty"`
LogFileURL *string `json:"log_file_url,omitempty"`
AutoRecovered bool `json:"auto_recovered"`
RecoveryAction *string `json:"recovery_action,omitempty"`
RecoveredAt *time.Time `json:"recovered_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type NodeUpdate struct {
ID uuid.UUID `json:"id"`
UpdateFileName string `json:"update_file_name"`
UpdateNumber int `json:"update_number"`
Description *string `json:"description,omitempty"`
TargetAEVersion *string `json:"target_ae_version,omitempty"`
InUpdateQueue bool `json:"in_update_queue"`
RolledOutToNodeIDs []uuid.UUID `json:"rolled_out_to_node_ids"`
LastUpdateQueueDate *time.Time `json:"last_update_queue_date,omitempty"`
CreateDate time.Time `json:"create_date"`
CreatedAt time.Time `json:"created_at"`
}
// ── Request / Response types ─────────────────────────────────────────────────
type PagedResponse[T any] struct {
Data []T `json:"data"`
Meta PaginationMeta `json:"meta"`
}
type PaginationMeta struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
HasMore bool `json:"has_more"`
}
type RenderJobCreateRequest struct {
SavedProjectID uuid.UUID `json:"saved_project_id" binding:"required"`
Quality string `json:"quality" binding:"required"`
Resolution string `json:"resolution" binding:"required"`
FrameRate *int `json:"frame_rate"`
Is60FPS *bool `json:"is_60_fps"`
PriceType *string `json:"price_type"`
DiscountCode *string `json:"discount_code"`
SupportFlatrender *bool `json:"support_flatrender"`
TellMeWhenDone *bool `json:"tell_me_when_done"`
PreferredRegion *string `json:"preferred_region"`
}
type SnapshotCreateRequest struct {
SavedProjectID uuid.UUID `json:"saved_project_id" binding:"required"`
SceneKey string `json:"scene_key" binding:"required"`
FrameNumber int `json:"frame_number" binding:"min=0"`
}
type NodeCreateRequest struct {
Name string `json:"name" binding:"required"`
Region string `json:"region" binding:"required"`
NodeIP string `json:"node_ip" binding:"required"`
WorkerPort int `json:"worker_port" binding:"required"`
CurrentAEVersion string `json:"current_ae_version" binding:"required"`
RamGB *int `json:"ram_gb"`
CPUCores *int `json:"cpu_cores"`
NodeKind *string `json:"node_kind"`
OwnerUserID *uuid.UUID `json:"owner_user_id"`
Priority *int `json:"priority"`
}
type NodePatchRequest struct {
Priority *int `json:"priority"`
IsActive *bool `json:"is_active"`
AcceptsNewJobs *bool `json:"accepts_new_jobs"`
NodeKind *string `json:"node_kind"`
OwnerUserID *uuid.UUID `json:"owner_user_id"`
NextMaintenanceAt *time.Time `json:"next_maintenance_at"`
MaintenanceReason *string `json:"maintenance_reason"`
}
type NodeHeartbeatRequest struct {
NodeID uuid.UUID `json:"node_id"`
Status string `json:"status"`
CPUPct *int `json:"cpu_pct"`
RAMAvailableMB *int `json:"ram_available_mb"`
DiskUsedPct *int `json:"disk_used_pct"`
DiskTotalGB *int `json:"disk_total_gb"`
AERunning *bool `json:"ae_running"`
CurrentJobID *uuid.UUID `json:"current_job_id"`
CurrentFrame *int `json:"current_frame"`
CacheUsedGB *int `json:"cache_used_gb"`
}
type NodeOnlineRequest struct {
NodeAgentVersion string `json:"node_agent_version"`
CurrentAEVersion string `json:"current_ae_version"`
AvailableAEVersions []string `json:"available_ae_versions"`
RamGB *int `json:"ram_gb"`
CPUCores *int `json:"cpu_cores"`
CacheUsedGB *int `json:"cache_used_gb"`
CachedTemplateMD5s []string `json:"cached_template_md5s"`
}
type FrameProgressRequest struct {
FrameJobID uuid.UUID `json:"frame_job_id" binding:"required"`
FrameNumber int `json:"frame_number"`
FileSizeBytes *int64 `json:"file_size_bytes"`
CompletedAt *time.Time `json:"completed_at"`
}
type CrashReportRequest struct {
NodeID uuid.UUID `json:"node_id" binding:"required"`
FrameJobID *uuid.UUID `json:"frame_job_id"`
LastKnownFrame *int `json:"last_known_frame"`
CrashSignal *string `json:"crash_signal"`
AEVersion *string `json:"ae_version"`
ErrorLogTail *string `json:"error_log_tail"`
LogFileURL *string `json:"log_file_url"`
}
type ClaimJobRequest struct {
NodeID uuid.UUID `json:"node_id" binding:"required"`
Region string `json:"region"`
}
type ClaimedJob struct {
JobID uuid.UUID `json:"job_id"`
SavedProjectID uuid.UUID `json:"saved_project_id"`
Quality string `json:"quality"`
Resolution string `json:"resolution"`
FrameRate int `json:"frame_rate"`
HasMusic bool `json:"has_music"`
HasVoiceover bool `json:"has_voiceover"`
// Engine selects the render engine: "AfterEffects" (default) or "Remotion".
// For Remotion jobs CompName is the composition id and AEPDownloadURL is empty.
Engine string `json:"engine,omitempty"`
// AEPDownloadURL is a presigned MinIO GET URL for the .aep project file
// (or .zip bundle). Valid for 2 hours. Empty when the template is not yet uploaded.
AEPDownloadURL string `json:"aep_download_url,omitempty"`
// IsBundle is true when AEPDownloadURL points to a .zip bundle (the .aep plus
// footage/fonts) that the node agent must extract before rendering.
IsBundle bool `json:"is_bundle,omitempty"`
// BundleMD5 is the stored object's ETag/MD5 — the node uses it as a cache key so
// repeated renders of the same template download + extract the bundle only once.
BundleMD5 string `json:"bundle_md5,omitempty"`
// CompName is the After Effects composition to render (-comp). From the
// template's render_aep_comp (e.g. "frfinal"). Empty → node falls back to the
// project's render queue.
CompName string `json:"comp_name,omitempty"`
// Bindings are the user's edited input values to write into the AE project before
// rendering (the render binder). Key = AE layer/footage name (frl_c{n}{t|m}{i}).
Bindings []RenderBinding `json:"bindings,omitempty"`
}
// RenderBinding is one input value the node writes into the AE project before render:
// a Text layer's source text, or a Media footage replacement.
type RenderBinding struct {
Key string `json:"key"` // AE layer/footage name, e.g. frl_c1t1 / frl_c1m1
Type string `json:"type"` // content element type (Text, Media, …)
Value string `json:"value"` // text content, or a media URL
}
// OutputUploadURLResponse is returned by POST .../output-upload-url.
type OutputUploadURLResponse struct {
ExportID uuid.UUID `json:"export_id"`
UploadURL string `json:"upload_url"`
ObjectKey string `json:"object_key"`
ExpiresAt time.Time `json:"expires_at"`
}
type CacheUpdateRequest struct {
Action string `json:"action" binding:"required"`
ProjectID *uuid.UUID `json:"project_id"`
AEPFileMD5 string `json:"aep_file_md5" binding:"required"`
FileSizeBytes *int64 `json:"file_size_bytes"`
CacheUsedGB *int `json:"cache_used_gb"`
ErrorMessage *string `json:"error_message"`
}
type ReplicaReadyRequest struct {
NodeID uuid.UUID `json:"node_id" binding:"required"`
ReplicaPath string `json:"replica_path" binding:"required"`
ReplicaMD5 *string `json:"replica_md5"`
}
type APIError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// Claims carries JWT payload
type Claims struct {
UserID uuid.UUID `json:"sub"`
TenantID uuid.UUID `json:"tenant_id"`
IsAdmin bool `json:"is_admin"`
Role string `json:"role"`
}