Coverage for src / lexigram / contracts / multimedia / types.py: 100%
199 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Value objects for the multimedia generation contracts."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from enum import Enum
7from typing import Any, Literal
10@dataclass(frozen=True)
11class MediaAsset:
12 """Result of a generation call — carries either raw bytes or a URI, never both.
14 Several v1 providers (ElevenLabs, OpenAI TTS, Stability) return raw bytes
15 in the response body with no hosted URL, so callers must check
16 :attr:`has_bytes`/:attr:`has_uri` rather than assuming a shape.
17 """
19 mime_type: str
20 provider: str
21 bytes_data: bytes | None = None
22 uri: str | None = None
23 metadata: dict[str, Any] = field(default_factory=dict)
25 @property
26 def has_bytes(self) -> bool:
27 return self.bytes_data is not None
29 @property
30 def has_uri(self) -> bool:
31 return self.uri is not None
34@dataclass(frozen=True)
35class TTSRequest:
36 text: str
37 voice: str | None = None
38 format: str = "mp3"
39 reference_audio_uri: str | None = None
40 emotion: str | None = None
41 extra: dict[str, Any] = field(default_factory=dict)
44@dataclass(frozen=True)
45class MusicRequest:
46 prompt: str
47 duration_seconds: float = 30.0
48 format: str = "mp3"
49 extra: dict[str, Any] = field(default_factory=dict)
52class VideoMode(str, Enum):
53 """Reference-input mode for a video generation request.
55 ``None`` on the request means the provider derives the mode from which
56 fields are set (see ``OpenAIVideoProvider._derive_mode``).
57 """
59 TEXT_TO_VIDEO = "text_to_video"
60 FIRST_FRAME = "first_frame"
61 FIRST_LAST_FRAME = "first_last_frame"
62 MULTIMODAL_REFERENCE = "multimodal_reference"
65@dataclass(frozen=True)
66class VideoRequest:
67 prompt: str
68 duration_seconds: float = 4.0
69 resolution: str = "1280x720"
70 image_uri: str | None = None
71 format: str = "mp4"
72 model: str | None = None
73 mode: VideoMode | None = None
74 last_frame_image: str | None = None
75 reference_images: list[str] = field(default_factory=list)
76 reference_videos: list[str] = field(default_factory=list)
77 reference_audios: list[str] = field(default_factory=list)
78 generate_audio: bool = False
79 return_last_frame: bool = False
80 ratio: str | None = None
81 seed: int | None = None
82 extra: dict[str, Any] = field(default_factory=dict)
85@dataclass(frozen=True)
86class ImageRequest:
87 prompt: str
88 width: int = 1024
89 height: int = 1024
90 format: str = "png"
91 reference_image: bytes | None = None
92 reference_mime_type: str | None = None
93 extra: dict[str, Any] = field(default_factory=dict)
96@dataclass(frozen=True)
97class InterpolationRequest:
98 frame_a: MediaAsset
99 frame_b: MediaAsset
100 extra: dict[str, Any] = field(default_factory=dict)
103@dataclass(frozen=True)
104class UpscaleRequest:
105 asset: MediaAsset
106 scale_factor: Literal[2, 4] = 4
107 extra: dict[str, Any] = field(default_factory=dict)
110OverlayPosition = Literal[
111 "top",
112 "bottom",
113 "center",
114 "top-left",
115 "top-right",
116 "bottom-left",
117 "bottom-right",
118]
121@dataclass(frozen=True)
122class TransitionSpec:
123 kind: Literal["cut", "crossfade"]
124 duration: float = 0.5
127@dataclass(frozen=True)
128class SubtitleCue:
129 start: float
130 end: float
131 text: str
134@dataclass(frozen=True)
135class Trim:
136 asset: MediaAsset
137 start: float
138 end: float
141@dataclass(frozen=True)
142class Concat:
143 assets: list[MediaAsset]
144 transitions: list[TransitionSpec] | None = None
147@dataclass(frozen=True)
148class OverlayText:
149 asset: MediaAsset
150 text: str
151 position: OverlayPosition
152 start: float | None = None
153 end: float | None = None
154 font_size: int = 32
155 color: str = "white"
158@dataclass(frozen=True)
159class OverlayImage:
160 asset: MediaAsset
161 image_asset: MediaAsset
162 position: OverlayPosition
163 opacity: float = 1.0
164 start: float | None = None
165 end: float | None = None
168@dataclass(frozen=True)
169class ComposeLayer:
170 """A full-canvas transparent overlay placed on the base at a time offset.
172 `start`/`end` are seconds on the composed timeline. `fade_in`/`fade_out`
173 are durations in seconds on the layer's own timeline (fade-in starts when
174 the layer appears; fade-out ends at the layer's end).
175 """
177 asset: MediaAsset
178 start: float = 0.0
179 end: float | None = None
180 fade_in: float = 0.0
181 fade_out: float = 0.0
184@dataclass(frozen=True)
185class ComposeAudioLayer:
186 """An audio input placed at a time offset on the composed timeline."""
188 asset: MediaAsset
189 start: float = 0.0
190 volume: float = 1.0
193@dataclass(frozen=True)
194class EncodeSpec:
195 """Output encoding parameters for the finished composition."""
197 codec: str = "libx264"
198 bitrate: str | None = None
199 resolution: str | None = None
200 fps: int | None = None
203@dataclass(frozen=True)
204class ComposeVideo:
205 """Compose a base video with timed overlays, fades, and audio layers.
207 Semantics:
208 - Output duration == base asset duration.
209 - The base's audio is dropped unless `audio_layers` is non-empty.
210 - `fade_in`/`fade_out` fade the whole composition from/to black.
211 - `base_fade_out` fades the BASE stream to black before its end (e.g. a CTA
212 lead-in) while later overlay layers keep compositing on top.
213 - `layers` composite over the base in list order (later layers on top) and
214 are only visible within their `[start, end)` window.
215 """
217 asset: MediaAsset
218 layers: list[ComposeLayer] = field(default_factory=list)
219 audio_layers: list[ComposeAudioLayer] = field(default_factory=list)
220 fade_in: float = 0.0
221 fade_out: float = 0.0
222 base_fade_out: float = 0.0
223 encode: EncodeSpec | None = None
226@dataclass(frozen=True)
227class BeatAnalysisRequest:
228 asset: MediaAsset
229 extra: dict[str, Any] = field(default_factory=dict)
232@dataclass(frozen=True)
233class BeatAnalysisResult:
234 tempo_bpm: float
235 beat_timestamps: list[float]
238@dataclass(frozen=True)
239class BurnSubtitles:
240 asset: MediaAsset
241 cues: list[SubtitleCue]
244@dataclass(frozen=True)
245class MuxAudio:
246 asset: MediaAsset
247 audio_asset: MediaAsset
248 mode: Literal["replace", "mix"]
249 music_volume: float = 1.0
250 duck_under_existing: bool = False
253@dataclass(frozen=True)
254class ExtractThumbnail:
255 asset: MediaAsset
256 timestamp: float
259@dataclass(frozen=True)
260class ToGif:
261 asset: MediaAsset
262 start: float | None = None
263 end: float | None = None
264 fps: int = 10
265 width: int = 480
268@dataclass(frozen=True)
269class Transcode:
270 asset: MediaAsset
271 format: str
272 codec: str | None = None
273 resolution: str | None = None
274 bitrate: str | None = None
277@dataclass(frozen=True)
278class ChangeSpeed:
279 asset: MediaAsset
280 factor: float
283@dataclass(frozen=True)
284class Crop:
285 asset: MediaAsset
286 x: int
287 y: int
288 width: int
289 height: int
292@dataclass(frozen=True)
293class ColorFilter:
294 asset: MediaAsset
295 preset: Literal["none", "grayscale", "sepia", "vintage"] | None = None
296 brightness: float = 0.0
297 contrast: float = 1.0
298 saturation: float = 1.0
301@dataclass(frozen=True)
302class RawFilter:
303 assets: list[MediaAsset]
304 filter_complex: str
305 maps: list[str]
306 extra_args: list[str] = field(default_factory=list)
309VideoOperation = (
310 Trim
311 | Concat
312 | OverlayText
313 | OverlayImage
314 | ComposeVideo
315 | BurnSubtitles
316 | MuxAudio
317 | ExtractThumbnail
318 | ToGif
319 | Transcode
320 | ChangeSpeed
321 | Crop
322 | ColorFilter
323 | RawFilter
324)
327__all__ = [
328 "BeatAnalysisRequest",
329 "BeatAnalysisResult",
330 "BurnSubtitles",
331 "ChangeSpeed",
332 "ColorFilter",
333 "ComposeAudioLayer",
334 "ComposeLayer",
335 "ComposeVideo",
336 "Concat",
337 "Crop",
338 "EncodeSpec",
339 "ExtractThumbnail",
340 "ImageRequest",
341 "InterpolationRequest",
342 "MediaAsset",
343 "MusicRequest",
344 "MuxAudio",
345 "OverlayImage",
346 "OverlayPosition",
347 "OverlayText",
348 "RawFilter",
349 "SubtitleCue",
350 "TTSRequest",
351 "ToGif",
352 "Transcode",
353 "TransitionSpec",
354 "Trim",
355 "UpscaleRequest",
356 "VideoMode",
357 "VideoOperation",
358 "VideoRequest",
359]