acd23 commited on
Commit
4f2a326
·
verified ·
1 Parent(s): 21a70bc

Upload src/models/schemas.py

Browse files
Files changed (1) hide show
  1. src/models/schemas.py +245 -332
src/models/schemas.py CHANGED
@@ -1,25 +1,18 @@
1
  """
2
- Pydantic models for AI Reel Creator Platform.
3
- Defines all data schemas for the end-to-end pipeline.
 
 
 
4
  """
5
 
6
- from typing import Optional, List, Dict, Any, Literal
7
- from pydantic import BaseModel, Field, UUID4
8
- from datetime import datetime
9
  from enum import Enum
 
 
10
 
11
 
12
- # ============================================================
13
- # ENUMS
14
- # ============================================================
15
-
16
- class AssetType(str, Enum):
17
- VIDEO = "video"
18
- IMAGE = "image"
19
- AUDIO = "audio"
20
-
21
-
22
- class SectionType(str, Enum):
23
  EXTERIOR = "Exterior"
24
  INTERIOR = "Interior"
25
  PERFORMANCE = "Performance"
@@ -41,6 +34,53 @@ class Tone(str, Enum):
41
  SERENE = "serene"
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  class ShotType(str, Enum):
45
  CLOSE_UP = "close_up"
46
  MEDIUM_SHOT = "medium_shot"
@@ -53,7 +93,7 @@ class ShotType(str, Enum):
53
  PAN = "pan"
54
  TILT = "tilt"
55
  DRONE = "drone"
56
- THREE_SIXTY = "360"
57
 
58
 
59
  class CameraAngle(str, Enum):
@@ -69,38 +109,34 @@ class CameraAngle(str, Enum):
69
  DETAIL = "detail"
70
 
71
 
72
- class Platform(str, Enum):
73
- INSTAGRAM_REELS = "instagram_reels"
74
- TIKTOK = "tiktok"
75
- YOUTUBE_SHORTS = "youtube_shorts"
76
- LINKEDIN = "linkedin"
77
- TWITTER = "twitter"
78
- FACEBOOK = "facebook"
79
- CUSTOM = "custom"
80
 
81
 
82
- class DurationTarget(str, Enum):
83
- TEN_SECONDS = "10s"
84
- TWENTY_SECONDS = "20s"
85
- THIRTY_SECONDS = "30s"
86
- SIXTY_SECONDS = "60s"
87
- CUSTOM = "custom"
88
 
89
 
90
- class AspectRatio(str, Enum):
91
- NINE_SIXTEEN = "9:16"
92
- SIXTEEN_NINE = "16:9"
93
- ONE_ONE = "1:1"
94
- FOUR_FIVE = "4:5"
95
 
96
 
97
- class TransitionType(str, Enum):
98
- CUT = "cut"
99
- CROSS_DISSOLVE = "cross_dissolve"
100
- FLASH_WHITE = "flash_white"
101
- ZOOM_IN = "zoom_in"
102
- SLIDE_LEFT = "slide_left"
103
- SLIDE_RIGHT = "slide_right"
104
 
105
 
106
  class DurationClass(str, Enum):
@@ -109,41 +145,46 @@ class DurationClass(str, Enum):
109
  LONG = "long"
110
 
111
 
112
- class MappingType(str, Enum):
113
- SEMANTIC = "semantic"
114
- RULE_BASED = "rule_based"
115
- MANUAL = "manual"
116
- OVERRIDE = "override"
117
-
118
-
119
- class RequestStatus(str, Enum):
120
- PENDING = "pending"
121
- PLANNING = "planning"
122
- GENERATING = "generating"
123
- REVIEW = "review"
124
- COMPLETED = "completed"
125
- FAILED = "failed"
 
 
126
 
 
 
 
127
 
128
- # ============================================================
129
- # ASSET MODELS
130
- # ============================================================
131
 
