use bytes::Bytes;
use derive_more::IsVariant;
use mediaframe::frame::Dimensions;
use mediatime::Timestamp;
use smol_str::SmolStr;
use crate::domain::{KeyframeExtractor, Uuid7};
use super::detections::{
ActionDetection, Aesthetics, AnimalAnalysis, BarcodeDetection, Detection, DocumentSegment,
DominantColor, HorizonInfo, HumanAnalysis, ObjectDetection, SaliencyRegion, TextDetection,
VlmAnalysis,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Keyframe<Id = Uuid7> {
id: Id,
scene_id: Id,
pts: Timestamp,
data: Bytes,
mime: SmolStr,
dimensions: Dimensions,
extractor: KeyframeExtractor,
classifications: std::vec::Vec<Detection>,
objects: std::vec::Vec<ObjectDetection>,
humans: HumanAnalysis,
animals: AnimalAnalysis,
actions: std::vec::Vec<ActionDetection>,
text_detections: std::vec::Vec<TextDetection>,
barcodes: std::vec::Vec<BarcodeDetection>,
attention_saliency: std::vec::Vec<SaliencyRegion>,
objectness_saliency: std::vec::Vec<SaliencyRegion>,
horizon: HorizonInfo,
document_segments: std::vec::Vec<DocumentSegment>,
aesthetics: Aesthetics,
colors: std::vec::Vec<DominantColor>,
vlm: VlmAnalysis,
}
impl Keyframe<Uuid7> {
pub fn try_new(
id: Uuid7,
scene_id: Uuid7,
pts: Timestamp,
dimensions: Dimensions,
extractor: KeyframeExtractor,
) -> Result<Self, KeyframeError> {
if id.is_nil() {
return Err(KeyframeError::NilId);
}
if scene_id.is_nil() {
return Err(KeyframeError::NilSceneId);
}
if dimensions.width() == 0 || dimensions.height() == 0 {
return Err(KeyframeError::ZeroDimensions);
}
Ok(Self {
id,
scene_id,
pts,
data: Bytes::new(),
mime: SmolStr::default(),
dimensions,
extractor,
classifications: std::vec::Vec::new(),
objects: std::vec::Vec::new(),
humans: HumanAnalysis::new(),
animals: AnimalAnalysis::new(),
actions: std::vec::Vec::new(),
text_detections: std::vec::Vec::new(),
barcodes: std::vec::Vec::new(),
attention_saliency: std::vec::Vec::new(),
objectness_saliency: std::vec::Vec::new(),
horizon: HorizonInfo::try_new(0.0, 0.0).expect("0.0 confidence is within range"),
document_segments: std::vec::Vec::new(),
aesthetics: Aesthetics::new(0.0, false),
colors: std::vec::Vec::new(),
vlm: VlmAnalysis::new(),
})
}
}
impl<Id> Keyframe<Id> {
#[inline(always)]
pub const fn id_ref(&self) -> &Id {
&self.id
}
#[inline(always)]
pub const fn scene_id_ref(&self) -> &Id {
&self.scene_id
}
#[inline(always)]
pub const fn pts_ref(&self) -> &Timestamp {
&self.pts
}
#[inline(always)]
pub fn data(&self) -> &[u8] {
&self.data
}
#[inline(always)]
pub fn mime(&self) -> &str {
self.mime.as_str()
}
#[inline(always)]
pub fn size(&self) -> u64 {
self.data.len() as u64
}
#[inline(always)]
pub const fn dimensions(&self) -> Dimensions {
self.dimensions
}
#[inline(always)]
pub const fn extractor(&self) -> KeyframeExtractor {
self.extractor
}
#[inline(always)]
pub fn classifications_slice(&self) -> &[Detection] {
&self.classifications
}
#[inline(always)]
pub fn objects_slice(&self) -> &[ObjectDetection] {
&self.objects
}
#[inline(always)]
pub const fn humans_ref(&self) -> &HumanAnalysis {
&self.humans
}
#[inline(always)]
pub const fn animals_ref(&self) -> &AnimalAnalysis {
&self.animals
}
#[inline(always)]
pub fn actions_slice(&self) -> &[ActionDetection] {
&self.actions
}
#[inline(always)]
pub fn text_detections_slice(&self) -> &[TextDetection] {
&self.text_detections
}
#[inline(always)]
pub fn barcodes_slice(&self) -> &[BarcodeDetection] {
&self.barcodes
}
#[inline(always)]
pub fn attention_saliency_slice(&self) -> &[SaliencyRegion] {
&self.attention_saliency
}
#[inline(always)]
pub fn objectness_saliency_slice(&self) -> &[SaliencyRegion] {
&self.objectness_saliency
}
#[inline(always)]
pub const fn horizon_ref(&self) -> &HorizonInfo {
&self.horizon
}
#[inline(always)]
pub fn document_segments_slice(&self) -> &[DocumentSegment] {
&self.document_segments
}
#[inline(always)]
pub const fn aesthetics_ref(&self) -> &Aesthetics {
&self.aesthetics
}
#[inline(always)]
pub fn colors_slice(&self) -> &[DominantColor] {
&self.colors
}
#[inline(always)]
pub const fn vlm_ref(&self) -> &VlmAnalysis {
&self.vlm
}
}
impl<Id> Keyframe<Id> {
#[must_use]
#[inline(always)]
pub fn with_data(mut self, v: impl Into<Bytes>) -> Self {
self.data = v.into();
self
}
#[inline(always)]
pub fn set_data(&mut self, v: impl Into<Bytes>) -> &mut Self {
self.data = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_mime(mut self, v: impl Into<SmolStr>) -> Self {
self.mime = v.into();
self
}
#[inline(always)]
pub fn set_mime(&mut self, v: impl Into<SmolStr>) -> &mut Self {
self.mime = v.into();
self
}
#[inline]
pub fn try_with_dimensions(mut self, v: Dimensions) -> Result<Self, KeyframeError> {
if v.width() == 0 || v.height() == 0 {
return Err(KeyframeError::ZeroDimensions);
}
self.dimensions = v;
Ok(self)
}
#[inline]
pub const fn try_set_dimensions(&mut self, v: Dimensions) -> Result<&mut Self, KeyframeError> {
if v.width() == 0 || v.height() == 0 {
return Err(KeyframeError::ZeroDimensions);
}
self.dimensions = v;
Ok(self)
}
#[must_use]
#[inline(always)]
pub const fn with_extractor(mut self, v: KeyframeExtractor) -> Self {
self.extractor = v;
self
}
#[inline(always)]
pub const fn set_extractor(&mut self, v: KeyframeExtractor) -> &mut Self {
self.extractor = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_classifications(mut self, v: impl Into<std::vec::Vec<Detection>>) -> Self {
self.classifications = v.into();
self
}
#[inline(always)]
pub fn set_classifications(&mut self, v: impl Into<std::vec::Vec<Detection>>) -> &mut Self {
self.classifications = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_objects(mut self, v: impl Into<std::vec::Vec<ObjectDetection>>) -> Self {
self.objects = v.into();
self
}
#[inline(always)]
pub fn set_objects(&mut self, v: impl Into<std::vec::Vec<ObjectDetection>>) -> &mut Self {
self.objects = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_humans(mut self, v: HumanAnalysis) -> Self {
self.humans = v;
self
}
#[inline(always)]
pub fn set_humans(&mut self, v: HumanAnalysis) -> &mut Self {
self.humans = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_animals(mut self, v: AnimalAnalysis) -> Self {
self.animals = v;
self
}
#[inline(always)]
pub fn set_animals(&mut self, v: AnimalAnalysis) -> &mut Self {
self.animals = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_actions(mut self, v: impl Into<std::vec::Vec<ActionDetection>>) -> Self {
self.actions = v.into();
self
}
#[inline(always)]
pub fn set_actions(&mut self, v: impl Into<std::vec::Vec<ActionDetection>>) -> &mut Self {
self.actions = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_text_detections(mut self, v: impl Into<std::vec::Vec<TextDetection>>) -> Self {
self.text_detections = v.into();
self
}
#[inline(always)]
pub fn set_text_detections(&mut self, v: impl Into<std::vec::Vec<TextDetection>>) -> &mut Self {
self.text_detections = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_barcodes(mut self, v: impl Into<std::vec::Vec<BarcodeDetection>>) -> Self {
self.barcodes = v.into();
self
}
#[inline(always)]
pub fn set_barcodes(&mut self, v: impl Into<std::vec::Vec<BarcodeDetection>>) -> &mut Self {
self.barcodes = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_attention_saliency(mut self, v: impl Into<std::vec::Vec<SaliencyRegion>>) -> Self {
self.attention_saliency = v.into();
self
}
#[inline(always)]
pub fn set_attention_saliency(
&mut self,
v: impl Into<std::vec::Vec<SaliencyRegion>>,
) -> &mut Self {
self.attention_saliency = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_objectness_saliency(mut self, v: impl Into<std::vec::Vec<SaliencyRegion>>) -> Self {
self.objectness_saliency = v.into();
self
}
#[inline(always)]
pub fn set_objectness_saliency(
&mut self,
v: impl Into<std::vec::Vec<SaliencyRegion>>,
) -> &mut Self {
self.objectness_saliency = v.into();
self
}
#[must_use]
#[inline(always)]
pub const fn with_horizon(mut self, v: HorizonInfo) -> Self {
self.horizon = v;
self
}
#[inline(always)]
pub const fn set_horizon(&mut self, v: HorizonInfo) -> &mut Self {
self.horizon = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_document_segments(mut self, v: impl Into<std::vec::Vec<DocumentSegment>>) -> Self {
self.document_segments = v.into();
self
}
#[inline(always)]
pub fn set_document_segments(
&mut self,
v: impl Into<std::vec::Vec<DocumentSegment>>,
) -> &mut Self {
self.document_segments = v.into();
self
}
#[must_use]
#[inline(always)]
pub const fn with_aesthetics(mut self, v: Aesthetics) -> Self {
self.aesthetics = v;
self
}
#[inline(always)]
pub const fn set_aesthetics(&mut self, v: Aesthetics) -> &mut Self {
self.aesthetics = v;
self
}
#[must_use]
#[inline(always)]
pub fn with_colors(mut self, v: impl Into<std::vec::Vec<DominantColor>>) -> Self {
self.colors = v.into();
self
}
#[inline(always)]
pub fn set_colors(&mut self, v: impl Into<std::vec::Vec<DominantColor>>) -> &mut Self {
self.colors = v.into();
self
}
#[must_use]
#[inline(always)]
pub fn with_vlm(mut self, v: VlmAnalysis) -> Self {
self.vlm = v;
self
}
#[inline(always)]
pub fn set_vlm(&mut self, v: VlmAnalysis) -> &mut Self {
self.vlm = v;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, IsVariant, thiserror::Error)]
#[non_exhaustive]
pub enum KeyframeError {
#[error("Keyframe id must not be the nil UUID")]
NilId,
#[error("Keyframe `scene_id` (FK → Scene) must not be the nil UUID")]
NilSceneId,
#[error("Keyframe dimensions must be non-zero (locked invariant)")]
ZeroDimensions,
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::domain::vo::LocalizedText;
use core::num::NonZeroU32;
use mediatime::Timebase;
fn tb() -> Timebase {
Timebase::new(1, NonZeroU32::new(1000).unwrap())
}
#[test]
fn try_new_happy_path() {
let scene_id = Uuid7::new();
let ts = Timestamp::new(1234, tb());
let kf = Keyframe::try_new(
Uuid7::new(),
scene_id,
ts,
Dimensions::new(320, 180),
KeyframeExtractor::CompositeQuality,
)
.unwrap();
assert_eq!(kf.scene_id_ref(), &scene_id);
assert_eq!(kf.pts_ref(), &ts);
assert_eq!(kf.dimensions(), Dimensions::new(320, 180));
assert!(kf.extractor().is_composite_quality());
assert!(kf.data().is_empty());
assert!(kf.classifications_slice().is_empty());
assert!(kf.colors_slice().is_empty());
assert_eq!(kf.vlm_ref().shot_type(), "");
}
#[test]
fn try_new_rejects_nil_id_and_parent() {
let ts = Timestamp::new(0, tb());
assert_eq!(
Keyframe::try_new(
Uuid7::nil(),
Uuid7::new(),
ts,
Dimensions::new(1, 1),
KeyframeExtractor::Manual
)
.err(),
Some(KeyframeError::NilId)
);
assert_eq!(
Keyframe::try_new(
Uuid7::new(),
Uuid7::nil(),
ts,
Dimensions::new(1, 1),
KeyframeExtractor::Manual
)
.err(),
Some(KeyframeError::NilSceneId)
);
assert!(KeyframeError::NilId.is_nil_id());
assert!(KeyframeError::NilSceneId.is_nil_scene_id());
}
#[test]
fn try_new_rejects_zero_dimensions() {
let ts = Timestamp::new(0, tb());
for (w, h) in [(0u32, 1u32), (1, 0), (0, 0)] {
let r = Keyframe::try_new(
Uuid7::new(),
Uuid7::new(),
ts,
Dimensions::new(w, h),
KeyframeExtractor::IFrame,
);
assert_eq!(
r.err(),
Some(KeyframeError::ZeroDimensions),
"({w}, {h}) should be rejected"
);
}
assert!(KeyframeError::ZeroDimensions.is_zero_dimensions());
}
#[test]
fn builders_and_setters_chain() {
let ts = Timestamp::new(7000, tb());
let kf = Keyframe::try_new(
Uuid7::new(),
Uuid7::new(),
ts,
Dimensions::new(1920, 1080),
KeyframeExtractor::IFrame,
)
.unwrap()
.with_mime("image/jpeg")
.with_data(std::vec![0xff, 0xd8, 0xff])
.with_classifications(std::vec![Detection::try_new("dog", 0.97).unwrap()])
.with_vlm(
VlmAnalysis::new()
.with_description(LocalizedText::from_src("a dog running"))
.with_tags(std::vec![LocalizedText::from_src("dog")])
.with_shot_type("medium-shot"),
);
assert_eq!(kf.mime(), "image/jpeg");
assert_eq!(kf.size(), 3);
assert_eq!(kf.data().len(), 3);
assert_eq!(kf.classifications_slice().len(), 1);
assert_eq!(kf.classifications_slice()[0].label(), "dog");
assert_eq!(kf.vlm_ref().description_ref().src(), "a dog running");
assert_eq!(kf.vlm_ref().tags_slice().len(), 1);
assert_eq!(kf.vlm_ref().shot_type(), "medium-shot");
let mut kf = kf;
kf.set_mime("");
kf.set_data(Bytes::new());
kf.try_set_dimensions(Dimensions::new(2, 2)).unwrap();
assert!(kf.mime().is_empty());
assert_eq!(kf.size(), 0);
assert!(kf.data().is_empty());
assert_eq!(kf.dimensions(), Dimensions::new(2, 2));
}
#[test]
fn size_is_derived_from_data() {
let ts = Timestamp::new(0, tb());
let kf = Keyframe::try_new(
Uuid7::new(),
Uuid7::new(),
ts,
Dimensions::new(8, 8),
KeyframeExtractor::IFrame,
)
.unwrap()
.with_data(std::vec![1u8, 2, 3, 4, 5]);
assert_eq!(kf.size(), kf.data().len() as u64);
assert_eq!(kf.size(), 5);
}
#[test]
fn dimension_mutators_reject_zero_extent() {
let ts = Timestamp::new(0, tb());
let mut kf = Keyframe::try_new(
Uuid7::new(),
Uuid7::new(),
ts,
Dimensions::new(320, 180),
KeyframeExtractor::IFrame,
)
.unwrap();
for (w, h) in [(0u32, 1u32), (1, 0), (0, 0)] {
assert_eq!(
kf.clone().try_with_dimensions(Dimensions::new(w, h)).err(),
Some(KeyframeError::ZeroDimensions),
"({w}, {h}) builder should be rejected"
);
assert_eq!(
kf.try_set_dimensions(Dimensions::new(w, h)).err(),
Some(KeyframeError::ZeroDimensions),
"({w}, {h}) setter should be rejected"
);
}
assert_eq!(kf.dimensions(), Dimensions::new(320, 180));
kf.try_set_dimensions(Dimensions::new(2, 2)).unwrap();
assert_eq!(kf.dimensions(), Dimensions::new(2, 2));
}
}