1"""Video-related document types."""
2
3from __future__ import annotations
4
5from dataclasses import dataclass
6from pathlib import Path
7from typing import TYPE_CHECKING, Any
8
9from lexigram.domain import DomainModel
10from lexigram.validation import Field, field_validator
11
12if TYPE_CHECKING:
13 from datetime import datetime
14
15 from lexigram.ai.rag.multimodal.types.audio import AudioDocument
16 from lexigram.ai.rag.multimodal.types.enums import VideoFormat
17 from lexigram.ai.rag.multimodal.types.image import ImageDocument
18
19
20@dataclass(init=False)
21class VideoMetadata(DomainModel):
22 """Metadata for video documents."""
23
24 title: str | None = None
25 description: str | None = None
26 author: str | None = None
27 created_at: datetime | None = None
28 modified_at: datetime | None = None
29 tags: list[str] = Field(default_factory=list)
30 chapters: list[dict[str, Any]] = Field(default_factory=list)
31 codec: str | None = None
32 bitrate: int | None = None
33
34 model_config = {
35 "json_schema_extra": {
36 "example": {
37 "title": "Product Demo",
38 "tags": ["demo", "product"],
39 "chapters": [{"time": 0, "title": "Introduction"}],
40 },
41 },
42 }
43
44
45@dataclass(init=False)
46class VideoDocument(DomainModel):
47 """Document representing video content."""
48
49 content: bytes | str
50 format: VideoFormat
51 duration: float = Field(gt=0)
52 fps: float = Field(gt=0)
53 width: int = Field(gt=0)
54 height: int = Field(gt=0)
55 metadata: VideoMetadata = Field(default_factory=VideoMetadata)
56 frames: list[ImageDocument] = Field(default_factory=list)
57 audio_track: AudioDocument | None = None
58 transcript: str | None = None
59 embedding: list[float] | None = None
60 file_path: Path | None = None
61
62 @field_validator("content")
63 @classmethod
64 def validate_content(cls, v: bytes | str) -> bytes | str:
65 if isinstance(v, str):
66 path = Path(v)
67 if not path.exists():
68 raise ValueError(f"File path does not exist: {v}")
69 return v
70
71 @property
72 def aspect_ratio(self) -> float:
73 return self.width / self.height
74
75 @property
76 def total_frames(self) -> int:
77 return int(self.duration * self.fps)
78
79 @property
80 def has_audio(self) -> bool:
81 return self.audio_track is not None
82
83 @property
84 def has_transcript(self) -> bool:
85 return self.transcript is not None and len(self.transcript) > 0
86
87 @property
88 def has_frames(self) -> bool:
89 return len(self.frames) > 0
90
91 @property
92 def has_embedding(self) -> bool:
93 return self.embedding is not None and len(self.embedding) > 0
94
95 model_config = {
96 "arbitrary_types_allowed": True,
97 "json_schema_extra": {
98 "example": {
99 "content": "/path/to/video.mp4",
100 "format": "mp4",
101 "duration": 300.0,
102 "fps": 30.0,
103 "width": 1920,
104 "height": 1080,
105 "metadata": {"title": "Tutorial Video"},
106 },
107 },
108 }