132
- class AssetMetadata(BaseModel):
133
- """Rich metadata for an asset extracted by vision models."""
134
- description: str = Field(description="Natural language description of the asset")
135
- shot_type: Optional[ShotType] = Field(None, description="Camera shot type")
136
- camera_angle: Optional[CameraAngle] = Field(None, description="Camera angle")
137
- subject_part: Optional[str] = Field(None, description="Subject part shown (e.g., 'headlight', 'wheel')")
138
- mood: Optional[Tone] = Field(None, description="Visual mood/tone")
139
- dominant_colours: Optional[List[str]] = Field(None, description="Dominant hex colour codes")
140
- confidence_score: float = Field(default=1.0, ge=0.0, le=1.0, description="Extraction confidence")
141
- review_flag: bool = Field(default=False, description="Flag for manual review")
 
 
 
 
142
 
143
 
144
- class Asset(BaseModel):
145
- """Represents a raw video, image, or audio asset."""
146
- id: Optional[UUID4] = None
147
  file_path: str
148
  file_name: str
149
  asset_type: AssetType
@@ -152,15 +193,14 @@ class Asset(BaseModel):
152
  duration_ms: Optional[int] = None
153
  frame_rate: Optional[float] = None
154
  file_size_bytes: Optional[int] = None
155
- metadata: Optional[AssetMetadata] = None
156
  created_at: Optional[datetime] = None
157
  updated_at: Optional[datetime] = None
158
 
159
 
160
- class VideoEvent(BaseModel):
161
- """A temporal segment within a video asset."""
162
- id: Optional[UUID4] = None
163
- asset_id: UUID4
164
  start_ms: int
165
  end_ms: int
166
  duration_ms: Optional[int] = None
@@ -169,101 +209,32 @@ class VideoEvent(BaseModel):
169
  camera_angle: Optional[CameraAngle] = None
170
  subject_part: Optional[str] = None
171
  mood: Optional[Tone] = None
172
- confidence_score: Optional[float] = None
 
173
  keyframe_path: Optional[str] = None
174
  created_at: Optional[datetime] = None
175
 
176
 
177
- # ============================================================
178
- # BRAND CONFIG MODELS
179
- # ============================================================
180
-
181
- class BrandColours(BaseModel):
182
- primary: str
183
- secondary: str
184
- accent: Optional[str] = None
185
- background: Optional[str] = None
186
- text: Optional[str] = None
187
-
188
-
189
- class BrandTypography(BaseModel):
190
- primary_font: str
191
- secondary_font: Optional[str] = None
192
- heading_size: Optional[str] = None
193
- body_size: Optional[str] = None
194
- caption_size: Optional[str] = None
195
-
196
-
197
- class BrandToneOfVoice(BaseModel):
198
- adjectives: List[str]
199
- forbidden_words: Optional[List[str]] = None
200
- preferred_phrases: Optional[List[str]] = None
201
-
202
-
203
- class BrandConfig(BaseModel):
204
- """Structured brand guidelines for compliance checking."""
205
- id: Optional[UUID4] = None
206
  name: str
207
  version: int = 1
208
- colours: BrandColours
209
- typography: BrandTypography
210
- tone_of_voice: BrandToneOfVoice
211
- approved_terminology: Optional[List[str]] = None
212
- restricted_visuals: Optional[List[str]] = None
213
  logo_paths: Optional[Dict[str, str]] = None
214
  created_at: Optional[datetime] = None
215
  updated_at: Optional[datetime] = None
216
 
217
 
218
- # ============================================================
219
- # BROCHURE MODELS
220
- # ============================================================
221
-
222
- class BrochureNode(BaseModel):
223
- """A knowledge node extracted from a brochure section."""
224
- id: Optional[UUID4] = None
225
- section: SectionType
226
- title: str
227
- content: str
228
- key_features: Optional[List[str]] = Field(default_factory=list)
229
- taglines: Optional[List[str]] = Field(default_factory=list)
230
- spec_highlights: Optional[Dict[str, Any]] = None
231
- car_part_referenced: Optional[List[str]] = Field(default_factory=list)
232
- tone_tags: Optional[List[Tone]] = Field(default_factory=list)
233
- page_number: Optional[int] = None
234
- source_pdf: Optional[str] = None
235
- embedding_768: Optional[List[float]] = None
236
- created_at: Optional[datetime] = None
237
- updated_at: Optional[datetime] = None
238
-
239
-
240
- class BrochureAssetMap(BaseModel):
241
- """Mapping between a brochure node and visual assets."""
242
- id: Optional[UUID4] = None
243
- brochure_node_id: UUID4
244
- asset_id: UUID4
245
- video_event_id: Optional[UUID4] = None
246
- similarity_score: float = Field(ge=0.0, le=1.0)
247
- mapping_type: MappingType = MappingType.SEMANTIC
248
- confidence_score: float
249
- is_approved: Optional[bool] = None
250
- reviewer_notes: Optional[str] = None
251
- rank: int
252
- created_at: Optional[datetime] = None
253
- updated_at: Optional[datetime] = None
254
-
255
-
256
- # ============================================================
257
- # CAPTION & VOICEOVER MODELS
258
- # ============================================================
259
-
260
- class CaptionVariant(BaseModel):
261
- """A pre-generated caption variant."""
262
- id: Optional[UUID4] = None
263
- brochure_node_id: Optional[UUID4] = None
264
  car_part: Optional[str] = None
