Skip to main content

oxihuman_morph/
expression_recorder.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Record and playback expression state over time.
5
6#[allow(dead_code)]
7#[derive(Clone)]
8pub struct ExpressionSnapshot {
9    pub time: f32,
10    pub weights: Vec<(String, f32)>,
11    pub metadata: String,
12}
13
14#[allow(dead_code)]
15pub struct ExpressionRecording {
16    pub name: String,
17    pub snapshots: Vec<ExpressionSnapshot>,
18    pub fps: f32,
19    pub looping: bool,
20}
21
22#[allow(dead_code)]
23pub struct RecorderState {
24    pub recording: bool,
25    pub playing: bool,
26    pub current_time: f32,
27    pub playback_speed: f32,
28}
29
30#[allow(dead_code)]
31pub fn new_recording(name: &str, fps: f32) -> ExpressionRecording {
32    ExpressionRecording {
33        name: name.to_string(),
34        snapshots: Vec::new(),
35        fps,
36        looping: false,
37    }
38}
39
40#[allow(dead_code)]
41pub fn record_snapshot(rec: &mut ExpressionRecording, time: f32, weights: Vec<(String, f32)>) {
42    rec.snapshots.push(ExpressionSnapshot {
43        time,
44        weights,
45        metadata: String::new(),
46    });
47    rec.snapshots.sort_by(|a, b| {
48        a.time
49            .partial_cmp(&b.time)
50            .unwrap_or(std::cmp::Ordering::Equal)
51    });
52}
53
54#[allow(dead_code)]
55pub fn playback_at(rec: &ExpressionRecording, time: f32) -> Vec<(String, f32)> {
56    if rec.snapshots.is_empty() {
57        return Vec::new();
58    }
59    if rec.snapshots.len() == 1 {
60        return rec.snapshots[0].weights.clone();
61    }
62    // Find surrounding snapshots
63    let before = rec.snapshots.iter().rfind(|s| s.time <= time);
64    let after = rec.snapshots.iter().find(|s| s.time > time);
65    match (before, after) {
66        (Some(b), Some(a)) => {
67            let span = a.time - b.time;
68            let t = if span > 1e-8 {
69                (time - b.time) / span
70            } else {
71                0.0
72            };
73            interpolate_weights(&b.weights, &a.weights, t)
74        }
75        (Some(b), None) => b.weights.clone(),
76        (None, Some(a)) => a.weights.clone(),
77        (None, None) => Vec::new(),
78    }
79}
80
81fn interpolate_weights(a: &[(String, f32)], b: &[(String, f32)], t: f32) -> Vec<(String, f32)> {
82    let mut result: Vec<(String, f32)> = Vec::new();
83    for (name, wa) in a {
84        let wb = b
85            .iter()
86            .find(|(n, _)| n == name)
87            .map(|(_, w)| *w)
88            .unwrap_or(0.0);
89        result.push((name.clone(), wa + (wb - wa) * t));
90    }
91    for (name, wb) in b {
92        if !result.iter().any(|(n, _)| n == name) {
93            result.push((name.clone(), *wb * t));
94        }
95    }
96    result
97}
98
99#[allow(dead_code)]
100pub fn recording_duration(rec: &ExpressionRecording) -> f32 {
101    if rec.snapshots.is_empty() {
102        return 0.0;
103    }
104    let first = rec.snapshots.first().map_or(0.0, |s| s.time);
105    let last = rec.snapshots.last().map_or(0.0, |s| s.time);
106    last - first
107}
108
109#[allow(dead_code)]
110pub fn snapshot_count(rec: &ExpressionRecording) -> usize {
111    rec.snapshots.len()
112}
113
114#[allow(dead_code)]
115pub fn trim_recording(rec: &mut ExpressionRecording, start: f32, end: f32) {
116    rec.snapshots.retain(|s| s.time >= start && s.time <= end);
117}
118
119#[allow(dead_code)]
120pub fn reverse_recording(rec: &mut ExpressionRecording) {
121    if rec.snapshots.is_empty() {
122        return;
123    }
124    let max_t = rec.snapshots[rec.snapshots.len() - 1].time;
125    for snap in &mut rec.snapshots {
126        snap.time = max_t - snap.time;
127    }
128    rec.snapshots.reverse();
129}
130
131#[allow(dead_code)]
132pub fn scale_recording_time(rec: &mut ExpressionRecording, factor: f32) {
133    for snap in &mut rec.snapshots {
134        snap.time *= factor;
135    }
136}
137
138#[allow(dead_code)]
139pub fn new_recorder_state() -> RecorderState {
140    RecorderState {
141        recording: false,
142        playing: false,
143        current_time: 0.0,
144        playback_speed: 1.0,
145    }
146}
147
148#[allow(dead_code)]
149pub fn start_recording(state: &mut RecorderState) {
150    state.recording = true;
151    state.playing = false;
152}
153
154#[allow(dead_code)]
155pub fn stop_recording(state: &mut RecorderState) {
156    state.recording = false;
157}
158
159#[allow(dead_code)]
160pub fn start_playback(state: &mut RecorderState) {
161    state.playing = true;
162    state.recording = false;
163}
164
165#[allow(dead_code)]
166pub fn stop_playback(state: &mut RecorderState) {
167    state.playing = false;
168}
169
170#[allow(dead_code)]
171pub fn advance_playback(
172    state: &mut RecorderState,
173    rec: &ExpressionRecording,
174    dt: f32,
175) -> Vec<(String, f32)> {
176    if !state.playing {
177        return Vec::new();
178    }
179    state.current_time += dt * state.playback_speed;
180    let duration = recording_duration(rec);
181    if rec.looping && duration > 0.0 {
182        state.current_time %= duration;
183    } else if state.current_time > duration {
184        state.current_time = duration;
185        state.playing = false;
186    }
187    playback_at(rec, state.current_time)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_new_recording() {
196        let rec = new_recording("test", 30.0);
197        assert_eq!(rec.name, "test");
198        assert_eq!(rec.fps, 30.0);
199        assert!(!rec.looping);
200        assert!(rec.snapshots.is_empty());
201    }
202
203    #[test]
204    fn test_record_snapshot() {
205        let mut rec = new_recording("r", 24.0);
206        record_snapshot(&mut rec, 0.0, vec![("smile".to_string(), 0.5)]);
207        record_snapshot(&mut rec, 1.0, vec![("smile".to_string(), 1.0)]);
208        assert_eq!(snapshot_count(&rec), 2);
209    }
210
211    #[test]
212    fn test_snapshot_count() {
213        let mut rec = new_recording("r", 24.0);
214        assert_eq!(snapshot_count(&rec), 0);
215        record_snapshot(&mut rec, 0.0, vec![]);
216        assert_eq!(snapshot_count(&rec), 1);
217    }
218
219    #[test]
220    fn test_recording_duration() {
221        let mut rec = new_recording("r", 24.0);
222        assert!((recording_duration(&rec)).abs() < 1e-5);
223        record_snapshot(&mut rec, 0.0, vec![]);
224        record_snapshot(&mut rec, 2.0, vec![]);
225        assert!((recording_duration(&rec) - 2.0).abs() < 1e-5);
226    }
227
228    #[test]
229    fn test_playback_at_empty() {
230        let rec = new_recording("r", 24.0);
231        let result = playback_at(&rec, 0.5);
232        assert!(result.is_empty());
233    }
234
235    #[test]
236    fn test_playback_at_interpolates() {
237        let mut rec = new_recording("r", 24.0);
238        record_snapshot(&mut rec, 0.0, vec![("brow".to_string(), 0.0)]);
239        record_snapshot(&mut rec, 1.0, vec![("brow".to_string(), 1.0)]);
240        let weights = playback_at(&rec, 0.5);
241        let brow = weights
242            .iter()
243            .find(|(n, _)| n == "brow")
244            .expect("should succeed");
245        assert!((brow.1 - 0.5).abs() < 1e-5);
246    }
247
248    #[test]
249    fn test_playback_at_before_start() {
250        let mut rec = new_recording("r", 24.0);
251        record_snapshot(&mut rec, 1.0, vec![("x".to_string(), 0.8)]);
252        let weights = playback_at(&rec, 0.0);
253        assert_eq!(weights.len(), 1);
254    }
255
256    #[test]
257    fn test_trim_recording() {
258        let mut rec = new_recording("r", 24.0);
259        for i in 0..5 {
260            record_snapshot(&mut rec, i as f32, vec![]);
261        }
262        trim_recording(&mut rec, 1.0, 3.0);
263        assert_eq!(snapshot_count(&rec), 3);
264    }
265
266    #[test]
267    fn test_reverse_recording() {
268        let mut rec = new_recording("r", 24.0);
269        record_snapshot(&mut rec, 0.0, vec![("a".to_string(), 0.0)]);
270        record_snapshot(&mut rec, 1.0, vec![("a".to_string(), 1.0)]);
271        reverse_recording(&mut rec);
272        assert!((rec.snapshots[0].time).abs() < 1e-5);
273        assert!((rec.snapshots[1].time - 1.0).abs() < 1e-5);
274        assert!((rec.snapshots[0].weights[0].1 - 1.0).abs() < 1e-5);
275    }
276
277    #[test]
278    fn test_scale_recording_time() {
279        let mut rec = new_recording("r", 24.0);
280        record_snapshot(&mut rec, 1.0, vec![]);
281        record_snapshot(&mut rec, 2.0, vec![]);
282        scale_recording_time(&mut rec, 2.0);
283        assert!((rec.snapshots[0].time - 2.0).abs() < 1e-5);
284        assert!((rec.snapshots[1].time - 4.0).abs() < 1e-5);
285    }
286
287    #[test]
288    fn test_new_recorder_state() {
289        let state = new_recorder_state();
290        assert!(!state.recording);
291        assert!(!state.playing);
292        assert!((state.current_time).abs() < 1e-5);
293        assert!((state.playback_speed - 1.0).abs() < 1e-5);
294    }
295
296    #[test]
297    fn test_recorder_state_transitions() {
298        let mut state = new_recorder_state();
299        start_recording(&mut state);
300        assert!(state.recording);
301        stop_recording(&mut state);
302        assert!(!state.recording);
303        start_playback(&mut state);
304        assert!(state.playing);
305        stop_playback(&mut state);
306        assert!(!state.playing);
307    }
308
309    #[test]
310    fn test_start_recording_stops_playback() {
311        let mut state = new_recorder_state();
312        start_playback(&mut state);
313        start_recording(&mut state);
314        assert!(!state.playing);
315        assert!(state.recording);
316    }
317
318    #[test]
319    fn test_advance_playback() {
320        let mut rec = new_recording("r", 24.0);
321        record_snapshot(&mut rec, 0.0, vec![("x".to_string(), 0.0)]);
322        record_snapshot(&mut rec, 2.0, vec![("x".to_string(), 1.0)]);
323        let mut state = new_recorder_state();
324        start_playback(&mut state);
325        let weights = advance_playback(&mut state, &rec, 1.0);
326        assert!(!weights.is_empty());
327        assert!((state.current_time - 1.0).abs() < 1e-5);
328    }
329
330    #[test]
331    fn test_advance_playback_not_playing() {
332        let rec = new_recording("r", 24.0);
333        let mut state = new_recorder_state();
334        let weights = advance_playback(&mut state, &rec, 1.0);
335        assert!(weights.is_empty());
336    }
337}