Skip to main content

oxihuman_morph/
expression_library.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Named facial expression preset library.
5//!
6//! Provides a collection of morph-target weight maps for common facial expressions,
7//! along with blending, combining, and nearest-neighbour search utilities.
8
9#![allow(dead_code)]
10
11use std::collections::HashMap;
12
13// ---------------------------------------------------------------------------
14// ExpressionPreset
15// ---------------------------------------------------------------------------
16
17/// A named facial expression preset: a set of morph target weights.
18#[derive(Debug, Clone)]
19pub struct ExpressionPreset {
20    /// Unique expression name (e.g. "smile", "anger").
21    pub name: String,
22    /// Human-readable description.
23    pub description: String,
24    /// Morph target name → base weight [0..1].
25    pub weights: HashMap<String, f32>,
26    /// Global intensity scale [0..1] applied on top of individual weights.
27    pub intensity: f32,
28    /// Semantic tags, e.g. `["happy", "positive", "mouth"]`.
29    pub tags: Vec<String>,
30}
31
32impl ExpressionPreset {
33    /// Create a new preset with intensity 1.0 and no tags.
34    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    /// Builder: set intensity.
49    pub fn with_intensity(mut self, intensity: f32) -> Self {
50        self.intensity = intensity.clamp(0.0, 1.0);
51        self
52    }
53
54    /// Builder: set tags.
55    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
61// ---------------------------------------------------------------------------
62// ExpressionLibrary
63// ---------------------------------------------------------------------------
64
65/// A library of named expression presets.
66pub struct ExpressionLibrary {
67    presets: HashMap<String, ExpressionPreset>,
68}
69
70impl ExpressionLibrary {
71    /// Create an empty library.
72    pub fn new() -> Self {
73        Self {
74            presets: HashMap::new(),
75        }
76    }
77
78    /// Add a preset to the library (keyed by `preset.name`).
79    pub fn add(&mut self, preset: ExpressionPreset) {
80        self.presets.insert(preset.name.clone(), preset);
81    }
82
83    /// Look up a preset by name.
84    pub fn get(&self, name: &str) -> Option<&ExpressionPreset> {
85        self.presets.get(name)
86    }
87
88    /// Return sorted list of all preset names.
89    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    /// Return all presets that carry the given tag.
96    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    /// Number of presets in the library.
107    pub fn count(&self) -> usize {
108        self.presets.len()
109    }
110
111    // -----------------------------------------------------------------------
112    // Default library
113    // -----------------------------------------------------------------------
114
115    /// Build a default library with ~11 common facial expressions.
116    pub fn default_library() -> Self {
117        let mut lib = Self::new();
118
119        // Helper macro to reduce boilerplate
120        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        // neutral
132        lib.add(preset!(
133            "neutral",
134            "Relaxed, expressionless face",
135            1.0,
136            ["neutral", "base"],
137            // No morphs active — all at zero is represented by absence of keys
138        ));
139
140        // smile
141        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        // frown
152        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        // surprise
163        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        // anger
176        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        // disgust
189        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        // fear
201        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        // contempt
216        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        // blink
226        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        // wink_left
234        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        // wink_right
242        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    // -----------------------------------------------------------------------
253    // Blending utilities
254    // -----------------------------------------------------------------------
255
256    /// Linearly interpolate morph weights between two named presets.
257    ///
258    /// `t = 0.0` → all weights from `name_a`; `t = 1.0` → all weights from `name_b`.
259    /// Keys not present in a preset are treated as weight 0.0.
260    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    /// Scale a preset's weights by its `intensity` field.
267    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    /// Additively combine multiple presets (clamped to [0, 1]).
276    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    /// Generate a pseudo-random blend of 2–3 presets from the given slice.
288    ///
289    /// Uses a simple LCG seeded by `seed` to pick indices and mixing weights.
290    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        // LCG random
299        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        // Pick 2 or 3 presets
306        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        // Pick distinct indices
314        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        // Random mixing weights (normalised)
323        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        // Weighted sum
330        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    /// Find the preset most similar to the given weight map (L2 distance).
342    ///
343    /// Returns `None` if the library is empty.
344    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
362// ---------------------------------------------------------------------------
363// expression_distance
364// ---------------------------------------------------------------------------
365
366/// L2 distance between two morph weight maps over the union of their keys.
367///
368/// Keys absent from a map contribute a value of 0.0.
369pub fn expression_distance(a: &HashMap<String, f32>, b: &HashMap<String, f32>) -> f32 {
370    let mut sum_sq = 0.0f32;
371
372    // Keys in a
373    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    // Keys only in b
379    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// ---------------------------------------------------------------------------
388// ExpressionBlender
389// ---------------------------------------------------------------------------
390
391/// Interpolate between multiple named expressions using (name, weight) anchors.
392///
393/// This is a generalised barycentric blender: each anchor contributes its
394/// preset's weights scaled by the anchor weight, then all contributions are
395/// summed and clamped.
396#[derive(Debug, Clone)]
397pub struct ExpressionBlender {
398    /// (preset name, blending weight) pairs.
399    pub anchors: Vec<(String, f32)>,
400}
401
402impl ExpressionBlender {
403    /// Create a new blender with no anchors.
404    pub fn new() -> Self {
405        Self {
406            anchors: Vec::new(),
407        }
408    }
409
410    /// Add an anchor (preset name, weight).
411    pub fn add_anchor(&mut self, name: String, weight: f32) {
412        self.anchors.push((name, weight));
413    }
414
415    /// Evaluate the blend against the given library.
416    ///
417    /// Missing presets are silently skipped. Result is clamped to [0, 1].
418    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    /// Normalise anchor weights so they sum to 1.0.
432    ///
433    /// If the total is zero the weights are left unchanged.
434    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// ---------------------------------------------------------------------------
451// ExpressionLibConfig
452// ---------------------------------------------------------------------------
453
454/// Configuration for the expression preset library.
455#[derive(Debug, Clone)]
456#[allow(dead_code)]
457pub struct ExpressionLibConfig {
458    /// Maximum number of presets allowed in the library (0 = unlimited).
459    pub max_presets: usize,
460    /// Whether to allow overwriting presets with the same name.
461    pub allow_overwrite: bool,
462    /// Default intensity applied to newly added presets.
463    pub default_intensity: f32,
464}
465
466/// Return a sensible default `ExpressionLibConfig`.
467#[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/// Create an empty expression library.
477#[allow(dead_code)]
478pub fn new_expression_library() -> ExpressionLibrary {
479    ExpressionLibrary::new()
480}
481
482/// Add a preset to the library (keyed by `preset.name`).
483#[allow(dead_code)]
484pub fn add_preset(lib: &mut ExpressionLibrary, preset: ExpressionPreset) {
485    lib.add(preset);
486}
487
488/// Remove a preset by name. Returns `true` if it was present.
489#[allow(dead_code)]
490pub fn remove_preset(lib: &mut ExpressionLibrary, name: &str) -> bool {
491    lib.presets.remove(name).is_some()
492}
493
494/// Get a preset by name.
495#[allow(dead_code)]
496pub fn get_preset<'a>(lib: &'a ExpressionLibrary, name: &str) -> Option<&'a ExpressionPreset> {
497    lib.get(name)
498}
499
500/// Number of presets in the library.
501#[allow(dead_code)]
502pub fn preset_count(lib: &ExpressionLibrary) -> usize {
503    lib.count()
504}
505
506/// Find a preset whose name contains `substring` (case-insensitive). Returns first match.
507#[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/// Linearly blend two named presets by `t` [0..1].
519#[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/// Serialize the library to a simple JSON-like string.
530#[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/// Build a basic library with 7 core emotion presets.
552#[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/// Return the morph weight map for a named preset (with intensity applied).
620#[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/// Return a sorted list of preset names.
627#[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
632// ---------------------------------------------------------------------------
633// Private helpers
634// ---------------------------------------------------------------------------
635
636/// Lerp two weight maps over the union of their keys.
637fn 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// ---------------------------------------------------------------------------
658// Tests
659// ---------------------------------------------------------------------------
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    // Helper: build a small weight map
666    fn wmap(pairs: &[(&str, f32)]) -> HashMap<String, f32> {
667        pairs.iter().map(|&(k, v)| (k.to_string(), v)).collect()
668    }
669
670    // -----------------------------------------------------------------------
671    // 1. default_library has all expected presets
672    // -----------------------------------------------------------------------
673    #[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    // -----------------------------------------------------------------------
684    // 2. list_names returns sorted names
685    // -----------------------------------------------------------------------
686    #[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    // -----------------------------------------------------------------------
696    // 3. get known preset returns Some
697    // -----------------------------------------------------------------------
698    #[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    // -----------------------------------------------------------------------
714    // 4. get unknown preset returns None
715    // -----------------------------------------------------------------------
716    #[test]
717    fn get_unknown_preset_returns_none() {
718        let lib = ExpressionLibrary::default_library();
719        assert!(lib.get("__nonexistent__").is_none());
720    }
721
722    // -----------------------------------------------------------------------
723    // 5. list_by_tag
724    // -----------------------------------------------------------------------
725    #[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        // blink, wink_left, wink_right at minimum
730        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    // -----------------------------------------------------------------------
743    // 6. blend at t=0 gives weights of a
744    // -----------------------------------------------------------------------
745    #[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    // -----------------------------------------------------------------------
765    // 7. blend at t=1 gives weights of b
766    // -----------------------------------------------------------------------
767    #[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    // -----------------------------------------------------------------------
787    // 8. blend with unknown name returns None
788    // -----------------------------------------------------------------------
789    #[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    // -----------------------------------------------------------------------
797    // 9. apply_intensity scales weights
798    // -----------------------------------------------------------------------
799    #[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    // -----------------------------------------------------------------------
813    // 10. combine is clamped to [0, 1]
814    // -----------------------------------------------------------------------
815    #[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    // -----------------------------------------------------------------------
831    // 11. expression_distance — same map gives 0
832    // -----------------------------------------------------------------------
833    #[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    // -----------------------------------------------------------------------
845    // 12. expression_distance triangle inequality
846    // -----------------------------------------------------------------------
847    #[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    // -----------------------------------------------------------------------
865    // 13. find_nearest — neutral map closest to neutral preset
866    // -----------------------------------------------------------------------
867    #[test]
868    fn find_nearest_empty_returns_neutral() {
869        let lib = ExpressionLibrary::default_library();
870        // Empty weight map should be closest to "neutral" (which has no weights)
871        let nearest = lib.find_nearest(&HashMap::new()).expect("must return Some");
872        assert_eq!(nearest, "neutral");
873    }
874
875    // -----------------------------------------------------------------------
876    // 14. ExpressionBlender evaluate and normalize
877    // -----------------------------------------------------------------------
878    #[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    // -----------------------------------------------------------------------
893    // 15. ExpressionBlender evaluate produces values in [0, 1]
894    // -----------------------------------------------------------------------
895    #[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    // -----------------------------------------------------------------------
909    // 16. random_blend produces values in [0, 1] for various seeds
910    // -----------------------------------------------------------------------
911    #[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    // -----------------------------------------------------------------------
928    // 17. add and count
929    // -----------------------------------------------------------------------
930    #[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        // Overwrite same name
939        lib.add(ExpressionPreset::new("a", "overwrite", HashMap::new()));
940        assert_eq!(lib.count(), 2);
941    }
942
943    // -----------------------------------------------------------------------
944    // 18. write test artefact to /tmp/
945    // -----------------------------------------------------------------------
946    #[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    // -----------------------------------------------------------------------
958    // 19. default_library_config has expected defaults
959    // -----------------------------------------------------------------------
960    #[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    // -----------------------------------------------------------------------
969    // 20. new_expression_library is empty
970    // -----------------------------------------------------------------------
971    #[test]
972    fn new_expression_library_is_empty() {
973        let lib = new_expression_library();
974        assert_eq!(preset_count(&lib), 0);
975    }
976
977    // -----------------------------------------------------------------------
978    // 21. add_preset / remove_preset round-trip
979    // -----------------------------------------------------------------------
980    #[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    // -----------------------------------------------------------------------
992    // 22. get_preset returns correct entry
993    // -----------------------------------------------------------------------
994    #[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    // -----------------------------------------------------------------------
1003    // 23. find_preset_by_name substring match
1004    // -----------------------------------------------------------------------
1005    #[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    // -----------------------------------------------------------------------
1014    // 24. blend_presets at midpoint is between a and b
1015    // -----------------------------------------------------------------------
1016    #[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    // -----------------------------------------------------------------------
1024    // 25. library_to_json contains preset names
1025    // -----------------------------------------------------------------------
1026    #[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    // -----------------------------------------------------------------------
1035    // 26. build_basic_library has exactly 7 presets
1036    // -----------------------------------------------------------------------
1037    #[test]
1038    fn build_basic_library_has_seven_presets() {
1039        let lib = build_basic_library();
1040        assert_eq!(lib.count(), 7);
1041    }
1042
1043    // -----------------------------------------------------------------------
1044    // 27. build_basic_library has all 7 emotion names
1045    // -----------------------------------------------------------------------
1046    #[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    // -----------------------------------------------------------------------
1063    // 28. preset_morph_weights applies intensity
1064    // -----------------------------------------------------------------------
1065    #[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    // -----------------------------------------------------------------------
1076    // 29. list_preset_names returns sorted strings
1077    // -----------------------------------------------------------------------
1078    #[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}