265
  tone: Tone
266
- duration_class: DurationClass
267
  text: str
268
  word_count: Optional[int] = None
269
  is_brand_compliant: bool = True
@@ -272,13 +243,12 @@ class CaptionVariant(BaseModel):
272
  created_at: Optional[datetime] = None
273
 
274
 
275
- class VoiceoverLine(BaseModel):
276
- """A pre-generated voiceover script line."""
277
- id: Optional[UUID4] = None
278
- brochure_node_id: Optional[UUID4] = None
279
  car_part: Optional[str] = None
280
  tone: Tone
281
- duration_class: DurationClass
282
  text: str
283
  estimated_duration_ms: Optional[int] = None
284
  is_brand_compliant: bool = True
@@ -287,207 +257,150 @@ class VoiceoverLine(BaseModel):
287
  created_at: Optional[datetime] = None
288
 
289
 
290
- # ============================================================
291
- # QUERY & REEL REQUEST MODELS
292
- # ============================================================
 
 
 
 
 
 
 
293
 
294
- class ReelRequest(BaseModel):
295
- """User request for a reel generation."""
296
- id: Optional[UUID4] = None
297
- user_query: str = Field(description="Free-text description of desired reel")
298
  duration_target: DurationTarget
299
- duration_ms: Optional[int] = None
300
  platform: Platform
301
  tone: Tone
302
- aspect_ratio: AspectRatio = AspectRatio.NINE_SIXTEEN
303
- brand_config_id: Optional[UUID4] = None
304
- additional_constraints: Optional[Dict[str, Any]] = None
305
- status: RequestStatus = RequestStatus.PENDING
306
  error_message: Optional[str] = None
307
  created_at: Optional[datetime] = None
308
  updated_at: Optional[datetime] = None
309
 
310
 
311
- class BeatDefinition(BaseModel):
312
- """A single beat in a reel script."""
313
- beat_number: int
314
- duration_ms: int
315
- intent: str = Field(description="Creative intent for this beat")
316
- car_parts: Optional[List[str]] = Field(default_factory=list)
317
  tone: Optional[Tone] = None
318
  caption_variant: Optional[str] = None
319
  voiceover_line: Optional[str] = None
320
  transition_type: Optional[TransitionType] = TransitionType.CUT
321
- filter_suggestion: Optional[str] = None
322
- start_ms: Optional[int] = None # Will be computed during assembly
323
- end_ms: Optional[int] = None
324
 
325
 
326
- class ReelScript(BaseModel):
327
- """Beat-by-beat reel script generated by the LLM orchestrator."""
328
- id: Optional[UUID4] = None
329
- reel_request_id: UUID4
330
- title: Optional[str] = None
331
- beats: List[BeatDefinition]
332
- total_duration_ms: int
333
- total_beats: int
334
- platform: Optional[Platform] = None
335
- aspect_ratio: Optional[AspectRatio] = None
336
- filter_preset: Optional[str] = None
337
- voiceover_script: Optional[str] = None
338
- validation_status: str = "pending"
339
- validation_errors: Optional[List[str]] = None
340
- generation_attempts: int = 1
341
- created_at: Optional[datetime] = None
342
 
 
 
 
 
 
 
 
343
 
