Skip to main content

elara_visual/
keyframe.rs

1//! Keyframe and Delta Encoding - ELARA-native visual compression
2//!
3//! This is NOT H.264/VP8/AV1. This is state-based keyframe/delta encoding.
4//! We encode the CHANGE in visual state, not pixel differences.
5
6use elara_core::StateTime;
7
8use crate::{FaceState, PoseState, SceneState, VisualState, VisualStateId};
9
10/// Keyframe - Complete visual state snapshot
11#[derive(Debug, Clone)]
12pub struct Keyframe {
13    /// Keyframe identifier
14    pub id: VisualStateId,
15
16    /// Timestamp
17    pub timestamp: StateTime,
18
19    /// Complete visual state
20    pub state: VisualState,
21
22    /// Keyframe interval (how often keyframes are sent)
23    pub interval_ms: u32,
24
25    /// Sequence number
26    pub sequence: u64,
27}
28
29impl Keyframe {
30    /// Create a new keyframe
31    pub fn new(state: VisualState, interval_ms: u32) -> Self {
32        Self {
33            id: state.id,
34            timestamp: state.timestamp,
35            sequence: state.sequence,
36            state,
37            interval_ms,
38        }
39    }
40
41    /// Estimated size in bytes (for bandwidth calculation)
42    pub fn estimated_size(&self) -> usize {
43        let mut size = 32; // Base header
44
45        if self.state.face.is_some() {
46            size += 128; // Face state
47        }
48        if self.state.pose.is_some() {
49            size += 256; // Pose state (21 joints)
50        }
51        if self.state.scene.is_some() {
52            size += 64; // Scene state
53        }
54
55        size
56    }
57}
58
59/// Delta type - what changed
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum DeltaType {
62    /// No change
63    None,
64    /// Face state changed
65    Face,
66    /// Pose state changed
67    Pose,
68    /// Scene state changed
69    Scene,
70    /// Multiple things changed
71    Multiple,
72}
73
74/// Delta - Changes since last keyframe or delta
75#[derive(Debug, Clone)]
76pub struct Delta {
77    /// Delta identifier
78    pub id: VisualStateId,
79
80    /// Reference to keyframe this delta is based on
81    pub keyframe_ref: VisualStateId,
82
83    /// Reference to previous delta (if any)
84    pub prev_delta_ref: Option<VisualStateId>,
85
86    /// Timestamp
87    pub timestamp: StateTime,
88
89    /// What changed
90    pub delta_type: DeltaType,
91
92    /// Face delta (if changed)
93    pub face_delta: Option<FaceDelta>,
94
95    /// Pose delta (if changed)
96    pub pose_delta: Option<PoseDelta>,
97
98    /// Scene delta (if changed)
99    pub scene_delta: Option<SceneDelta>,
100
101    /// Sequence number
102    pub sequence: u64,
103}
104
105impl Delta {
106    /// Create a delta from two visual states
107    pub fn from_states(
108        prev: &VisualState,
109        curr: &VisualState,
110        keyframe_ref: VisualStateId,
111    ) -> Self {
112        let face_delta = match (&prev.face, &curr.face) {
113            (Some(prev_face), Some(curr_face)) => FaceDelta::compute(prev_face, curr_face),
114            (None, Some(curr_face)) => Some(FaceDelta::full(curr_face.clone())),
115            (Some(_), None) => Some(FaceDelta::removed()),
116            (None, None) => None,
117        };
118
119        let pose_delta = match (&prev.pose, &curr.pose) {
120            (Some(prev_pose), Some(curr_pose)) => PoseDelta::compute(prev_pose, curr_pose),
121            (None, Some(curr_pose)) => Some(PoseDelta::full(curr_pose.clone())),
122            (Some(_), None) => Some(PoseDelta::removed()),
123            (None, None) => None,
124        };
125
126        let scene_delta = match (&prev.scene, &curr.scene) {
127            (Some(prev_scene), Some(curr_scene)) => SceneDelta::compute(prev_scene, curr_scene),
128            (None, Some(curr_scene)) => Some(SceneDelta::full(curr_scene.clone())),
129            (Some(_), None) => Some(SceneDelta::removed()),
130            (None, None) => None,
131        };
132
133        let delta_type = match (
134            face_delta.is_some(),
135            pose_delta.is_some(),
136            scene_delta.is_some(),
137        ) {
138            (false, false, false) => DeltaType::None,
139            (true, false, false) => DeltaType::Face,
140            (false, true, false) => DeltaType::Pose,
141            (false, false, true) => DeltaType::Scene,
142            _ => DeltaType::Multiple,
143        };
144
145        Self {
146            id: curr.id,
147            keyframe_ref,
148            prev_delta_ref: Some(prev.id),
149            timestamp: curr.timestamp,
150            delta_type,
151            face_delta,
152            pose_delta,
153            scene_delta,
154            sequence: curr.sequence,
155        }
156    }
157
158    /// Is this an empty delta (no changes)?
159    pub fn is_empty(&self) -> bool {
160        self.delta_type == DeltaType::None
161    }
162
163    /// Estimated size in bytes
164    pub fn estimated_size(&self) -> usize {
165        let mut size = 24; // Base header
166
167        if let Some(ref fd) = self.face_delta {
168            size += fd.estimated_size();
169        }
170        if let Some(ref pd) = self.pose_delta {
171            size += pd.estimated_size();
172        }
173        if let Some(ref sd) = self.scene_delta {
174            size += sd.estimated_size();
175        }
176
177        size
178    }
179
180    /// Apply this delta to a visual state
181    pub fn apply(&self, base: &VisualState) -> VisualState {
182        let mut result = base.clone();
183        result.id = self.id;
184        result.timestamp = self.timestamp;
185        result.sequence = self.sequence;
186        result.is_keyframe = false;
187        result.keyframe_ref = Some(self.keyframe_ref);
188
189        if let Some(ref fd) = self.face_delta {
190            result.face = fd.apply(base.face.as_ref());
191        }
192        if let Some(ref pd) = self.pose_delta {
193            result.pose = pd.apply(base.pose.as_ref());
194        }
195        if let Some(ref sd) = self.scene_delta {
196            result.scene = sd.apply(base.scene.as_ref());
197        }
198
199        result
200    }
201}
202
203/// Face delta - changes in face state
204#[derive(Debug, Clone)]
205pub enum FaceDelta {
206    /// No face anymore
207    Removed,
208    /// Full face state (new face appeared)
209    Full(FaceState),
210    /// Partial update
211    Partial {
212        /// Head rotation change (if significant)
213        head_rotation: Option<(f32, f32, f32)>,
214        /// Emotion change (if significant)
215        emotion_change: Option<(String, f32)>, // (emotion_name, new_value)
216        /// Mouth openness change
217        mouth_openness: Option<f32>,
218        /// Speaking state change
219        speaking: Option<bool>,
220        /// Gaze change
221        gaze_yaw: Option<f32>,
222        gaze_pitch: Option<f32>,
223    },
224}
225
226impl FaceDelta {
227    /// Compute delta between two face states
228    pub fn compute(prev: &FaceState, curr: &FaceState) -> Option<FaceDelta> {
229        // Check if there are significant changes
230        let head_changed = (prev.head_rotation.0 - curr.head_rotation.0).abs() > 0.05
231            || (prev.head_rotation.1 - curr.head_rotation.1).abs() > 0.05
232            || (prev.head_rotation.2 - curr.head_rotation.2).abs() > 0.05;
233
234        let mouth_changed = (prev.mouth.openness - curr.mouth.openness).abs() > 0.1;
235        let speaking_changed = prev.speaking != curr.speaking;
236        let gaze_changed = (prev.gaze.yaw - curr.gaze.yaw).abs() > 0.1
237            || (prev.gaze.pitch - curr.gaze.pitch).abs() > 0.1;
238
239        // Check emotion changes
240        let prev_dom = prev.emotion.dominant();
241        let curr_dom = curr.emotion.dominant();
242        let emotion_changed = prev_dom.0 != curr_dom.0 || (prev_dom.1 - curr_dom.1).abs() > 0.2;
243
244        if !head_changed && !mouth_changed && !speaking_changed && !gaze_changed && !emotion_changed
245        {
246            return None;
247        }
248
249        Some(FaceDelta::Partial {
250            head_rotation: if head_changed {
251                Some(curr.head_rotation)
252            } else {
253                None
254            },
255            emotion_change: if emotion_changed {
256                Some((curr_dom.0.to_string(), curr_dom.1))
257            } else {
258                None
259            },
260            mouth_openness: if mouth_changed {
261                Some(curr.mouth.openness)
262            } else {
263                None
264            },
265            speaking: if speaking_changed {
266                Some(curr.speaking)
267            } else {
268                None
269            },
270            gaze_yaw: if gaze_changed {
271                Some(curr.gaze.yaw)
272            } else {
273                None
274            },
275            gaze_pitch: if gaze_changed {
276                Some(curr.gaze.pitch)
277            } else {
278                None
279            },
280        })
281    }
282
283    /// Full face state
284    pub fn full(face: FaceState) -> FaceDelta {
285        FaceDelta::Full(face)
286    }
287
288    /// Face removed
289    pub fn removed() -> FaceDelta {
290        FaceDelta::Removed
291    }
292
293    /// Estimated size
294    pub fn estimated_size(&self) -> usize {
295        match self {
296            FaceDelta::Removed => 1,
297            FaceDelta::Full(_) => 128,
298            FaceDelta::Partial { .. } => 32,
299        }
300    }
301
302    /// Apply delta to base face state
303    pub fn apply(&self, base: Option<&FaceState>) -> Option<FaceState> {
304        match self {
305            FaceDelta::Removed => None,
306            FaceDelta::Full(face) => Some(face.clone()),
307            FaceDelta::Partial {
308                head_rotation,
309                emotion_change,
310                mouth_openness,
311                speaking,
312                gaze_yaw,
313                gaze_pitch,
314            } => {
315                let mut face = base?.clone();
316
317                if let Some(rot) = head_rotation {
318                    face.head_rotation = *rot;
319                }
320                if let Some((_, val)) = emotion_change {
321                    // Simplified: just update joy for now
322                    face.emotion.joy = *val;
323                }
324                if let Some(openness) = mouth_openness {
325                    face.mouth.openness = *openness;
326                }
327                if let Some(spk) = speaking {
328                    face.speaking = *spk;
329                }
330                if let Some(yaw) = gaze_yaw {
331                    face.gaze.yaw = *yaw;
332                }
333                if let Some(pitch) = gaze_pitch {
334                    face.gaze.pitch = *pitch;
335                }
336
337                Some(face)
338            }
339        }
340    }
341}
342
343/// Pose delta - changes in pose state
344#[derive(Debug, Clone)]
345pub enum PoseDelta {
346    /// No pose anymore
347    Removed,
348    /// Full pose state
349    Full(PoseState),
350    /// Partial update - only changed joints
351    Partial {
352        /// Changed joint indices and their new states
353        changed_joints: Vec<(usize, crate::JointState)>,
354        /// Gesture change
355        gesture: Option<crate::Gesture>,
356        /// Activity change
357        activity: Option<crate::ActivityState>,
358    },
359}
360
361impl PoseDelta {
362    /// Compute delta between two pose states
363    pub fn compute(prev: &PoseState, curr: &PoseState) -> Option<PoseDelta> {
364        let mut changed_joints = Vec::new();
365
366        // Find changed joints
367        for (i, (prev_joint, curr_joint)) in prev.joints.iter().zip(curr.joints.iter()).enumerate()
368        {
369            let pos_changed = prev_joint.position.distance(&curr_joint.position) > 0.01;
370            if pos_changed {
371                changed_joints.push((i, *curr_joint));
372            }
373        }
374
375        let gesture_changed = prev.gesture != curr.gesture;
376        let activity_changed = prev.activity != curr.activity;
377
378        if changed_joints.is_empty() && !gesture_changed && !activity_changed {
379            return None;
380        }
381
382        Some(PoseDelta::Partial {
383            changed_joints,
384            gesture: if gesture_changed {
385                Some(curr.gesture)
386            } else {
387                None
388            },
389            activity: if activity_changed {
390                Some(curr.activity)
391            } else {
392                None
393            },
394        })
395    }
396
397    pub fn full(pose: PoseState) -> PoseDelta {
398        PoseDelta::Full(pose)
399    }
400
401    pub fn removed() -> PoseDelta {
402        PoseDelta::Removed
403    }
404
405    pub fn estimated_size(&self) -> usize {
406        match self {
407            PoseDelta::Removed => 1,
408            PoseDelta::Full(_) => 256,
409            PoseDelta::Partial { changed_joints, .. } => 8 + changed_joints.len() * 16,
410        }
411    }
412
413    pub fn apply(&self, base: Option<&PoseState>) -> Option<PoseState> {
414        match self {
415            PoseDelta::Removed => None,
416            PoseDelta::Full(pose) => Some(pose.clone()),
417            PoseDelta::Partial {
418                changed_joints,
419                gesture,
420                activity,
421            } => {
422                let mut pose = base?.clone();
423
424                for (idx, joint_state) in changed_joints {
425                    if *idx < pose.joints.len() {
426                        pose.joints[*idx] = *joint_state;
427                    }
428                }
429
430                if let Some(g) = gesture {
431                    pose.gesture = *g;
432                }
433                if let Some(a) = activity {
434                    pose.activity = *a;
435                }
436
437                Some(pose)
438            }
439        }
440    }
441}
442
443/// Scene delta - changes in scene state
444#[derive(Debug, Clone)]
445pub enum SceneDelta {
446    /// No scene anymore
447    Removed,
448    /// Full scene state
449    Full(SceneState),
450    /// Partial update
451    Partial {
452        /// Background color change
453        background_color: Option<crate::Color>,
454        /// Lighting change
455        lighting: Option<crate::LightingCondition>,
456        /// Detail level change
457        detail_level: Option<f32>,
458    },
459}
460
461impl SceneDelta {
462    pub fn compute(prev: &SceneState, curr: &SceneState) -> Option<SceneDelta> {
463        let color_changed = (prev.background_color.r - curr.background_color.r).abs() > 0.1
464            || (prev.background_color.g - curr.background_color.g).abs() > 0.1
465            || (prev.background_color.b - curr.background_color.b).abs() > 0.1;
466
467        let lighting_changed = prev.lighting != curr.lighting;
468        let detail_changed = (prev.detail_level - curr.detail_level).abs() > 0.1;
469
470        if !color_changed && !lighting_changed && !detail_changed {
471            return None;
472        }
473
474        Some(SceneDelta::Partial {
475            background_color: if color_changed {
476                Some(curr.background_color)
477            } else {
478                None
479            },
480            lighting: if lighting_changed {
481                Some(curr.lighting)
482            } else {
483                None
484            },
485            detail_level: if detail_changed {
486                Some(curr.detail_level)
487            } else {
488                None
489            },
490        })
491    }
492
493    pub fn full(scene: SceneState) -> SceneDelta {
494        SceneDelta::Full(scene)
495    }
496
497    pub fn removed() -> SceneDelta {
498        SceneDelta::Removed
499    }
500
501    pub fn estimated_size(&self) -> usize {
502        match self {
503            SceneDelta::Removed => 1,
504            SceneDelta::Full(_) => 64,
505            SceneDelta::Partial { .. } => 16,
506        }
507    }
508
509    pub fn apply(&self, base: Option<&SceneState>) -> Option<SceneState> {
510        match self {
511            SceneDelta::Removed => None,
512            SceneDelta::Full(scene) => Some(scene.clone()),
513            SceneDelta::Partial {
514                background_color,
515                lighting,
516                detail_level,
517            } => {
518                let mut scene = base?.clone();
519
520                if let Some(color) = background_color {
521                    scene.background_color = *color;
522                }
523                if let Some(light) = lighting {
524                    scene.lighting = *light;
525                }
526                if let Some(detail) = detail_level {
527                    scene.detail_level = *detail;
528                }
529
530                Some(scene)
531            }
532        }
533    }
534}
535
536#[cfg(test)]
537mod tests {
538    use super::*;
539    use elara_core::NodeId;
540
541    #[test]
542    fn test_keyframe_creation() {
543        let node = NodeId::new(1);
544        let time = StateTime::from_millis(0);
545        let state = VisualState::keyframe(node, time, 1);
546        let keyframe = Keyframe::new(state, 1000);
547
548        assert_eq!(keyframe.interval_ms, 1000);
549        assert!(keyframe.estimated_size() > 0);
550    }
551
552    #[test]
553    fn test_delta_computation() {
554        let node = NodeId::new(1);
555        let time1 = StateTime::from_millis(0);
556        let time2 = StateTime::from_millis(100);
557
558        let state1 = VisualState::keyframe(node, time1, 1);
559        let state2 = VisualState::keyframe(node, time2, 2);
560
561        let delta = Delta::from_states(&state1, &state2, state1.id);
562
563        // Both states have no face/pose/scene, so delta should be empty
564        assert!(delta.is_empty());
565    }
566}