1#![allow(dead_code)]
10
11use std::collections::HashMap;
12
13#[derive(Debug, Clone)]
19pub struct ExpressionPreset {
20 pub name: String,
22 pub description: String,
24 pub weights: HashMap<String, f32>,
26 pub intensity: f32,
28 pub tags: Vec<String>,
30}
31
32impl ExpressionPreset {
33 pub fn new(
35 name: impl Into<String>,
36 description: impl Into<String>,
37 weights: HashMap<String, f32>,
38 ) -> Self {
39 Self {
40 name: name.into(),
41 description: description.into(),
42 weights,
43 intensity: 1.0,
44 tags: Vec::new(),
45 }
46 }
47
48 pub fn with_intensity(mut self, intensity: f32) -> Self {
50 self.intensity = intensity.clamp(0.0, 1.0);
51 self
52 }
53
54 pub fn with_tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
56 self.tags = tags.into_iter().map(|t| t.into()).collect();
57 self
58 }
59}
60
61pub struct ExpressionLibrary {
67 presets: HashMap<String, ExpressionPreset>,
68}
69
70impl ExpressionLibrary {
71 pub fn new() -> Self {
73 Self {
74 presets: HashMap::new(),
75 }
76 }
77
78 pub fn add(&mut self, preset: ExpressionPreset) {
80 self.presets.insert(preset.name.clone(), preset);
81 }
82
83 pub fn get(&self, name: &str) -> Option<&ExpressionPreset> {
85 self.presets.get(name)
86 }
87
88 pub fn list_names(&self) -> Vec<&str> {
90 let mut names: Vec<&str> = self.presets.keys().map(|s| s.as_str()).collect();
91 names.sort_unstable();
92 names
93 }
94
95 pub fn list_by_tag(&self, tag: &str) -> Vec<&ExpressionPreset> {
97 let mut result: Vec<&ExpressionPreset> = self
98 .presets
99 .values()
100 .filter(|p| p.tags.iter().any(|t| t == tag))
101 .collect();
102 result.sort_by(|a, b| a.name.cmp(&b.name));
103 result
104 }
105
106 pub fn count(&self) -> usize {
108 self.presets.len()
109 }
110
111 pub fn default_library() -> Self {
117 let mut lib = Self::new();
118
119 macro_rules! preset {
121 ($name:expr, $desc:expr, $intensity:expr, $tags:expr, $($key:expr => $val:expr),* $(,)?) => {{
122 #[allow(unused_mut)]
123 let mut w = HashMap::new();
124 $( w.insert($key.to_string(), $val as f32); )*
125 ExpressionPreset::new($name, $desc, w)
126 .with_intensity($intensity)
127 .with_tags($tags)
128 }};
129 }
130
131 lib.add(preset!(
133 "neutral",
134 "Relaxed, expressionless face",
135 1.0,
136 ["neutral", "base"],
137 ));
139
140 lib.add(preset!(
142 "smile", "Genuine open smile with cheek raise", 1.0,
143 ["happy", "positive", "mouth", "cheek"],
144 "mouth_smile_L" => 0.9_f32,
145 "mouth_smile_R" => 0.9_f32,
146 "cheek_raise_L" => 0.6_f32,
147 "cheek_raise_R" => 0.6_f32,
148 "lip_upper_raise" => 0.3_f32,
149 ));
150
151 lib.add(preset!(
153 "frown", "Downturned mouth with furrowed brows", 1.0,
154 ["sad", "negative", "mouth", "brow"],
155 "mouth_frown_L" => 0.8_f32,
156 "mouth_frown_R" => 0.8_f32,
157 "brow_lower_L" => 0.5_f32,
158 "brow_lower_R" => 0.5_f32,
159 "lip_lower_drop" => 0.2_f32,
160 ));
161
162 lib.add(preset!(
164 "surprise", "Wide eyes and open mouth", 1.0,
165 ["surprise", "positive", "mouth", "eye", "brow"],
166 "brow_raise_L" => 0.9_f32,
167 "brow_raise_R" => 0.9_f32,
168 "eye_wide_L" => 0.8_f32,
169 "eye_wide_R" => 0.8_f32,
170 "jaw_open" => 0.7_f32,
171 "lip_upper_raise" => 0.4_f32,
172 "lip_lower_drop" => 0.4_f32,
173 ));
174
175 lib.add(preset!(
177 "anger", "Furrowed brows and compressed lips", 1.0,
178 ["angry", "negative", "brow", "mouth"],
179 "brow_lower_L" => 0.8_f32,
180 "brow_lower_R" => 0.8_f32,
181 "brow_inner_raise_L" => 0.4_f32,
182 "brow_inner_raise_R" => 0.4_f32,
183 "nose_wrinkle" => 0.3_f32,
184 "mouth_compress" => 0.7_f32,
185 "jaw_clench" => 0.5_f32,
186 ));
187
188 lib.add(preset!(
190 "disgust", "Nose wrinkle and upper lip curl", 1.0,
191 ["disgust", "negative", "nose", "mouth"],
192 "nose_wrinkle" => 0.8_f32,
193 "lip_upper_raise" => 0.6_f32,
194 "mouth_frown_L" => 0.4_f32,
195 "mouth_frown_R" => 0.4_f32,
196 "brow_lower_L" => 0.3_f32,
197 "brow_lower_R" => 0.3_f32,
198 ));
199
200 lib.add(preset!(
202 "fear", "Raised brows and parted lips", 1.0,
203 ["fear", "negative", "brow", "mouth", "eye"],
204 "brow_raise_L" => 0.7_f32,
205 "brow_raise_R" => 0.7_f32,
206 "brow_inner_raise_L" => 0.8_f32,
207 "brow_inner_raise_R" => 0.8_f32,
208 "eye_wide_L" => 0.6_f32,
209 "eye_wide_R" => 0.6_f32,
210 "jaw_open" => 0.3_f32,
211 "lip_stretch_L" => 0.5_f32,
212 "lip_stretch_R" => 0.5_f32,
213 ));
214
215 lib.add(preset!(
217 "contempt", "One-sided mouth raise (sneer)", 1.0,
218 ["contempt", "negative", "mouth"],
219 "mouth_smile_L" => 0.5_f32,
220 "mouth_frown_R" => 0.3_f32,
221 "brow_raise_L" => 0.2_f32,
222 "brow_lower_R" => 0.3_f32,
223 ));
224
225 lib.add(preset!(
227 "blink", "Both eyelids fully closed", 1.0,
228 ["blink", "eye"],
229 "eye_close_L" => 1.0_f32,
230 "eye_close_R" => 1.0_f32,
231 ));
232
233 lib.add(preset!(
235 "wink_left", "Left eye wink (right eye open)", 1.0,
236 ["wink", "eye", "left"],
237 "eye_close_L" => 1.0_f32,
238 "eye_close_R" => 0.0_f32,
239 ));
240
241 lib.add(preset!(
243 "wink_right", "Right eye wink (left eye open)", 1.0,
244 ["wink", "eye", "right"],
245 "eye_close_L" => 0.0_f32,
246 "eye_close_R" => 1.0_f32,
247 ));
248
249 lib
250 }
251
252 pub fn blend(&self, name_a: &str, name_b: &str, t: f32) -> Option<HashMap<String, f32>> {
261 let a = self.presets.get(name_a)?;
262 let b = self.presets.get(name_b)?;
263 Some(lerp_weight_maps(&a.weights, &b.weights, t))
264 }
265
266 pub fn apply_intensity(preset: &ExpressionPreset) -> HashMap<String, f32> {
268 preset
269 .weights
270 .iter()
271 .map(|(k, &v)| (k.clone(), (v * preset.intensity).clamp(0.0, 1.0)))
272 .collect()
273 }
274
275 pub fn combine(presets: &[&ExpressionPreset]) -> HashMap<String, f32> {
277 let mut result: HashMap<String, f32> = HashMap::new();
278 for preset in presets {
279 for (key, &val) in &preset.weights {
280 let entry = result.entry(key.clone()).or_insert(0.0);
281 *entry = (*entry + val * preset.intensity).clamp(0.0, 1.0);
282 }
283 }
284 result
285 }
286
287 pub fn random_blend(presets: &[&ExpressionPreset], seed: u32) -> HashMap<String, f32> {
291 if presets.is_empty() {
292 return HashMap::new();
293 }
294 if presets.len() == 1 {
295 return presets[0].weights.clone();
296 }
297
298 let mut state = seed.wrapping_add(1);
300 let mut lcg = || -> f32 {
301 state = state.wrapping_mul(1664525).wrapping_add(1013904223);
302 (state >> 16) as f32 / 65535.0
303 };
304
305 let n = if presets.len() >= 3 && lcg() > 0.5 {
307 3
308 } else {
309 2
310 };
311 let n = n.min(presets.len());
312
313 let mut indices: Vec<usize> = Vec::with_capacity(n);
315 while indices.len() < n {
316 let idx = (lcg() * presets.len() as f32) as usize % presets.len();
317 if !indices.contains(&idx) {
318 indices.push(idx);
319 }
320 }
321
322 let mut raw_weights: Vec<f32> = (0..n).map(|_| lcg() + 0.1).collect();
324 let total: f32 = raw_weights.iter().sum();
325 for w in &mut raw_weights {
326 *w /= total;
327 }
328
329 let mut result: HashMap<String, f32> = HashMap::new();
331 for (i, &preset_idx) in indices.iter().enumerate() {
332 let mix = raw_weights[i];
333 for (key, &val) in &presets[preset_idx].weights {
334 let entry = result.entry(key.clone()).or_insert(0.0);
335 *entry = (*entry + val * mix).clamp(0.0, 1.0);
336 }
337 }
338 result
339 }
340
341 pub fn find_nearest<'a>(&'a self, weights: &HashMap<String, f32>) -> Option<&'a str> {
345 self.presets
346 .values()
347 .min_by(|a, b| {
348 let da = expression_distance(&a.weights, weights);
349 let db = expression_distance(&b.weights, weights);
350 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
351 })
352 .map(|p| p.name.as_str())
353 }
354}
355
356impl Default for ExpressionLibrary {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362pub fn expression_distance(a: &HashMap<String, f32>, b: &HashMap<String, f32>) -> f32 {
370 let mut sum_sq = 0.0f32;
371
372 for (k, &va) in a {
374 let vb = b.get(k).copied().unwrap_or(0.0);
375 let d = va - vb;
376 sum_sq += d * d;
377 }
378 for (k, &vb) in b {
380 if !a.contains_key(k) {
381 sum_sq += vb * vb;
382 }
383 }
384 sum_sq.sqrt()
385}
386
387#[derive(Debug, Clone)]
397pub struct ExpressionBlender {
398 pub anchors: Vec<(String, f32)>,
400}
401
402impl ExpressionBlender {
403 pub fn new() -> Self {
405 Self {
406 anchors: Vec::new(),
407 }
408 }
409
410 pub fn add_anchor(&mut self, name: String, weight: f32) {
412 self.anchors.push((name, weight));
413 }
414
415 pub fn evaluate(&self, library: &ExpressionLibrary) -> HashMap<String, f32> {
419 let mut result: HashMap<String, f32> = HashMap::new();
420 for (name, anchor_w) in &self.anchors {
421 if let Some(preset) = library.get(name) {
422 for (key, &val) in &preset.weights {
423 let entry = result.entry(key.clone()).or_insert(0.0);
424 *entry = (*entry + val * preset.intensity * anchor_w).clamp(0.0, 1.0);
425 }
426 }
427 }
428 result
429 }
430
431 pub fn normalize(&mut self) {
435 let total: f32 = self.anchors.iter().map(|(_, w)| w).sum();
436 if total > 0.0 {
437 for (_, w) in &mut self.anchors {
438 *w /= total;
439 }
440 }
441 }
442}
443
444impl Default for ExpressionBlender {
445 fn default() -> Self {
446 Self::new()
447 }
448}
449
450#[derive(Debug, Clone)]
456#[allow(dead_code)]
457pub struct ExpressionLibConfig {
458 pub max_presets: usize,
460 pub allow_overwrite: bool,
462 pub default_intensity: f32,
464}
465
466#[allow(dead_code)]
468pub fn default_library_config() -> ExpressionLibConfig {
469 ExpressionLibConfig {
470 max_presets: 0,
471 allow_overwrite: true,
472 default_intensity: 1.0,
473 }
474}
475
476#[allow(dead_code)]
478pub fn new_expression_library() -> ExpressionLibrary {
479 ExpressionLibrary::new()
480}
481
482#[allow(dead_code)]
484pub fn add_preset(lib: &mut ExpressionLibrary, preset: ExpressionPreset) {
485 lib.add(preset);
486}
487
488#[allow(dead_code)]
490pub fn remove_preset(lib: &mut ExpressionLibrary, name: &str) -> bool {
491 lib.presets.remove(name).is_some()
492}
493
494#[allow(dead_code)]
496pub fn get_preset<'a>(lib: &'a ExpressionLibrary, name: &str) -> Option<&'a ExpressionPreset> {
497 lib.get(name)
498}
499
500#[allow(dead_code)]
502pub fn preset_count(lib: &ExpressionLibrary) -> usize {
503 lib.count()
504}
505
506#[allow(dead_code)]
508pub fn find_preset_by_name<'a>(
509 lib: &'a ExpressionLibrary,
510 substring: &str,
511) -> Option<&'a ExpressionPreset> {
512 let lower = substring.to_lowercase();
513 lib.presets
514 .values()
515 .find(|p| p.name.to_lowercase().contains(&lower))
516}
517
518#[allow(dead_code)]
520pub fn blend_presets(
521 lib: &ExpressionLibrary,
522 name_a: &str,
523 name_b: &str,
524 t: f32,
525) -> Option<HashMap<String, f32>> {
526 lib.blend(name_a, name_b, t)
527}
528
529#[allow(dead_code)]
531pub fn library_to_json(lib: &ExpressionLibrary) -> String {
532 let mut out = String::from("{\"presets\":[");
533 let names = lib.list_names();
534 for (i, name) in names.iter().enumerate() {
535 let Some(p) = lib.get(name) else { continue };
536 out.push_str(&format!(
537 "{{\"name\":\"{}\",\"description\":\"{}\",\"intensity\":{:.4},\"weight_count\":{}}}",
538 p.name,
539 p.description,
540 p.intensity,
541 p.weights.len(),
542 ));
543 if i + 1 < names.len() {
544 out.push(',');
545 }
546 }
547 out.push_str("]}");
548 out
549}
550
551#[allow(dead_code)]
553pub fn build_basic_library() -> ExpressionLibrary {
554 let mut lib = ExpressionLibrary::new();
555
556 macro_rules! p {
557 ($name:expr, $desc:expr, $tags:expr, $($k:expr => $v:expr),* $(,)?) => {{
558 #[allow(unused_mut)]
559 let mut w = HashMap::new();
560 $( w.insert($k.to_string(), $v as f32); )*
561 ExpressionPreset::new($name, $desc, w).with_tags($tags)
562 }};
563 }
564
565 lib.add(p!("neutral", "Neutral resting expression", ["neutral"],));
566 lib.add(p!(
567 "happy", "Happiness / joy", ["happy", "positive"],
568 "mouth_smile_L" => 0.9_f32,
569 "mouth_smile_R" => 0.9_f32,
570 "cheek_raise_L" => 0.55_f32,
571 "cheek_raise_R" => 0.55_f32,
572 ));
573 lib.add(p!(
574 "sad", "Sadness / sorrow", ["sad", "negative"],
575 "mouth_frown_L" => 0.8_f32,
576 "mouth_frown_R" => 0.8_f32,
577 "brow_lower_L" => 0.4_f32,
578 "brow_lower_R" => 0.4_f32,
579 "lip_lower_drop" => 0.2_f32,
580 ));
581 lib.add(p!(
582 "angry", "Anger / aggression", ["angry", "negative"],
583 "brow_lower_L" => 0.85_f32,
584 "brow_lower_R" => 0.85_f32,
585 "nose_wrinkle" => 0.3_f32,
586 "mouth_compress" => 0.7_f32,
587 ));
588 lib.add(p!(
589 "fearful", "Fear / apprehension", ["fear", "negative"],
590 "brow_raise_L" => 0.7_f32,
591 "brow_raise_R" => 0.7_f32,
592 "brow_inner_raise_L" => 0.8_f32,
593 "brow_inner_raise_R" => 0.8_f32,
594 "eye_wide_L" => 0.6_f32,
595 "eye_wide_R" => 0.6_f32,
596 "jaw_open" => 0.25_f32,
597 ));
598 lib.add(p!(
599 "disgusted", "Disgust", ["disgust", "negative"],
600 "nose_wrinkle" => 0.85_f32,
601 "lip_upper_raise" => 0.6_f32,
602 "mouth_frown_L" => 0.35_f32,
603 "mouth_frown_R" => 0.35_f32,
604 ));
605 lib.add(p!(
606 "surprised", "Surprise", ["surprise", "positive"],
607 "brow_raise_L" => 0.9_f32,
608 "brow_raise_R" => 0.9_f32,
609 "eye_wide_L" => 0.8_f32,
610 "eye_wide_R" => 0.8_f32,
611 "jaw_open" => 0.7_f32,
612 "lip_upper_raise" => 0.4_f32,
613 "lip_lower_drop" => 0.4_f32,
614 ));
615
616 lib
617}
618
619#[allow(dead_code)]
621pub fn preset_morph_weights(lib: &ExpressionLibrary, name: &str) -> Option<HashMap<String, f32>> {
622 let p = lib.get(name)?;
623 Some(ExpressionLibrary::apply_intensity(p))
624}
625
626#[allow(dead_code)]
628pub fn list_preset_names(lib: &ExpressionLibrary) -> Vec<String> {
629 lib.list_names().iter().map(|s| s.to_string()).collect()
630}
631
632fn lerp_weight_maps(
638 a: &HashMap<String, f32>,
639 b: &HashMap<String, f32>,
640 t: f32,
641) -> HashMap<String, f32> {
642 let t = t.clamp(0.0, 1.0);
643 let mut result: HashMap<String, f32> = HashMap::new();
644
645 for (k, &va) in a {
646 let vb = b.get(k).copied().unwrap_or(0.0);
647 result.insert(k.clone(), va + (vb - va) * t);
648 }
649 for (k, &vb) in b {
650 if !a.contains_key(k) {
651 result.insert(k.clone(), vb * t);
652 }
653 }
654 result
655}
656
657#[cfg(test)]
662mod tests {
663 use super::*;
664
665 fn wmap(pairs: &[(&str, f32)]) -> HashMap<String, f32> {
667 pairs.iter().map(|&(k, v)| (k.to_string(), v)).collect()
668 }
669
670 #[test]
674 fn default_library_has_eleven_presets() {
675 let lib = ExpressionLibrary::default_library();
676 assert!(
677 lib.count() >= 11,
678 "expected ≥11 presets, got {}",
679 lib.count()
680 );
681 }
682
683 #[test]
687 fn list_names_is_sorted() {
688 let lib = ExpressionLibrary::default_library();
689 let names = lib.list_names();
690 let mut sorted = names.clone();
691 sorted.sort_unstable();
692 assert_eq!(names, sorted);
693 }
694
695 #[test]
699 fn get_known_preset_returns_some() {
700 let lib = ExpressionLibrary::default_library();
701 for name in &[
702 "neutral",
703 "smile",
704 "frown",
705 "blink",
706 "wink_left",
707 "wink_right",
708 ] {
709 assert!(lib.get(name).is_some(), "preset '{}' must exist", name);
710 }
711 }
712
713 #[test]
717 fn get_unknown_preset_returns_none() {
718 let lib = ExpressionLibrary::default_library();
719 assert!(lib.get("__nonexistent__").is_none());
720 }
721
722 #[test]
726 fn list_by_tag_returns_correct_presets() {
727 let lib = ExpressionLibrary::default_library();
728 let eye_presets = lib.list_by_tag("eye");
729 let names: Vec<&str> = eye_presets.iter().map(|p| p.name.as_str()).collect();
731 assert!(names.contains(&"blink"), "blink should be tagged 'eye'");
732 assert!(
733 names.contains(&"wink_left"),
734 "wink_left should be tagged 'eye'"
735 );
736 assert!(
737 names.contains(&"wink_right"),
738 "wink_right should be tagged 'eye'"
739 );
740 }
741
742 #[test]
746 fn blend_at_t0_equals_a() {
747 let lib = ExpressionLibrary::default_library();
748 let blended = lib
749 .blend("smile", "frown", 0.0)
750 .expect("blend must succeed");
751 let smile = lib.get("smile").expect("should succeed");
752 for (k, &va) in &smile.weights {
753 let bv = blended.get(k).copied().unwrap_or(0.0);
754 assert!(
755 (bv - va).abs() < 1e-5,
756 "key '{}': expected {}, got {}",
757 k,
758 va,
759 bv
760 );
761 }
762 }
763
764 #[test]
768 fn blend_at_t1_equals_b() {
769 let lib = ExpressionLibrary::default_library();
770 let blended = lib
771 .blend("smile", "frown", 1.0)
772 .expect("blend must succeed");
773 let frown = lib.get("frown").expect("should succeed");
774 for (k, &vb) in &frown.weights {
775 let bv = blended.get(k).copied().unwrap_or(0.0);
776 assert!(
777 (bv - vb).abs() < 1e-5,
778 "key '{}': expected {}, got {}",
779 k,
780 vb,
781 bv
782 );
783 }
784 }
785
786 #[test]
790 fn blend_unknown_name_returns_none() {
791 let lib = ExpressionLibrary::default_library();
792 assert!(lib.blend("smile", "__no__", 0.5).is_none());
793 assert!(lib.blend("__no__", "frown", 0.5).is_none());
794 }
795
796 #[test]
800 fn apply_intensity_scales_correctly() {
801 let mut preset = ExpressionPreset::new(
802 "test",
803 "desc",
804 wmap(&[("eye_close_L", 0.8), ("eye_close_R", 0.6)]),
805 );
806 preset.intensity = 0.5;
807 let scaled = ExpressionLibrary::apply_intensity(&preset);
808 assert!((scaled["eye_close_L"] - 0.4).abs() < 1e-5);
809 assert!((scaled["eye_close_R"] - 0.3).abs() < 1e-5);
810 }
811
812 #[test]
816 fn combine_clamps_to_unit() {
817 let lib = ExpressionLibrary::default_library();
818 let smile = lib.get("smile").expect("should succeed");
819 let blink = lib.get("blink").expect("should succeed");
820 let combined = ExpressionLibrary::combine(&[smile, smile, blink]);
821 for &v in combined.values() {
822 assert!(
823 (0.0..=1.0).contains(&v),
824 "combined weight {} is out of [0, 1]",
825 v
826 );
827 }
828 }
829
830 #[test]
834 fn expression_distance_same_is_zero() {
835 let m = wmap(&[("a", 0.5), ("b", 0.3)]);
836 let d = expression_distance(&m, &m);
837 assert!(
838 d.abs() < 1e-6,
839 "distance of map to itself must be 0, got {}",
840 d
841 );
842 }
843
844 #[test]
848 fn expression_distance_triangle_inequality() {
849 let a = wmap(&[("x", 0.0), ("y", 0.0)]);
850 let b = wmap(&[("x", 1.0), ("y", 0.0)]);
851 let c = wmap(&[("x", 0.5), ("y", 0.5)]);
852 let dab = expression_distance(&a, &b);
853 let dac = expression_distance(&a, &c);
854 let dcb = expression_distance(&c, &b);
855 assert!(
856 dab <= dac + dcb + 1e-5,
857 "triangle inequality failed: {} > {} + {}",
858 dab,
859 dac,
860 dcb
861 );
862 }
863
864 #[test]
868 fn find_nearest_empty_returns_neutral() {
869 let lib = ExpressionLibrary::default_library();
870 let nearest = lib.find_nearest(&HashMap::new()).expect("must return Some");
872 assert_eq!(nearest, "neutral");
873 }
874
875 #[test]
879 fn expression_blender_normalize_sums_to_one() {
880 let mut blender = ExpressionBlender::new();
881 blender.add_anchor("smile".to_string(), 2.0);
882 blender.add_anchor("frown".to_string(), 2.0);
883 blender.normalize();
884 let total: f32 = blender.anchors.iter().map(|(_, w)| w).sum();
885 assert!(
886 (total - 1.0).abs() < 1e-5,
887 "total after normalize = {}",
888 total
889 );
890 }
891
892 #[test]
896 fn expression_blender_evaluate_in_unit() {
897 let lib = ExpressionLibrary::default_library();
898 let mut blender = ExpressionBlender::new();
899 blender.add_anchor("smile".to_string(), 0.5);
900 blender.add_anchor("surprise".to_string(), 0.5);
901 blender.add_anchor("anger".to_string(), 0.5);
902 let result = blender.evaluate(&lib);
903 for &v in result.values() {
904 assert!((0.0..=1.0).contains(&v), "value {} out of [0,1]", v);
905 }
906 }
907
908 #[test]
912 fn random_blend_in_unit_range() {
913 let lib = ExpressionLibrary::default_library();
914 let names = lib.list_names();
915 let presets: Vec<&ExpressionPreset> = names.iter().filter_map(|n| lib.get(n)).collect();
916 for seed in [0u32, 1, 42, 999, u32::MAX] {
917 let result = ExpressionLibrary::random_blend(&presets, seed);
918 for &v in result.values() {
919 assert!(
920 (0.0..=1.0).contains(&v),
921 "seed={seed}: value {v} out of [0,1]"
922 );
923 }
924 }
925 }
926
927 #[test]
931 fn add_increases_count() {
932 let mut lib = ExpressionLibrary::new();
933 assert_eq!(lib.count(), 0);
934 lib.add(ExpressionPreset::new("a", "desc", HashMap::new()));
935 assert_eq!(lib.count(), 1);
936 lib.add(ExpressionPreset::new("b", "desc", HashMap::new()));
937 assert_eq!(lib.count(), 2);
938 lib.add(ExpressionPreset::new("a", "overwrite", HashMap::new()));
940 assert_eq!(lib.count(), 2);
941 }
942
943 #[test]
947 fn write_expression_names_to_tmp() {
948 let lib = ExpressionLibrary::default_library();
949 let names = lib.list_names().join("\n");
950 std::fs::write("/tmp/oxihuman_expression_library_names.txt", &names)
951 .expect("write to /tmp/ must succeed");
952 let read_back = std::fs::read_to_string("/tmp/oxihuman_expression_library_names.txt")
953 .expect("should succeed");
954 assert_eq!(read_back, names);
955 }
956
957 #[test]
961 fn default_library_config_defaults() {
962 let cfg = default_library_config();
963 assert_eq!(cfg.max_presets, 0);
964 assert!(cfg.allow_overwrite);
965 assert!((cfg.default_intensity - 1.0).abs() < 1e-6);
966 }
967
968 #[test]
972 fn new_expression_library_is_empty() {
973 let lib = new_expression_library();
974 assert_eq!(preset_count(&lib), 0);
975 }
976
977 #[test]
981 fn add_remove_preset_round_trip() {
982 let mut lib = new_expression_library();
983 add_preset(&mut lib, ExpressionPreset::new("test", "d", HashMap::new()));
984 assert_eq!(preset_count(&lib), 1);
985 let removed = remove_preset(&mut lib, "test");
986 assert!(removed);
987 assert_eq!(preset_count(&lib), 0);
988 assert!(!remove_preset(&mut lib, "test"));
989 }
990
991 #[test]
995 fn get_preset_correct() {
996 let lib = ExpressionLibrary::default_library();
997 let p = get_preset(&lib, "smile");
998 assert!(p.is_some());
999 assert_eq!(p.expect("should succeed").name, "smile");
1000 }
1001
1002 #[test]
1006 fn find_preset_by_name_substring() {
1007 let lib = ExpressionLibrary::default_library();
1008 let p = find_preset_by_name(&lib, "wink");
1009 assert!(p.is_some());
1010 assert!(p.expect("should succeed").name.contains("wink"));
1011 }
1012
1013 #[test]
1017 fn blend_presets_at_midpoint() {
1018 let lib = ExpressionLibrary::default_library();
1019 let result = blend_presets(&lib, "smile", "frown", 0.5).expect("blend must succeed");
1020 assert!(!result.is_empty());
1021 }
1022
1023 #[test]
1027 fn library_to_json_contains_names() {
1028 let lib = ExpressionLibrary::default_library();
1029 let json = library_to_json(&lib);
1030 assert!(json.contains("smile"));
1031 assert!(json.contains("neutral"));
1032 }
1033
1034 #[test]
1038 fn build_basic_library_has_seven_presets() {
1039 let lib = build_basic_library();
1040 assert_eq!(lib.count(), 7);
1041 }
1042
1043 #[test]
1047 fn build_basic_library_emotion_names() {
1048 let lib = build_basic_library();
1049 for name in &[
1050 "neutral",
1051 "happy",
1052 "sad",
1053 "angry",
1054 "fearful",
1055 "disgusted",
1056 "surprised",
1057 ] {
1058 assert!(lib.get(name).is_some(), "missing preset: {name}");
1059 }
1060 }
1061
1062 #[test]
1066 fn preset_morph_weights_applies_intensity() {
1067 let mut lib = new_expression_library();
1068 let mut p = ExpressionPreset::new("x", "d", wmap(&[("k", 0.8)]));
1069 p.intensity = 0.5;
1070 add_preset(&mut lib, p);
1071 let weights = preset_morph_weights(&lib, "x").expect("should succeed");
1072 assert!((weights["k"] - 0.4).abs() < 1e-5);
1073 }
1074
1075 #[test]
1079 fn list_preset_names_sorted() {
1080 let lib = ExpressionLibrary::default_library();
1081 let names = list_preset_names(&lib);
1082 let mut sorted = names.clone();
1083 sorted.sort();
1084 assert_eq!(names, sorted);
1085 }
1086}