344
- # ============================================================
345
- # ASSET CANDIDATE MODELS
346
- # ============================================================
347
 
348
- class AssetCandidate(BaseModel):
349
- """A candidate asset for a specific beat."""
350
- asset_id: UUID4
351
- video_event_id: Optional[UUID4] = None
352
- file_path: str
353
- asset_type: AssetType
354
- description: Optional[str] = None
355
- shot_type: Optional[ShotType] = None
356
- camera_angle: Optional[CameraAngle] = None
357
- subject_part: Optional[str] = None
358
- mood: Optional[Tone] = None
359
- similarity_score: float
360
- rank: int
361
- mapping_source: str = "semantic"
362
  start_ms: Optional[int] = None
363
  end_ms: Optional[int] = None
364
- is_selected: bool = False
365
-
366
-
367
- class BeatWithCandidates(BaseModel):
368
- """A beat enriched with top-k asset candidates."""
369
- beat: BeatDefinition
370
- candidates: List[AssetCandidate]
371
- selected_candidate_index: Optional[int] = None
372
- caption: Optional[str] = None
373
- voiceover: Optional[str] = None
374
-
375
-
376
- # ============================================================
377
- # REEL MANIFEST MODELS
378
- # ============================================================
379
-
380
- class BeatManifest(BaseModel):
381
- """A fully-resolved beat ready for rendering."""
382
- beat_number: int
383
- asset_id: UUID4
384
- video_event_id: Optional[UUID4] = None
385
- file_path: str
386
- asset_type: AssetType
387
- start_ms: int # Start time within the source asset
388
- end_ms: int # End time within the source asset
389
- duration_ms: int # Target duration for this beat
390
  caption: Optional[str] = None
391
  voiceover_line: Optional[str] = None
392
- voiceover_start_ms: Optional[int] = None
393
- transition_type: TransitionType = TransitionType.CUT
394
- filter_preset: Optional[str] = None
395
- speed_adjustment: Optional[float] = Field(None, description="Speed multiplier if clip needs adjustment")
396
- description: Optional[str] = None
397
-
398
-
399
- class AudioTrack(BaseModel):
400
- """Audio track definition for the reel."""
401
- voiceover_segments: List[Dict[str, Any]] # {start_ms, end_ms, text, audio_file}
402
- background_music_path: Optional[str] = None
403
- background_music_volume: float = 0.2
404
- fade_in_ms: int = 500
405
- fade_out_ms: int = 1000
406
 
407
 
408
- class ReelManifest(BaseModel):
409
- """Complete reel manifest for the composition engine."""
410
- id: Optional[UUID4] = None
411
- reel_request_id: UUID4
412
- title: Optional[str] = None
 
413
  platform: Platform
 
414
  aspect_ratio: AspectRatio
415
- total_duration_ms: int
416
- total_beats: int
417
- beats: List[BeatManifest]
418
- audio_track: Optional[AudioTrack] = None
419
- filter_preset: Optional[str] = None
420
- brand_config_id: Optional[UUID4] = None
421
  is_validated: bool = False
422
  validation_report: Optional[Dict[str, Any]] = None
423
- created_at: Optional[datetime] = None
 
 
424
 
425
 
426
- # ============================================================
427
- # API REQUEST/RESPONSE MODELS
428
- # ============================================================
429
-
430
- class ReelQueryRequest(BaseModel):
431
- """API request for creating a new reel."""
432
- user_query: str = Field(..., min_length=5, max_length=2000, description="Description of desired reel")
433
- duration_target: DurationTarget = DurationTarget.TWENTY_SECONDS
434
- platform: Platform = Platform.INSTAGRAM_REELS
435
- tone: Tone = Tone.CINEMATIC if False else Tone.DYNAMIC # default
436
- aspect_ratio: Optional[AspectRatio] = None
437
- brand_config_id: Optional[str] = None
438
- additional_constraints: Optional[Dict[str, Any]] = None
439
-
440
-
441
- class ReelQueryResponse(BaseModel):
442
- """API response with reel generation status."""
443
- request_id: str
444
- status: RequestStatus
445
- reel_script: Optional[ReelScript] = None
446
- reel_manifest: Optional[ReelManifest] = None
447
- message: Optional[str] = None
448
- estimated_completion_seconds: Optional[int] = None
449
-
450
-
451
- class ReelManifestResponse(BaseModel):
452
- """API response for a completed reel manifest."""
453
- request_id: str
454
- manifest: ReelManifest
455
- preview_url: Optional[str] = None
456
- render_status: str = "pending"
457
-
458
-
459
- class AssetSwapRequest(BaseModel):
460
- """Request to swap an asset for a specific beat."""
461
- request_id: str
462
  beat_number: int
