1use std::collections::HashMap;
7
8#[allow(dead_code)]
12pub struct MicroExpression {
13 pub name: String,
15 pub morph_weights: HashMap<String, f32>,
17 pub duration: f32,
19 pub intensity: f32,
21}
22
23#[allow(dead_code)]
25pub struct MicroExpressionEvent {
26 pub expr: MicroExpression,
28 pub trigger_time: f32,
30 pub fade_in: f32,
32 pub fade_out: f32,
34}
35
36#[allow(dead_code)]
38pub struct MicroExpressionLayer {
39 pub events: Vec<MicroExpressionEvent>,
41 pub base_weights: HashMap<String, f32>,
43}
44
45impl MicroExpressionLayer {
48 #[allow(dead_code)]
50 pub fn new(base_weights: HashMap<String, f32>) -> Self {
51 Self {
52 events: Vec::new(),
53 base_weights,
54 }
55 }
56
57 #[allow(dead_code)]
59 pub fn add_event(&mut self, event: MicroExpressionEvent) {
60 self.events.push(event);
61 }
62
63 #[allow(dead_code)]
65 pub fn sample(&self, t: f32) -> HashMap<String, f32> {
66 let mut result = self.base_weights.clone();
67
68 for event in &self.events {
69 let blend = micro_expr_weight_at(event, t);
70 if blend > 0.0 {
71 result = merge_weights(&result, &event.expr.morph_weights, blend);
72 }
73 }
74 result
75 }
76}
77
78#[allow(dead_code)]
83pub fn micro_expr_weight_at(event: &MicroExpressionEvent, t: f32) -> f32 {
84 let start = event.trigger_time;
85 let end = start + event.expr.duration;
86
87 if t < start - event.fade_in || t > end + event.fade_out {
89 return 0.0;
90 }
91
92 let env = if t < start {
93 let elapsed = t - (start - event.fade_in);
95 (elapsed / event.fade_in).clamp(0.0, 1.0)
96 } else if t <= end {
97 1.0
99 } else {
100 let elapsed = t - end;
102 1.0 - (elapsed / event.fade_out).clamp(0.0, 1.0)
103 };
104
105 env * event.expr.intensity
106}
107
108#[allow(dead_code)]
111pub fn merge_weights(
112 base: &HashMap<String, f32>,
113 overlay: &HashMap<String, f32>,
114 blend: f32,
115) -> HashMap<String, f32> {
116 let mut result = base.clone();
117 for (k, &v) in overlay {
118 let existing = result.get(k).copied().unwrap_or(0.0);
119 result.insert(k.clone(), (existing + v * blend).clamp(0.0, 1.0));
120 }
121 result
122}
123
124#[allow(dead_code)]
126pub fn standard_micro_expressions() -> Vec<MicroExpression> {
127 vec![
128 MicroExpression {
129 name: "disgust_flash".to_string(),
130 morph_weights: [
131 ("nose_wrinkle".to_string(), 0.8),
132 ("upper_lip_raise".to_string(), 0.7),
133 ("brow_lower_inner".to_string(), 0.5),
134 ]
135 .into_iter()
136 .collect(),
137 duration: 0.12,
138 intensity: 0.85,
139 },
140 MicroExpression {
141 name: "fear_flash".to_string(),
142 morph_weights: [
143 ("brow_raise_inner".to_string(), 0.9),
144 ("eye_widen".to_string(), 0.8),
145 ("lip_stretch".to_string(), 0.6),
146 ]
147 .into_iter()
148 .collect(),
149 duration: 0.10,
150 intensity: 0.80,
151 },
152 MicroExpression {
153 name: "surprise_flash".to_string(),
154 morph_weights: [
155 ("brow_raise_outer".to_string(), 0.95),
156 ("eye_widen".to_string(), 0.9),
157 ("jaw_drop".to_string(), 0.7),
158 ]
159 .into_iter()
160 .collect(),
161 duration: 0.08,
162 intensity: 0.90,
163 },
164 MicroExpression {
165 name: "contempt_flash".to_string(),
166 morph_weights: [
167 ("lip_corner_pull_right".to_string(), 0.7),
168 ("brow_lower_right".to_string(), 0.4),
169 ]
170 .into_iter()
171 .collect(),
172 duration: 0.15,
173 intensity: 0.75,
174 },
175 MicroExpression {
176 name: "joy_flash".to_string(),
177 morph_weights: [
178 ("cheek_raise".to_string(), 0.8),
179 ("lip_corner_pull".to_string(), 0.9),
180 ("eye_squint".to_string(), 0.6),
181 ]
182 .into_iter()
183 .collect(),
184 duration: 0.18,
185 intensity: 0.70,
186 },
187 ]
188}
189
190#[allow(dead_code)]
193pub fn inject_random_micros(layer: &mut MicroExpressionLayer, duration: f32, rate: f32, seed: u64) {
194 let library = standard_micro_expressions();
195 if library.is_empty() || rate <= 0.0 || duration <= 0.0 {
196 return;
197 }
198
199 let mut state = seed;
201 let lcg_next = |s: &mut u64| -> u64 {
202 *s = s
203 .wrapping_mul(6364136223846793005)
204 .wrapping_add(1442695040888963407);
205 *s
206 };
207
208 let expected = (duration * rate).ceil() as usize;
210 let avg_interval = duration / (expected as f32).max(1.0);
211
212 let mut t = 0.0_f32;
213 for _ in 0..expected {
214 let rand_u = lcg_next(&mut state);
216 let jitter = (rand_u % 1000) as f32 / 1000.0 - 0.5; t += avg_interval * (1.0 + jitter);
218 if t >= duration {
219 break;
220 }
221
222 let idx_u = lcg_next(&mut state);
224 let idx = (idx_u % library.len() as u64) as usize;
225 let src = &library[idx];
226
227 let event = MicroExpressionEvent {
228 expr: MicroExpression {
229 name: src.name.clone(),
230 morph_weights: src.morph_weights.clone(),
231 duration: src.duration,
232 intensity: src.intensity,
233 },
234 trigger_time: t,
235 fade_in: 0.02,
236 fade_out: 0.04,
237 };
238 layer.add_event(event);
239 }
240}
241
242#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn simple_event(trigger: f32, duration: f32, intensity: f32) -> MicroExpressionEvent {
248 MicroExpressionEvent {
249 expr: MicroExpression {
250 name: "test".to_string(),
251 morph_weights: [("brow".to_string(), 1.0)].into_iter().collect(),
252 duration,
253 intensity,
254 },
255 trigger_time: trigger,
256 fade_in: 0.1,
257 fade_out: 0.1,
258 }
259 }
260
261 #[test]
263 fn test_weight_before_event() {
264 let ev = simple_event(1.0, 0.1, 0.8);
265 assert!((micro_expr_weight_at(&ev, 0.5) - 0.0).abs() < 1e-6);
266 }
267
268 #[test]
270 fn test_weight_at_peak() {
271 let ev = simple_event(1.0, 0.1, 0.8);
272 let w = micro_expr_weight_at(&ev, 1.05);
274 assert!((w - 0.8).abs() < 1e-5);
275 }
276
277 #[test]
279 fn test_weight_during_fade_in() {
280 let ev = simple_event(1.0, 0.1, 1.0);
281 let w = micro_expr_weight_at(&ev, 0.95);
283 assert!((w - 0.5).abs() < 1e-5);
284 }
285
286 #[test]
288 fn test_weight_during_fade_out() {
289 let ev = simple_event(1.0, 0.1, 1.0);
290 let w = micro_expr_weight_at(&ev, 1.15);
292 assert!((w - 0.5).abs() < 1e-5);
293 }
294
295 #[test]
297 fn test_weight_after_event() {
298 let ev = simple_event(1.0, 0.1, 0.8);
299 let w = micro_expr_weight_at(&ev, 1.5);
301 assert!((w - 0.0).abs() < 1e-6);
302 }
303
304 #[test]
306 fn test_sample_at_peak_merges() {
307 let base: HashMap<String, f32> = [("brow".to_string(), 0.0)].into_iter().collect();
308 let mut layer = MicroExpressionLayer::new(base);
309 layer.add_event(simple_event(0.0, 0.5, 1.0));
310 let result = layer.sample(0.25);
312 assert!((result["brow"] - 1.0).abs() < 1e-5);
313 }
314
315 #[test]
317 fn test_merge_weights_additive() {
318 let base: HashMap<String, f32> = [("cheek".to_string(), 0.3)].into_iter().collect();
319 let overlay: HashMap<String, f32> = [("cheek".to_string(), 0.4)].into_iter().collect();
320 let result = merge_weights(&base, &overlay, 1.0);
321 assert!((result["cheek"] - 0.7).abs() < 1e-5);
322 }
323
324 #[test]
326 fn test_merge_weights_clamp() {
327 let base: HashMap<String, f32> = [("x".to_string(), 0.8)].into_iter().collect();
328 let overlay: HashMap<String, f32> = [("x".to_string(), 0.9)].into_iter().collect();
329 let result = merge_weights(&base, &overlay, 1.0);
330 assert!((result["x"] - 1.0).abs() < 1e-6, "should clamp to 1.0");
331 }
332
333 #[test]
335 fn test_standard_micro_expressions_count() {
336 let lib = standard_micro_expressions();
337 assert!(lib.len() >= 5);
338 }
339
340 #[test]
342 fn test_inject_random_micros_adds_events() {
343 let base: HashMap<String, f32> = HashMap::new();
344 let mut layer = MicroExpressionLayer::new(base);
345 inject_random_micros(&mut layer, 10.0, 2.0, 42);
346 assert!(
347 !layer.events.is_empty(),
348 "should have injected at least one event"
349 );
350 }
351
352 #[test]
354 fn test_layer_no_events_returns_base() {
355 let base: HashMap<String, f32> = [("nose".to_string(), 0.4), ("jaw".to_string(), 0.6)]
356 .into_iter()
357 .collect();
358 let layer = MicroExpressionLayer::new(base.clone());
359 let result = layer.sample(5.0);
360 for (k, &v) in &base {
361 assert!((result[k] - v).abs() < 1e-6);
362 }
363 }
364
365 #[test]
367 fn test_merge_weights_new_key_added() {
368 let base: HashMap<String, f32> = [("a".to_string(), 0.5)].into_iter().collect();
369 let overlay: HashMap<String, f32> = [("b".to_string(), 0.6)].into_iter().collect();
370 let result = merge_weights(&base, &overlay, 0.5);
371 assert!((result["a"] - 0.5).abs() < 1e-6);
372 assert!((result["b"] - 0.3).abs() < 1e-5);
373 }
374}