Skip to main content

ferrin_spec/
video_model.rs

1//! Video model interface.
2//!
3//! A video model implements the synchronous [`VideoModel::do_generate`], the
4//! asynchronous operation pair [`VideoModel::do_start`] /
5//! [`VideoModel::do_status`], or both. Capability queries
6//! (`supports_generate`, `supports_operations`, `supports_webhook`) let the
7//! core choose a flow before calling. Polling, timeouts and webhook waiting
8//! are implemented by the core, not by adapters.
9
10use std::future::Future;
11use std::sync::Arc;
12
13use serde::Deserialize;
14use serde::Serialize;
15use tokio_util::sync::CancellationToken;
16use url::Url;
17
18use crate::dynamic::BoxFuture;
19use crate::error::ProviderError;
20use crate::image_model::AspectRatio;
21use crate::image_model::ImageSize;
22use crate::json::JsonValue;
23use crate::language_model::ResponseMetadata;
24use crate::shared::FileData;
25use crate::shared::Headers;
26use crate::shared::MediaType;
27use crate::shared::ModelId;
28use crate::shared::ProviderId;
29use crate::shared::ProviderMetadata;
30use crate::shared::ProviderOptions;
31use crate::shared::Warning;
32
33/// A model that generates videos.
34pub trait VideoModel: Send + Sync + 'static {
35    /// Provider identifier.
36    fn provider(&self) -> &ProviderId;
37
38    /// Model identifier.
39    fn model_id(&self) -> &ModelId;
40
41    /// Maximum number of videos per call, or `None` when unknown (treated as 1).
42    fn max_videos_per_call(&self) -> Option<usize>;
43
44    /// Whether [`do_generate`](Self::do_generate) is implemented.
45    fn supports_generate(&self) -> bool {
46        false
47    }
48
49    /// Generates videos and waits for the result in one call.
50    fn do_generate(
51        &self,
52        options: VideoOptions,
53    ) -> impl Future<Output = Result<VideoResult, ProviderError>> + Send {
54        let _ = options;
55        std::future::ready(Err(ProviderError::unsupported(
56            "synchronous video generation",
57        )))
58    }
59
60    /// Whether [`do_start`](Self::do_start) and [`do_status`](Self::do_status)
61    /// are implemented.
62    fn supports_operations(&self) -> bool {
63        false
64    }
65
66    /// Starts an asynchronous generation operation.
67    fn do_start(
68        &self,
69        options: VideoStartOptions,
70    ) -> impl Future<Output = Result<VideoStartResult, ProviderError>> + Send {
71        let _ = options;
72        std::future::ready(Err(ProviderError::unsupported(
73            "asynchronous video generation",
74        )))
75    }
76
77    /// Queries the status of an operation returned by `do_start`.
78    fn do_status(
79        &self,
80        options: VideoStatusOptions,
81    ) -> impl Future<Output = Result<VideoStatusResult, ProviderError>> + Send {
82        let _ = options;
83        std::future::ready(Err(ProviderError::unsupported(
84            "asynchronous video generation",
85        )))
86    }
87
88    /// Whether the provider can deliver completion through a webhook.
89    fn supports_webhook(&self) -> bool {
90        false
91    }
92
93    /// Prepares a webhook for an operation.
94    ///
95    /// The default implementation invokes `factory` unchanged. Providers that
96    /// need to register the URL or wrap the payload override this method.
97    fn handle_webhook(
98        &self,
99        factory: WebhookFactory,
100    ) -> impl Future<Output = Result<WebhookHandle, ProviderError>> + Send {
101        factory()
102    }
103}
104
105/// Aspect ratio for video: a fixed ratio or provider-chosen (`adaptive`).
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
107#[serde(untagged)]
108pub enum VideoAspectRatio {
109    /// A fixed ratio such as `16:9`.
110    Ratio(AspectRatio),
111    /// Let the provider choose based on the input.
112    #[serde(with = "adaptive")]
113    Adaptive,
114}
115
116mod adaptive {
117    pub(super) fn serialize<S: serde::Serializer>(serializer: S) -> Result<S::Ok, S::Error> {
118        serializer.serialize_str("adaptive")
119    }
120
121    pub(super) fn deserialize<'de, D: serde::Deserializer<'de>>(
122        deserializer: D,
123    ) -> Result<(), D::Error> {
124        let text = <std::borrow::Cow<'de, str> as serde::Deserialize>::deserialize(deserializer)?;
125        if text == "adaptive" {
126            Ok(())
127        } else {
128            Err(serde::de::Error::custom("expected `adaptive`"))
129        }
130    }
131}
132
133/// Which frame an input image anchors.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136#[non_exhaustive]
137pub enum FrameType {
138    /// The first frame.
139    FirstFrame,
140    /// The last frame.
141    LastFrame,
142}
143
144/// An input file for video generation: inline bytes or a URL.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct VideoFile {
147    /// File payload.
148    pub data: FileData,
149    /// Media type, if known.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub media_type: Option<MediaType>,
152    /// Provider-specific options for this file.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    pub provider_options: Option<ProviderOptions>,
155}
156
157/// An input image anchored to a frame.
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct FrameImage {
160    /// The image.
161    pub image: VideoFile,
162    /// Which frame it anchors.
163    pub frame_type: FrameType,
164}
165
166/// Options for a video generation call.
167#[derive(Debug, Clone)]
168pub struct VideoOptions {
169    /// Text prompt.
170    pub prompt: Option<String>,
171    /// Number of videos.
172    pub n: u32,
173    /// Aspect ratio.
174    pub aspect_ratio: Option<VideoAspectRatio>,
175    /// Resolution in pixels.
176    pub resolution: Option<ImageSize>,
177    /// Duration in seconds.
178    pub duration: Option<f64>,
179    /// Frames per second.
180    pub fps: Option<u32>,
181    /// Random seed.
182    pub seed: Option<u64>,
183    /// Single input image.
184    pub image: Option<VideoFile>,
185    /// Frame-anchored input images.
186    pub frame_images: Vec<FrameImage>,
187    /// Additional reference inputs.
188    pub input_references: Vec<VideoFile>,
189    /// Whether to generate audio.
190    pub generate_audio: Option<bool>,
191    /// Provider-specific options keyed by provider name.
192    pub provider_options: ProviderOptions,
193    /// Additional request headers.
194    pub headers: Headers,
195    /// Cancellation token.
196    pub cancellation: CancellationToken,
197}
198
199impl VideoOptions {
200    /// Creates options for `prompt` requesting one video.
201    #[must_use]
202    pub fn new(prompt: impl Into<String>) -> Self {
203        Self {
204            prompt: Some(prompt.into()),
205            ..Self::default()
206        }
207    }
208}
209
210impl Default for VideoOptions {
211    fn default() -> Self {
212        Self {
213            prompt: None,
214            n: 1,
215            aspect_ratio: None,
216            resolution: None,
217            duration: None,
218            fps: None,
219            seed: None,
220            image: None,
221            frame_images: Vec::new(),
222            input_references: Vec::new(),
223            generate_audio: None,
224            provider_options: ProviderOptions::new(),
225            headers: Headers::new(),
226            cancellation: CancellationToken::new(),
227        }
228    }
229}
230
231/// Options for starting an asynchronous operation.
232#[derive(Debug, Clone)]
233pub struct VideoStartOptions {
234    /// Generation options.
235    pub options: VideoOptions,
236    /// Webhook URL the provider should call on completion.
237    pub webhook_url: Option<Url>,
238}
239
240/// Options for querying an operation.
241#[derive(Debug, Clone)]
242pub struct VideoStatusOptions {
243    /// Opaque operation handle returned by `do_start`.
244    pub operation: JsonValue,
245    /// Additional request headers.
246    pub headers: Headers,
247    /// Cancellation token.
248    pub cancellation: CancellationToken,
249}
250
251/// A generated video.
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub struct VideoData {
254    /// Payload: inline bytes or a URL.
255    pub data: FileData,
256    /// Media type.
257    pub media_type: MediaType,
258}
259
260/// Result of a synchronous generation or a completed operation.
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262pub struct VideoResult {
263    /// Generated videos.
264    pub videos: Vec<VideoData>,
265    /// Warnings.
266    #[serde(default)]
267    pub warnings: Vec<Warning>,
268    /// Provider-specific metadata.
269    #[serde(default, skip_serializing_if = "Option::is_none")]
270    pub provider_metadata: Option<ProviderMetadata>,
271    /// Response metadata; `timestamp` and `model_id` are expected to be set.
272    #[serde(default)]
273    pub response: ResponseMetadata,
274}
275
276/// Result of starting an operation.
277#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
278pub struct VideoStartResult {
279    /// Opaque operation handle to pass to `do_status`.
280    pub operation: JsonValue,
281    /// Warnings.
282    #[serde(default)]
283    pub warnings: Vec<Warning>,
284    /// Provider-specific metadata.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub provider_metadata: Option<ProviderMetadata>,
287    /// Response metadata.
288    #[serde(default)]
289    pub response: ResponseMetadata,
290}
291
292/// Status of an operation, tagged by `status`.
293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
294#[serde(tag = "status", rename_all = "lowercase")]
295#[non_exhaustive]
296pub enum VideoStatusResult {
297    /// Still running.
298    Pending {
299        /// Warnings.
300        #[serde(default)]
301        warnings: Vec<Warning>,
302        /// Provider-specific metadata.
303        #[serde(default, skip_serializing_if = "Option::is_none")]
304        provider_metadata: Option<ProviderMetadata>,
305        /// Response metadata.
306        #[serde(default)]
307        response: ResponseMetadata,
308    },
309    /// Finished successfully.
310    Completed {
311        /// Generated videos.
312        videos: Vec<VideoData>,
313        /// Warnings.
314        #[serde(default)]
315        warnings: Vec<Warning>,
316        /// Provider-specific metadata.
317        #[serde(default, skip_serializing_if = "Option::is_none")]
318        provider_metadata: Option<ProviderMetadata>,
319        /// Response metadata.
320        #[serde(default)]
321        response: ResponseMetadata,
322    },
323    /// Failed.
324    Error {
325        /// Error message reported by the provider.
326        error: String,
327        /// Provider-specific metadata.
328        #[serde(default, skip_serializing_if = "Option::is_none")]
329        provider_metadata: Option<ProviderMetadata>,
330        /// Response metadata.
331        #[serde(default)]
332        response: ResponseMetadata,
333    },
334}
335
336/// Payload delivered to a webhook.
337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
338pub struct WebhookPayload {
339    /// Request headers of the callback.
340    pub headers: Headers,
341    /// Request body of the callback.
342    pub body: JsonValue,
343}
344
345/// A prepared webhook: the URL to hand to the provider and a future that
346/// resolves when the callback arrives.
347pub struct WebhookHandle {
348    /// Publicly reachable callback URL.
349    pub url: Url,
350    /// Resolves with the callback payload.
351    pub received: BoxFuture<'static, Result<WebhookPayload, ProviderError>>,
352}
353
354impl std::fmt::Debug for WebhookHandle {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        f.debug_struct("WebhookHandle")
357            .field("url", &self.url)
358            .field("received", &"<future>")
359            .finish()
360    }
361}
362
363/// Creates webhooks; implemented by the application (the SDK runs no server).
364pub type WebhookFactory =
365    Arc<dyn Fn() -> BoxFuture<'static, Result<WebhookHandle, ProviderError>> + Send + Sync>;