1#![allow(dead_code)]
23
24use std::collections::HashMap;
25
26#[derive(Debug, Clone)]
33pub struct ExpressionDef {
34 pub name: String,
36 pub targets: Vec<(String, f64)>,
39 pub tags: Vec<String>,
41 pub symmetry_pair: Option<String>,
44}
45
46impl ExpressionDef {
47 pub fn new(name: impl Into<String>, targets: Vec<(impl Into<String>, f64)>) -> Self {
49 Self {
50 name: name.into(),
51 targets: targets.into_iter().map(|(t, w)| (t.into(), w)).collect(),
52 tags: Vec::new(),
53 symmetry_pair: None,
54 }
55 }
56
57 pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
59 self.tags = tags.into_iter().map(|t| t.into()).collect();
60 self
61 }
62
63 pub fn with_symmetry_pair(mut self, pair: impl Into<String>) -> Self {
65 self.symmetry_pair = Some(pair.into());
66 self
67 }
68}
69
70fn make_def(
75 name: &str,
76 targets: &[(&str, f64)],
77 tags: &[&str],
78 sym: Option<&str>,
79) -> ExpressionDef {
80 let mut def = ExpressionDef::new(
81 name,
82 targets
83 .iter()
84 .map(|(t, w)| (t.to_string(), *w))
85 .collect::<Vec<_>>(),
86 )
87 .with_tags(tags.iter().copied());
88 if let Some(s) = sym {
89 def = def.with_symmetry_pair(s);
90 }
91 def
92}
93
94pub fn default_expression_defs() -> Vec<ExpressionDef> {
98 vec![
99 make_def("Neutral", &[], &["neutral", "baseline"], None),
103 make_def(
107 "Happy",
108 &[
109 ("mouth-corner-puller", 0.85),
110 ("mouth-elevation", 0.55),
111 ("cheek-raise-left", 0.60),
112 ("cheek-raise-right", 0.60),
113 ("eye-left-slight-close", 0.25),
114 ("eye-right-slight-close", 0.25),
115 ],
116 &["positive", "smile", "basic"],
117 None,
118 ),
119 make_def(
123 "Sad",
124 &[
125 ("eyebrows-left-inner-up", 0.75),
126 ("eyebrows-right-inner-up", 0.75),
127 ("mouth-depression", 0.65),
128 ("eye-left-opened-down", 0.35),
129 ("eye-right-opened-down", 0.35),
130 ("lower-lip-depression", 0.40),
131 ],
132 &["negative", "basic", "brow"],
133 Some("Happy"),
134 ),
135 make_def(
139 "Angry",
140 &[
141 ("eyebrows-left-down", 0.80),
142 ("eyebrows-right-down", 0.80),
143 ("eyebrows-left-inner-down", 0.70),
144 ("eyebrows-right-inner-down", 0.70),
145 ("mouth-compression", 0.60),
146 ("mouth-retraction", 0.30),
147 ("nose-wrinkle", 0.40),
148 ],
149 &["negative", "basic", "brow", "mouth"],
150 None,
151 ),
152 make_def(
156 "Surprised",
157 &[
158 ("mouth-open", 0.75),
159 ("eyebrows-left-up", 0.85),
160 ("eyebrows-right-up", 0.85),
161 ("eye-left-opened-up", 0.65),
162 ("eye-right-opened-up", 0.65),
163 ("jaw-drop", 0.50),
164 ],
165 &["surprise", "basic", "eyes", "mouth"],
166 None,
167 ),
168 make_def(
172 "Disgusted",
173 &[
174 ("nose-wrinkle", 0.75),
175 ("upper-lip-raise", 0.65),
176 ("eyebrows-left-down", 0.45),
177 ("eyebrows-right-down", 0.45),
178 ("mouth-left-corner-depression", 0.30),
179 ("mouth-right-corner-depression", 0.30),
180 ],
181 &["negative", "basic", "nose", "mouth"],
182 None,
183 ),
184 make_def(
188 "Fearful",
189 &[
190 ("eyebrows-left-up", 0.70),
191 ("eyebrows-right-up", 0.70),
192 ("eyebrows-left-inner-up", 0.65),
193 ("eyebrows-right-inner-up", 0.65),
194 ("eye-left-opened-up", 0.80),
195 ("eye-right-opened-up", 0.80),
196 ("mouth-open", 0.45),
197 ("mouth-stretch", 0.35),
198 ],
199 &["negative", "basic", "eyes", "brow"],
200 None,
201 ),
202 make_def(
206 "Contempt",
207 &[
208 ("mouth-right-corner-puller", 0.70),
209 ("eyebrows-right-up", 0.30),
210 ("nose-wrinkle", 0.20),
211 ],
212 &["negative", "basic", "asymmetric"],
213 Some("Contempt_L"),
214 ),
215 make_def(
221 "Smirk",
222 &[
223 ("mouth-right-corner-puller", 0.60),
224 ("eyebrows-right-up", 0.20),
225 ],
226 &["positive", "asymmetric"],
227 None,
228 ),
229 make_def(
231 "Wink",
232 &[("eye-right-blink", 0.90), ("mouth-corner-puller", 0.30)],
233 &["playful", "asymmetric"],
234 None,
235 ),
236 make_def(
238 "Pain",
239 &[
240 ("eyebrows-left-inner-up", 0.80),
241 ("eyebrows-right-inner-up", 0.80),
242 ("eyebrows-left-down", 0.50),
243 ("eyebrows-right-down", 0.50),
244 ("nose-wrinkle", 0.55),
245 ("mouth-compression", 0.50),
246 ("eye-left-slight-close", 0.70),
247 ("eye-right-slight-close", 0.70),
248 ],
249 &["negative", "brow", "eyes"],
250 None,
251 ),
252 make_def(
254 "Boredom",
255 &[
256 ("eye-left-slight-close", 0.50),
257 ("eye-right-slight-close", 0.50),
258 ("mouth-depression", 0.20),
259 ("eyebrows-left-down", 0.20),
260 ("eyebrows-right-down", 0.20),
261 ],
262 &["neutral", "eyes"],
263 None,
264 ),
265 ]
266}
267
268#[derive(Debug, Clone)]
275pub struct ExpressionBlender {
276 library: HashMap<String, ExpressionDef>,
278}
279
280impl ExpressionBlender {
281 pub fn new() -> Self {
283 Self {
284 library: HashMap::new(),
285 }
286 }
287
288 pub fn with_defaults() -> Self {
291 let mut blender = Self::new();
292 for def in default_expression_defs() {
293 blender.add(def);
294 }
295 blender
296 }
297
298 pub fn add(&mut self, def: ExpressionDef) {
301 let key = Self::canonical(&def.name);
302 self.library.insert(key, def);
303 }
304
305 pub fn get(&self, name: &str) -> Option<&ExpressionDef> {
307 self.library.get(&Self::canonical(name))
308 }
309
310 pub fn list_names(&self) -> Vec<&str> {
312 let mut names: Vec<&str> = self.library.values().map(|d| d.name.as_str()).collect();
313 names.sort_unstable();
314 names
315 }
316
317 pub fn len(&self) -> usize {
319 self.library.len()
320 }
321
322 pub fn is_empty(&self) -> bool {
324 self.library.is_empty()
325 }
326
327 pub fn blend_to_weights(&self, expr: &str, intensity: f64) -> Option<HashMap<String, f64>> {
332 let def = self.get(expr)?;
333 let scale = intensity.clamp(0.0, 1.0);
334 let mut out = HashMap::with_capacity(def.targets.len());
335 for (target, base_w) in &def.targets {
336 let w = (base_w * scale).clamp(0.0, 1.0);
337 out.insert(target.clone(), w);
338 }
339 Some(out)
340 }
341
342 pub fn blend_multi(&self, exprs: &[(String, f64)]) -> HashMap<String, f64> {
350 let mut acc: HashMap<String, f64> = HashMap::new();
351 for (name, intensity) in exprs {
352 if let Some(weights) = self.blend_to_weights(name, *intensity) {
353 for (target, w) in weights {
354 let entry = acc.entry(target).or_insert(0.0);
355 *entry = (*entry + w).clamp(0.0, 1.0);
356 }
357 }
358 }
359 acc
360 }
361
362 pub fn lerp_expression(&self, from: &str, to: &str, t: f64) -> HashMap<String, f64> {
371 let t = t.clamp(0.0, 1.0);
372 let from_map = self.blend_to_weights(from, 1.0).unwrap_or_default();
373 let to_map = self.blend_to_weights(to, 1.0).unwrap_or_default();
374
375 let mut keys: Vec<String> = from_map.keys().cloned().collect();
377 for k in to_map.keys() {
378 if !from_map.contains_key(k) {
379 keys.push(k.clone());
380 }
381 }
382
383 let mut out = HashMap::with_capacity(keys.len());
384 for key in keys {
385 let a = from_map.get(&key).copied().unwrap_or(0.0);
386 let b = to_map.get(&key).copied().unwrap_or(0.0);
387 let v = (a * (1.0 - t) + b * t).clamp(0.0, 1.0);
388 out.insert(key, v);
389 }
390 out
391 }
392
393 pub fn au_to_expression(au_code: u32, intensity: f64) -> HashMap<String, f64> {
404 let scale = intensity.clamp(0.0, 1.0);
405 let targets: &[(&str, f64)] = match au_code {
406 1 => &[
408 ("eyebrows-left-inner-up", 1.0),
409 ("eyebrows-right-inner-up", 1.0),
410 ],
411 2 => &[("eyebrows-left-up", 1.0), ("eyebrows-right-up", 1.0)],
413 4 => &[
415 ("eyebrows-left-down", 1.0),
416 ("eyebrows-right-down", 1.0),
417 ("eyebrows-left-inner-down", 0.70),
418 ("eyebrows-right-inner-down", 0.70),
419 ],
420 5 => &[("eye-left-opened-up", 1.0), ("eye-right-opened-up", 1.0)],
422 6 => &[("cheek-raise-left", 1.0), ("cheek-raise-right", 1.0)],
424 7 => &[
426 ("eye-left-slight-close", 0.70),
427 ("eye-right-slight-close", 0.70),
428 ],
429 9 => &[("nose-wrinkle", 1.0)],
431 10 => &[("upper-lip-raise", 1.0)],
433 11 => &[
435 ("nasolabial-deepener-left", 0.80),
436 ("nasolabial-deepener-right", 0.80),
437 ],
438 12 => &[("mouth-corner-puller", 1.0), ("mouth-elevation", 0.40)],
440 13 => &[("cheek-puff-left", 0.80), ("cheek-puff-right", 0.80)],
442 14 => &[("cheek-dimple-left", 0.80), ("cheek-dimple-right", 0.80)],
444 15 => &[
446 ("mouth-left-corner-depression", 0.90),
447 ("mouth-right-corner-depression", 0.90),
448 ("mouth-depression", 0.50),
449 ],
450 16 => &[("lower-lip-depression", 1.0)],
452 17 => &[("chin-raise", 1.0)],
454 18 => &[("lip-puckerer", 1.0)],
456 20 => &[("mouth-stretch", 1.0)],
458 22 => &[("lip-funnel", 1.0)],
460 23 => &[("mouth-compression", 0.80)],
462 24 => &[("mouth-compression", 0.60), ("lip-press", 0.80)],
464 25 => &[("mouth-open", 0.50)],
466 26 => &[("jaw-drop", 1.0), ("mouth-open", 0.70)],
468 27 => &[
470 ("mouth-open", 1.0),
471 ("jaw-drop", 0.80),
472 ("mouth-stretch", 0.60),
473 ],
474 28 => &[("lip-suck", 1.0)],
476 41 => &[
478 ("eye-left-opened-down", 0.80),
479 ("eye-right-opened-down", 0.80),
480 ],
481 42 => &[
483 ("eye-left-slight-close", 0.60),
484 ("eye-right-slight-close", 0.60),
485 ],
486 43 => &[("eye-left-blink", 1.0), ("eye-right-blink", 1.0)],
488 44 => &[
490 ("eye-left-slight-close", 0.90),
491 ("eye-right-slight-close", 0.90),
492 ("cheek-raise-left", 0.50),
493 ("cheek-raise-right", 0.50),
494 ],
495 45 => &[("eye-left-blink", 1.0), ("eye-right-blink", 1.0)],
497 46 => &[("eye-right-blink", 1.0)],
499 _ => &[],
501 };
502
503 let mut out = HashMap::with_capacity(targets.len());
504 for (t, base_w) in targets {
505 let w = (base_w * scale).clamp(0.0, 1.0);
506 out.insert(t.to_string(), w);
507 }
508 out
509 }
510
511 fn canonical(name: &str) -> String {
516 name.to_lowercase()
517 }
518}
519
520impl Default for ExpressionBlender {
521 fn default() -> Self {
522 Self::with_defaults()
523 }
524}
525
526#[cfg(test)]
531mod tests {
532 use super::*;
533
534 fn blender() -> ExpressionBlender {
535 ExpressionBlender::with_defaults()
536 }
537
538 #[test]
541 fn expression_def_basic_fields() {
542 let def = ExpressionDef::new("TestExpr", vec![("target-a", 0.5), ("target-b", 1.0)]);
543 assert_eq!(def.name, "TestExpr");
544 assert_eq!(def.targets.len(), 2);
545 assert!(def.tags.is_empty());
546 assert!(def.symmetry_pair.is_none());
547 }
548
549 #[test]
550 fn expression_def_with_tags_and_pair() {
551 let def = ExpressionDef::new("Expr", vec![("t", 1.0)])
552 .with_tags(["emotion", "face"])
553 .with_symmetry_pair("Expr_Mirror");
554 assert_eq!(def.tags, vec!["emotion", "face"]);
555 assert_eq!(def.symmetry_pair.as_deref(), Some("Expr_Mirror"));
556 }
557
558 #[test]
561 fn default_library_has_eight_basic_expressions() {
562 let b = blender();
563 for name in [
564 "Neutral",
565 "Happy",
566 "Sad",
567 "Angry",
568 "Surprised",
569 "Disgusted",
570 "Fearful",
571 "Contempt",
572 ] {
573 assert!(b.get(name).is_some(), "missing basic expression: {name}");
574 }
575 }
576
577 #[test]
578 fn default_library_len_at_least_eight() {
579 let b = blender();
580 assert!(b.len() >= 8, "expected >= 8 expressions, got {}", b.len());
581 }
582
583 #[test]
584 fn list_names_is_sorted() {
585 let b = blender();
586 let names = b.list_names();
587 let mut sorted = names.clone();
588 sorted.sort_unstable();
589 assert_eq!(names, sorted);
590 }
591
592 #[test]
595 fn blend_to_weights_happy_full() {
596 let b = blender();
597 let w = b.blend_to_weights("Happy", 1.0).expect("Happy not found");
598 assert!(w.contains_key("mouth-corner-puller"));
599 for v in w.values() {
600 assert!(*v >= 0.0 && *v <= 1.0, "weight out of range: {v}");
601 }
602 }
603
604 #[test]
605 fn blend_to_weights_scales_with_intensity() {
606 let b = blender();
607 let w_full = b.blend_to_weights("Happy", 1.0).expect("should succeed");
608 let w_half = b.blend_to_weights("Happy", 0.5).expect("should succeed");
609 for (k, v_full) in &w_full {
610 let v_half = w_half[k];
611 assert!(
612 (v_half - v_full * 0.5).abs() < 1e-10,
613 "scale mismatch for {k}: full={v_full} half={v_half}"
614 );
615 }
616 }
617
618 #[test]
619 fn blend_to_weights_zero_intensity_returns_all_zeros() {
620 let b = blender();
621 let w = b.blend_to_weights("Happy", 0.0).expect("should succeed");
622 for v in w.values() {
623 assert_eq!(*v, 0.0);
624 }
625 }
626
627 #[test]
628 fn blend_to_weights_neutral_returns_empty_map() {
629 let b = blender();
630 let w = b.blend_to_weights("Neutral", 1.0).expect("should succeed");
631 assert!(w.is_empty(), "Neutral should have no targets");
632 }
633
634 #[test]
635 fn blend_to_weights_unknown_expression_returns_none() {
636 let b = blender();
637 assert!(b.blend_to_weights("NonExistentXYZ", 1.0).is_none());
638 }
639
640 #[test]
641 fn blend_to_weights_clamps_intensity_over_one() {
642 let b = blender();
643 let w_one = b.blend_to_weights("Happy", 1.0).expect("should succeed");
644 let w_over = b.blend_to_weights("Happy", 2.5).expect("should succeed");
645 assert_eq!(w_one, w_over, "intensity > 1.0 should be clamped to 1.0");
646 }
647
648 #[test]
649 fn blend_to_weights_clamps_intensity_negative() {
650 let b = blender();
651 let w = b.blend_to_weights("Happy", -0.5).expect("should succeed");
652 for v in w.values() {
653 assert_eq!(*v, 0.0);
654 }
655 }
656
657 #[test]
660 fn blend_multi_two_expressions() {
661 let b = blender();
662 let exprs = vec![("Happy".to_string(), 0.6), ("Sad".to_string(), 0.4)];
663 let w = b.blend_multi(&exprs);
664 assert!(!w.is_empty());
665 for v in w.values() {
666 assert!(*v >= 0.0 && *v <= 1.0);
667 }
668 }
669
670 #[test]
671 fn blend_multi_skips_unknown_names() {
672 let b = blender();
673 let exprs = vec![("Happy".to_string(), 1.0), ("UnknownXYZ".to_string(), 1.0)];
674 let w = b.blend_multi(&exprs);
675 let w_happy = b.blend_to_weights("Happy", 1.0).expect("should succeed");
677 for k in w_happy.keys() {
678 assert!(w.contains_key(k));
679 }
680 }
681
682 #[test]
683 fn blend_multi_clamped_at_one() {
684 let b = blender();
685 let exprs = vec![("Happy".to_string(), 1.0), ("Happy".to_string(), 1.0)];
687 let w = b.blend_multi(&exprs);
688 for v in w.values() {
689 assert!(*v <= 1.0, "clamp failed: {v}");
690 }
691 }
692
693 #[test]
694 fn blend_multi_empty_input() {
695 let b = blender();
696 let w = b.blend_multi(&[]);
697 assert!(w.is_empty());
698 }
699
700 #[test]
703 fn lerp_at_t0_equals_from() {
704 let b = blender();
705 let lerped = b.lerp_expression("Happy", "Sad", 0.0);
706 let from = b.blend_to_weights("Happy", 1.0).expect("should succeed");
707 for (k, v) in &from {
708 let lv = lerped.get(k).copied().unwrap_or(0.0);
709 assert!((lv - v).abs() < 1e-10, "t=0 mismatch for {k}: {lv} vs {v}");
710 }
711 }
712
713 #[test]
714 fn lerp_at_t1_equals_to() {
715 let b = blender();
716 let lerped = b.lerp_expression("Happy", "Sad", 1.0);
717 let to_map = b.blend_to_weights("Sad", 1.0).expect("should succeed");
718 for (k, v) in &to_map {
719 let lv = lerped.get(k).copied().unwrap_or(0.0);
720 assert!((lv - v).abs() < 1e-10, "t=1 mismatch for {k}: {lv} vs {v}");
721 }
722 }
723
724 #[test]
725 fn lerp_at_t_half_midpoint() {
726 let b = blender();
727 let lerped = b.lerp_expression("Happy", "Angry", 0.5);
728 let happy = b.blend_to_weights("Happy", 1.0).expect("should succeed");
729 let angry = b.blend_to_weights("Angry", 1.0).expect("should succeed");
730 for k in happy.keys().chain(angry.keys()) {
731 let a = happy.get(k).copied().unwrap_or(0.0);
732 let c = angry.get(k).copied().unwrap_or(0.0);
733 let expected = (a * 0.5 + c * 0.5).clamp(0.0, 1.0);
734 let got = lerped.get(k).copied().unwrap_or(0.0);
735 assert!(
736 (got - expected).abs() < 1e-10,
737 "midpoint mismatch for {k}: expected {expected} got {got}"
738 );
739 }
740 }
741
742 #[test]
743 fn lerp_unknown_from_returns_to_at_t1() {
744 let b = blender();
745 let lerped = b.lerp_expression("NonExistent", "Happy", 1.0);
746 let to_map = b.blend_to_weights("Happy", 1.0).expect("should succeed");
747 for (k, v) in &to_map {
748 let lv = lerped.get(k).copied().unwrap_or(0.0);
749 assert!((lv - v).abs() < 1e-10, "t=1 mismatch for {k}");
750 }
751 }
752
753 #[test]
754 fn lerp_values_always_in_unit_interval() {
755 let b = blender();
756 for t in [0.0, 0.25, 0.5, 0.75, 1.0] {
757 let map = b.lerp_expression("Fearful", "Disgusted", t);
758 for v in map.values() {
759 assert!(*v >= 0.0 && *v <= 1.0, "out of range at t={t}: {v}");
760 }
761 }
762 }
763
764 #[test]
767 fn au_12_produces_lip_corner_puller() {
768 let w = ExpressionBlender::au_to_expression(12, 1.0);
769 assert!(
770 w.contains_key("mouth-corner-puller"),
771 "AU12 should drive mouth-corner-puller"
772 );
773 }
774
775 #[test]
776 fn au_4_produces_brow_lowerer() {
777 let w = ExpressionBlender::au_to_expression(4, 1.0);
778 assert!(
779 w.contains_key("eyebrows-left-down"),
780 "AU4 should lower brows"
781 );
782 assert!(w.contains_key("eyebrows-right-down"));
783 }
784
785 #[test]
786 fn au_intensity_scales_correctly() {
787 let w_full = ExpressionBlender::au_to_expression(12, 1.0);
788 let w_half = ExpressionBlender::au_to_expression(12, 0.5);
789 for (k, v_full) in &w_full {
790 let v_half = w_half[k];
791 assert!(
792 (v_half - v_full * 0.5).abs() < 1e-10,
793 "AU12 scale mismatch for {k}"
794 );
795 }
796 }
797
798 #[test]
799 fn au_unknown_code_returns_empty() {
800 let w = ExpressionBlender::au_to_expression(999, 1.0);
801 assert!(w.is_empty(), "unknown AU should return empty map");
802 }
803
804 #[test]
805 fn au_zero_intensity_returns_all_zeros() {
806 let w = ExpressionBlender::au_to_expression(6, 0.0);
807 for v in w.values() {
808 assert_eq!(*v, 0.0);
809 }
810 }
811
812 #[test]
813 fn au_values_always_clamped() {
814 for au in [1, 2, 4, 5, 6, 7, 9, 12, 15, 25, 26, 43, 45, 46] {
815 let w = ExpressionBlender::au_to_expression(au, 1.5);
816 for v in w.values() {
817 assert!(*v <= 1.0, "AU{au} weight > 1.0: {v}");
818 }
819 }
820 }
821
822 #[test]
823 fn au_45_blink_both_eyes() {
824 let w = ExpressionBlender::au_to_expression(45, 1.0);
825 assert!(w.contains_key("eye-left-blink"));
826 assert!(w.contains_key("eye-right-blink"));
827 }
828
829 #[test]
830 fn au_46_wink_right_eye_only() {
831 let w = ExpressionBlender::au_to_expression(46, 1.0);
832 assert!(w.contains_key("eye-right-blink"));
833 assert!(
834 !w.contains_key("eye-left-blink"),
835 "AU46 wink should be right eye only"
836 );
837 }
838
839 #[test]
842 fn get_is_case_insensitive() {
843 let b = blender();
844 assert!(b.get("happy").is_some());
845 assert!(b.get("HAPPY").is_some());
846 assert!(b.get("HaPpY").is_some());
847 }
848
849 #[test]
852 fn happy_has_basic_tag() {
853 let b = blender();
854 let def = b.get("Happy").expect("should succeed");
855 assert!(
856 def.tags.iter().any(|t| t == "basic"),
857 "Happy should have 'basic' tag"
858 );
859 }
860
861 #[test]
862 fn contempt_has_symmetry_pair() {
863 let b = blender();
864 let def = b.get("Contempt").expect("should succeed");
865 assert!(
866 def.symmetry_pair.is_some(),
867 "Contempt should have a symmetry_pair"
868 );
869 }
870}