463
- new_asset_id: str
464
- new_video_event_id: Optional[str] = None
465
-
466
-
467
- class RegenerateRequest(BaseModel):
468
- """Request to regenerate a reel with modified parameters."""
469
- request_id: str
470
- user_query: Optional[str] = None
471
  tone: Optional[Tone] = None
472
- duration_target: Optional[DurationTarget] = None
473
- additional_constraints: Optional[Dict[str, Any]] = None
 
 
 
 
474
 
475
 
476
- # ============================================================
477
- # VALIDATION & ERROR MODELS
478
- # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
 
480
- class ValidationResult(BaseModel):
481
- """Result of validating a reel script or manifest."""
482
- is_valid: bool
483
- errors: List[str] = Field(default_factory=list)
484
- warnings: List[str] = Field(default_factory=list)
485
- suggestions: List[str] = Field(default_factory=list)
486
 
487
 
488
- class PipelineError(BaseModel):
489
- """Standard error response from the API."""
490
- error_code: str
491
- message: str
492
- details: Optional[Dict[str, Any]] = None
493
- timestamp: datetime = Field(default_factory=datetime.utcnow)
 
1
  """
2
+ Pydantic Schemas -- AI Reel Creator Platform
3
+ =============================================
4
+
5
+ Defines all request / response / internal data schemas used across the API,
6
+ pipelines, and LLM orchestrator.
7
  """
8
 
9
+ from typing import List, Dict, Optional, Any, Literal
 
 
10
  from enum import Enum
11
+ from datetime import datetime
12
+ from pydantic import BaseModel, Field, validator
13
 
14
 
15
+ class Section(str, Enum):
 
 
 
 
 
 
 
 
 
 
16
  EXTERIOR = "Exterior"
17
  INTERIOR = "Interior"
18
  PERFORMANCE = "Performance"
 
34
  SERENE = "serene"
35
 
36
 
37
+ class DurationTarget(str, Enum):
38
+ S10 = "10s"
39
+ S20 = "20s"
40
+ S30 = "30s"
41
+ S60 = "60s"
42
+ CUSTOM = "custom"
43
+
44
+
45
+ class Platform(str, Enum):
46
+ INSTAGRAM_REELS = "instagram_reels"
47
+ TIKTOK = "tiktok"
48
+ YOUTUBE_SHORTS = "youtube_shorts"
49
+ LINKEDIN = "linkedin"
50
+ TWITTER = "twitter"
51
+ FACEBOOK = "facebook"
52
+ CUSTOM = "custom"
53
+
54
+
55
+ class AspectRatio(str, Enum):
56
+ R9_16 = "9:16"
57
+ R16_9 = "16:9"
58
+ R1_1 = "1:1"
59
+ R4_5 = "4:5"
60
+
61
+
62
+ class TransitionType(str, Enum):
63
+ CUT = "cut"
64
+ CROSS_DISSOLVE = "cross_dissolve"
65
+ FLASH_WHITE = "flash_white"
66
+ ZOOM_IN = "zoom_in"
67
+ SLIDE_LEFT = "slide_left"
68
+ SLIDE_RIGHT = "slide_right"
69
+
70
+
71
+ class FilterPreset(str, Enum):
72
+ CINEMATIC = "cinematic"
73
+ SPORTY = "sporty"
74
+ ELEGANT = "elegant"
75
+ TECHNICAL = "technical"
76
+
77
+
78
+ class AssetType(str, Enum):
79
+ VIDEO = "video"
80
+ IMAGE = "image"
81
+ AUDIO = "audio"
82
+
83
+
84
  class ShotType(str, Enum):
85
  CLOSE_UP = "close_up"
86
  MEDIUM_SHOT = "medium_shot"
 
93
  PAN = "pan"
94
  TILT = "tilt"
95
  DRONE = "drone"
96
+ R360 = "360"
97
 
98
 
99
  class CameraAngle(str, Enum):
 
109
  DETAIL = "detail"
110
 
111
 
112
+ class ReelStatus(str, Enum):
113
+ PENDING = "pending"
114
+ PLANNING = "planning"
115
+ GENERATING = "generating"
116
+ REVIEW = "review"
117
+ COMPLETED = "completed"
118
+ FAILED = "failed"
 
119
 
120
 
121
+ class RenderStatus(str, Enum):
122
+ PENDING = "pending"
123
+ RENDERING = "rendering"
124
+ COMPLETED = "completed"
125
+ FAILED = "failed"
 
126
 
127
 
128
+ class MappingType(str, Enum):
129
+ SEMANTIC = "semantic"
130
+ RULE_BASED = "rule_based"
131
+ MANUAL = "manual"
132
+ OVERRIDE = "override"
133
 
134
 
135
+ class MappingSource(str, Enum):
136
+ SEMANTIC = "semantic"
137
+ BROCHURE_MAP = "brochure_map"
138
+ RULE_BASED = "rule_based"
139
+ FALLBACK = "fallback"
 
 
140
 
141
 
142
  class DurationClass(str, Enum):
 
145
  LONG = "long"
146
 
147
 
148
+ class BrochureNodeSchema(BaseModel):
149
+ id: Optional[str] = None
150
+ section: Section
151
+ title: str = Field(..., max_length=200)
152
+ content: str = Field(..., min_length=20)
153
+ key_features: List[str] = Field(default_factory=list)
154
+ taglines: List[str] = Field(default_factory=list)
155
+ spec_highlights: Dict[str, str] = Field(default_factory=dict)
156
+ car_part_referenced: List[str] = Field(default_factory=list)
157
+ tone_tags: List[Tone] = Field(default_factory=list)
158
+ page_numbers: List[int] = Field(default_factory=list)
159
+ embedding_768: Optional[List[float]] = None
160
+ embedding_model: str = "openai/clip-vit-large-patch14"
161
+ source_pdf: Optional[str] = None
162
+ created_at: Optional[datetime] = None
163
+ updated_at: Optional[datetime] = None
164
 
165
+ @validator("car_part_referenced", each_item=True)
166
+ def lower_parts(cls, v: str) -> str:
167
+ return v.lower().strip().rstrip("s")
168
 
 
 
 
169
 
170
+ class AssetMetadataSchema(BaseModel):
171
+ id: Optional[str] = None
172
+ asset_id: str
173
+ description: Optional[str] = None
174
+ shot_type: Optional[ShotType] = None
175
+ camera_angle: Optional[CameraAngle] = None
176
+ subject_part: Optional[str] = None
177
+ mood: Optional[Tone] = None
178
+ dominant_colours: Optional[List[str]] = None
179
+ confidence_score: Optional[float] = Field(None, ge=0.0, le=1.0)
180
+ review_flag: bool = False
181
+ embedding_768: Optional[List[float]] = None
182
+ embedding_model: str = "openai/clip-vit-large-patch14"
183
+ extracted_at: Optional[datetime] = None
184
 
185
 
186
+ class AssetSchema(BaseModel):
187
+ id: Optional[str] = None
 
188
  file_path: str
189
  file_name: str
190
  asset_type: AssetType
 
193
  duration_ms: Optional[int] = None
194
  frame_rate: Optional[float] = None
195
  file_size_bytes: Optional[int] = None
196
+ metadata: Optional[AssetMetadataSchema] = None
197
  created_at: Optional[datetime] = None
198
  updated_at: Optional[datetime] = None
199
 
200
 
201
+ class VideoEventSchema(BaseModel):
202
+ id: Optional[str] = None
203
+ asset_id: str
 
204
  start_ms: int
205
  end_ms: int
206
  duration_ms: Optional[int] = None
 
209
  camera_angle: Optional[CameraAngle] = None
210
  subject_part: Optional[str] = None
211
  mood: Optional[Tone] = None
212
+ embedding_768: Optional[List[float]] = None
213
+ confidence_score: Optional[float] = Field(None, ge=0.0, le=1.0)
214
  keyframe_path: Optional[str] = None
215
  created_at: Optional[datetime] = None
216
 
217
 
218
+ class BrandConfigSchema(BaseModel):
219
+ id: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  name: str
221
  version: int = 1
222
+ colours: Dict[str, str] = Field(default_factory=dict)
223
+ typography: Dict[str, str] = Field(default_factory=dict)
224
+ tone_of_voice: Dict[str, Any] = Field(default_factory=dict)
225
+ approved_terminology: List[str] = Field(default_factory=list)
226
+ restricted_visuals: List[str] = Field(default_factory=list)
227
  logo_paths: Optional[Dict[str, str]] = None
228
  created_at: Optional[datetime] = None
229
  updated_at: Optional[datetime] = None
230
 
231
 
232
+ class CaptionSchema(BaseModel):
233
+ id: Optional[str] = None
234
+ brochure_node_id: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  car_part: Optional[str] = None
236
  tone: Tone
237
+ duration_class: DurationClass = DurationClass.MEDIUM
238
  text: str
239
  word_count: Optional[int] = None
240
  is_brand_compliant: bool = True
 
243
  created_at: Optional[datetime] = None
244
 
245
 
246
+ class VoiceoverSchema(BaseModel):
247
+ id: Optional[str] = None
248
+ brochure_node_id: Optional[str] = None
 
249
  car_part: Optional[str] = None
250
  tone: Tone
251
+ duration_class: DurationClass = DurationClass.MEDIUM
252
  text: str
253
  estimated_duration_ms: Optional[int] = None
254
  is_brand_compliant: bool = True
 
257
  created_at: Optional[datetime] = None
258
 
259
 
260
+ class ReelRequestCreate(BaseModel):
261
+ user_query: str = Field(..., min_length=5, description="Free-text description of the desired reel")
262
+ duration_target: DurationTarget
263
+ duration_ms: Optional[int] = Field(None, description="Exact duration in ms if duration_target='custom'")
264
+ platform: Platform
265
+ tone: Tone
266
+ aspect_ratio: AspectRatio = AspectRatio.R9_16
267
+ brand_config_id: Optional[str] = None
268
+ additional_constraints: Optional[Dict[str, Any]] = Field(default_factory=dict)
269
+
270
 
271
+ class ReelRequestResponse(BaseModel):
272
+ id: str
273
+ user_query: str
 
274
  duration_target: DurationTarget
275
+ duration_ms: Optional[int]
276
  platform: Platform
277
  tone: Tone
278
+ aspect_ratio: AspectRatio
279
+ brand_config_id: Optional[str]
280
+ additional_constraints: Optional[Dict[str, Any]]
281
+ status: ReelStatus
282
  error_message: Optional[str] = None
283
  created_at: Optional[datetime] = None
284
  updated_at: Optional[datetime] = None
285
 
286
 
287
+ class ReelBeatSchema(BaseModel):
288
+ beat_number: int = Field(..., ge=1)
289
+ duration_ms: int = Field(..., gt=0)
290
+ intent: str = Field(..., description="What this beat should communicate")
291
+ car_parts: List[str] = Field(default_factory=list)
 
292
  tone: Optional[Tone] = None
293
  caption_variant: Optional[str] = None
294
  voiceover_line: Optional[str] = None
295
  transition_type: Optional[TransitionType] = TransitionType.CUT
296
+ filter_suggestion: Optional[FilterPreset] = FilterPreset.CINEMATIC
 
 
297
 
298
 
299
+ class ReelScriptSchema(BaseModel):
300
+ title: str
301
+ beats: List[ReelBeatSchema] = Field(..., min_length=1)
302
+ total_duration_ms: int = Field(..., gt=0)
303
+ platform: Platform
304
+ tone: Tone
305
+ aspect_ratio: AspectRatio
306
+ voiceover_summary: Optional[str] = None
 
 
 
 
 
 
 
 
307
 
308
+ @validator("beats")
309
+ def check_total_duration(cls, beats: List[ReelBeatSchema], values: Dict[str, Any]) -> List[ReelBeatSchema]:
310
+ total = sum(b.duration_ms for b in beats)
311
+ declared = values.get("total_duration_ms", total)
312
+ if total and abs(total - declared) / max(declared, 1) > 0.10:
313
+ raise ValueError(f"Beat durations sum to {total}ms but declared total is {declared}ms")
314
+ return beats
315
 
 
 
 
316
 
317
+ class ResolvedAssetSchema(BaseModel):
318
+ beat_number: int
319
+ asset_id: str
320
+ video_event_id: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
321
  start_ms: Optional[int] = None
322
  end_ms: Optional[int] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  caption: Optional[str] = None
324
  voiceover_line: Optional[str] = None
325
+ transition: TransitionType = TransitionType.CUT
326
+ filter: FilterPreset = FilterPreset.CINEMATIC
327
+ speed_adjustment: Optional[float] = Field(None, ge=0.5, le=1.5)
 
 
 
 
 
 
 
 
 
 
 
328
 
329
 
330
+ class ReelManifestSchema(BaseModel):
331
+ reel_request_id: str
332
+ title: str
333
+ total_duration_ms: int
334
+ beat_count: int
335
+ asset_count: int
336
  platform: Platform
337
+ tone: Tone
338
  aspect_ratio: AspectRatio
339
+ brand_config_id: Optional[str] = None
340
+ beats: List[ResolvedAssetSchema] = Field(..., min_length=1)
341
+ background_music_track: Optional[str] = None
 
 
 
342
  is_validated: bool = False
343
  validation_report: Optional[Dict[str, Any]] = None
344
+ render_status: RenderStatus = RenderStatus.PENDING
345
+ render_output_path: Optional[str] = None
346
+ remotion_export_path: Optional[str] = None
347
 
348
 
349
+ class BeatAssetCandidateSchema(BaseModel):
350
+ reel_request_id: str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  beat_number: int
352
+ beat_intent: str
353
+ car_parts: List[str] = Field(default_factory=list)
 
 
 
 
 
 
354
  tone: Optional[Tone] = None
355
+ asset_id: Optional[str] = None
356
+ video_event_id: Optional[str] = None
357
+ similarity_score: Optional[float] = None
358
+ rank: int = Field(..., ge=1)
359
+ mapping_source: MappingSource = MappingSource.SEMANTIC
360
+ is_selected: bool = False
361
 
362
 
363
+ class ReelSummarySchema(BaseModel):
364
+ reel_request_id: str
365
+ title: str
366
+ duration_ms: int
367
+ beat_count: int
368
+ asset_count: int
369
+ platform: Platform
370
+ tone: Tone
371
+ aspect_ratio: AspectRatio
372
+ assets_used: List[str] = Field(default_factory=list)
373
+ brochure_facts_referenced: List[str] = Field(default_factory=list)
374
+ brand_compliance_score: Optional[float] = None
375
+ render_output_url: Optional[str] = None
376
+ remotion_download_url: Optional[str] = None
377
+
378
+
379
+ class BrochureAssetMapSchema(BaseModel):
380
+ id: Optional[str] = None
381
+ brochure_node_id: str
382
+ asset_id: str
383
+ video_event_id: Optional[str] = None
384
+ similarity_score: float = Field(..., ge=0.0, le=1.0)
385
+ mapping_type: MappingType = MappingType.SEMANTIC
386
+ confidence_score: float = Field(..., ge=0.0, le=1.0)
387
+ is_approved: Optional[bool] = None
388
+ reviewer_notes: Optional[str] = None
389
+ rank: int = Field(..., ge=1)
390
+ created_at: Optional[datetime] = None
391
+ updated_at: Optional[datetime] = None
392
+
393
 
394
+ class APIResponse(BaseModel):
395
+ success: bool
396
+ data: Optional[Any] = None
397
+ message: Optional[str] = None
398
+ error: Optional[str] = None
399
+ request_id: Optional[str] = None
400
 
401
 
402
+ class PaginatedResponse(APIResponse):
403
+ total: int = 0
404
+ page: int = 1
405
+ per_page: int = 20
406
+ has_next: bool = False