1#![allow(dead_code)]
11
12use std::collections::HashMap;
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
20pub enum Emotion {
21 Neutral,
22 Happy,
23 Sad,
24 Angry,
25 Surprised,
26 Fearful,
27 Disgusted,
28 Contempt,
29}
30
31impl Emotion {
32 pub fn all() -> &'static [Emotion] {
34 use Emotion::*;
35 &[
36 Neutral, Happy, Sad, Angry, Surprised, Fearful, Disgusted, Contempt,
37 ]
38 }
39
40 pub fn name(&self) -> &'static str {
42 match self {
43 Emotion::Neutral => "neutral",
44 Emotion::Happy => "happy",
45 Emotion::Sad => "sad",
46 Emotion::Angry => "angry",
47 Emotion::Surprised => "surprised",
48 Emotion::Fearful => "fearful",
49 Emotion::Disgusted => "disgusted",
50 Emotion::Contempt => "contempt",
51 }
52 }
53
54 pub fn valence(&self) -> f32 {
56 match self {
57 Emotion::Neutral => 0.0,
58 Emotion::Happy => 1.0,
59 Emotion::Sad => -1.0,
60 Emotion::Angry => -0.8,
61 Emotion::Surprised => 0.2,
62 Emotion::Fearful => -0.5,
63 Emotion::Disgusted => -0.7,
64 Emotion::Contempt => -0.6,
65 }
66 }
67
68 pub fn arousal(&self) -> f32 {
70 match self {
71 Emotion::Neutral => 0.0,
72 Emotion::Happy => 0.7,
73 Emotion::Sad => -0.4,
74 Emotion::Angry => 0.9,
75 Emotion::Surprised => 0.8,
76 Emotion::Fearful => 0.7,
77 Emotion::Disgusted => 0.3,
78 Emotion::Contempt => 0.2,
79 }
80 }
81}
82
83pub struct EmotionExpression {
89 pub emotion: Emotion,
91 pub intensity: f32,
93 pub weights: HashMap<String, f32>,
95}
96
97impl EmotionExpression {
98 pub fn new(emotion: Emotion) -> Self {
100 Self {
101 emotion,
102 intensity: 1.0,
103 weights: HashMap::new(),
104 }
105 }
106
107 pub fn with_weight(mut self, morph: impl Into<String>, weight: f32) -> Self {
109 self.weights.insert(morph.into(), weight);
110 self
111 }
112
113 pub fn with_intensity(mut self, intensity: f32) -> Self {
115 self.intensity = intensity.clamp(0.0, 1.0);
116 self
117 }
118
119 pub fn effective_weights(&self) -> HashMap<String, f32> {
121 self.weights
122 .iter()
123 .map(|(k, &v)| (k.clone(), v * self.intensity))
124 .collect()
125 }
126}
127
128pub struct EmotionBlend {
134 pub components: HashMap<Emotion, f32>,
136}
137
138impl EmotionBlend {
139 pub fn new() -> Self {
141 Self {
142 components: HashMap::new(),
143 }
144 }
145
146 pub fn single(emotion: Emotion, weight: f32) -> Self {
148 let mut blend = Self::new();
149 blend.components.insert(emotion, weight.clamp(0.0, 1.0));
150 blend
151 }
152
153 pub fn add(&mut self, emotion: Emotion, weight: f32) {
155 let entry = self.components.entry(emotion).or_insert(0.0);
156 *entry = (*entry + weight).clamp(0.0, 1.0);
157 }
158
159 pub fn normalize(&mut self) {
161 let sum: f32 = self.components.values().copied().sum();
162 if sum > f32::EPSILON {
163 for v in self.components.values_mut() {
164 *v /= sum;
165 }
166 }
167 }
168
169 pub fn dominant(&self) -> Option<&Emotion> {
171 self.components
172 .iter()
173 .filter(|(_, &w)| w > 0.0)
174 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
175 .map(|(e, _)| e)
176 }
177
178 pub fn is_neutral(&self) -> bool {
180 self.components.values().all(|&w| w < 0.05)
181 }
182}
183
184impl Default for EmotionBlend {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190pub struct EmotionSystem {
196 expressions: HashMap<Emotion, EmotionExpression>,
197}
198
199impl EmotionSystem {
200 pub fn new() -> Self {
202 Self {
203 expressions: HashMap::new(),
204 }
205 }
206
207 pub fn add_expression(&mut self, expr: EmotionExpression) {
209 self.expressions.insert(expr.emotion.clone(), expr);
210 }
211
212 pub fn get_expression(&self, emotion: &Emotion) -> Option<&EmotionExpression> {
214 self.expressions.get(emotion)
215 }
216
217 pub fn evaluate(&self, blend: &EmotionBlend) -> HashMap<String, f32> {
223 let mut result: HashMap<String, f32> = HashMap::new();
224
225 for (emotion, &blend_weight) in &blend.components {
226 if blend_weight <= 0.0 {
227 continue;
228 }
229 if let Some(expr) = self.expressions.get(emotion) {
230 for (morph, &base_w) in &expr.weights {
231 let contribution = base_w * expr.intensity * blend_weight;
232 let entry = result.entry(morph.clone()).or_insert(0.0);
233 *entry += contribution;
234 }
235 }
236 }
237
238 for v in result.values_mut() {
240 *v = v.clamp(0.0, 1.0);
241 }
242 result
243 }
244
245 pub fn evaluate_single(&self, emotion: &Emotion, intensity: f32) -> HashMap<String, f32> {
247 let intensity = intensity.clamp(0.0, 1.0);
248 match self.expressions.get(emotion) {
249 None => HashMap::new(),
250 Some(expr) => expr
251 .weights
252 .iter()
253 .map(|(k, &v)| (k.clone(), (v * intensity).clamp(0.0, 1.0)))
254 .collect(),
255 }
256 }
257
258 pub fn from_valence_arousal(&self, valence: f32, arousal: f32) -> EmotionBlend {
261 let k = 3usize; let mut distances: Vec<(Emotion, f32)> = Emotion::all()
265 .iter()
266 .map(|e| {
267 let dv = e.valence() - valence;
268 let da = e.arousal() - arousal;
269 let dist = (dv * dv + da * da).sqrt();
270 (e.clone(), dist)
271 })
272 .collect();
273
274 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
276
277 if distances[0].1 < f32::EPSILON {
279 return EmotionBlend::single(distances[0].0.clone(), 1.0);
280 }
281
282 let nearest = &distances[..k.min(distances.len())];
284 let inv_dist_sum: f32 = nearest.iter().map(|(_, d)| 1.0 / d).sum();
285
286 let mut blend = EmotionBlend::new();
287 for (emotion, dist) in nearest {
288 let w = (1.0 / dist) / inv_dist_sum;
289 blend.components.insert(emotion.clone(), w);
290 }
291 blend
292 }
293}
294
295impl Default for EmotionSystem {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301pub fn default_emotion_system() -> EmotionSystem {
307 let mut sys = EmotionSystem::new();
308
309 sys.add_expression(EmotionExpression::new(Emotion::Neutral));
311
312 sys.add_expression(
314 EmotionExpression::new(Emotion::Happy)
315 .with_weight("smile_mouth", 0.8)
316 .with_weight("cheeks_raise", 0.6)
317 .with_weight("eyes_squint", 0.3),
318 );
319
320 sys.add_expression(
322 EmotionExpression::new(Emotion::Sad)
323 .with_weight("mouth_frown", 0.7)
324 .with_weight("brow_inner_up", 0.5)
325 .with_weight("eyes_widen", 0.2),
326 );
327
328 sys.add_expression(
330 EmotionExpression::new(Emotion::Angry)
331 .with_weight("brow_down", 0.8)
332 .with_weight("nose_scrunch", 0.5)
333 .with_weight("lip_press", 0.6),
334 );
335
336 sys.add_expression(
338 EmotionExpression::new(Emotion::Surprised)
339 .with_weight("eyes_widen", 0.9)
340 .with_weight("brow_raise", 0.8)
341 .with_weight("jaw_drop", 0.6),
342 );
343
344 sys.add_expression(
346 EmotionExpression::new(Emotion::Fearful)
347 .with_weight("eyes_widen", 0.8)
348 .with_weight("brow_raise", 0.5)
349 .with_weight("mouth_open", 0.4)
350 .with_weight("lip_stretch", 0.5),
351 );
352
353 sys.add_expression(
355 EmotionExpression::new(Emotion::Disgusted)
356 .with_weight("nose_scrunch", 0.8)
357 .with_weight("upper_lip_raise", 0.7)
358 .with_weight("brow_down", 0.3),
359 );
360
361 sys.add_expression(
363 EmotionExpression::new(Emotion::Contempt)
364 .with_weight("lip_corner_pull_r", 0.6)
365 .with_weight("cheek_raise_r", 0.3)
366 .with_weight("brow_down", 0.2),
367 );
368
369 sys
370}
371
372pub fn lerp_emotion_blend(a: &EmotionBlend, b: &EmotionBlend, t: f32) -> EmotionBlend {
381 let t = t.clamp(0.0, 1.0);
382 let mut result = EmotionBlend::new();
383
384 let mut all_emotions: Vec<Emotion> = a.components.keys().cloned().collect();
386 for e in b.components.keys() {
387 if !all_emotions.contains(e) {
388 all_emotions.push(e.clone());
389 }
390 }
391
392 for emotion in all_emotions {
393 let wa = a.components.get(&emotion).copied().unwrap_or(0.0);
394 let wb = b.components.get(&emotion).copied().unwrap_or(0.0);
395 let w = wa + (wb - wa) * t;
396 if w > 0.0 {
397 result.components.insert(emotion, w);
398 }
399 }
400 result
401}
402
403#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
412 fn test_emotion_all() {
413 let all = Emotion::all();
414 assert_eq!(all.len(), 8);
415 assert!(all.contains(&Emotion::Neutral));
416 assert!(all.contains(&Emotion::Happy));
417 assert!(all.contains(&Emotion::Sad));
418 assert!(all.contains(&Emotion::Angry));
419 assert!(all.contains(&Emotion::Surprised));
420 assert!(all.contains(&Emotion::Fearful));
421 assert!(all.contains(&Emotion::Disgusted));
422 assert!(all.contains(&Emotion::Contempt));
423 }
424
425 #[test]
426 fn test_emotion_names() {
427 assert_eq!(Emotion::Neutral.name(), "neutral");
428 assert_eq!(Emotion::Happy.name(), "happy");
429 assert_eq!(Emotion::Sad.name(), "sad");
430 assert_eq!(Emotion::Angry.name(), "angry");
431 assert_eq!(Emotion::Surprised.name(), "surprised");
432 assert_eq!(Emotion::Fearful.name(), "fearful");
433 assert_eq!(Emotion::Disgusted.name(), "disgusted");
434 assert_eq!(Emotion::Contempt.name(), "contempt");
435 }
436
437 #[test]
438 fn test_emotion_valence_arousal() {
439 assert!((Emotion::Neutral.valence() - 0.0).abs() < f32::EPSILON);
440 assert!((Emotion::Neutral.arousal() - 0.0).abs() < f32::EPSILON);
441 assert!((Emotion::Happy.valence() - 1.0).abs() < f32::EPSILON);
442 assert!((Emotion::Happy.arousal() - 0.7).abs() < f32::EPSILON);
443 assert!((Emotion::Sad.valence() - (-1.0)).abs() < f32::EPSILON);
444 assert!((Emotion::Sad.arousal() - (-0.4)).abs() < f32::EPSILON);
445 assert!((Emotion::Angry.valence() - (-0.8)).abs() < f32::EPSILON);
446 assert!((Emotion::Angry.arousal() - 0.9).abs() < f32::EPSILON);
447 assert!((Emotion::Contempt.valence() - (-0.6)).abs() < f32::EPSILON);
448 assert!((Emotion::Contempt.arousal() - 0.2).abs() < f32::EPSILON);
449 }
450
451 #[test]
452 fn test_expression_effective_weights() {
453 let expr = EmotionExpression::new(Emotion::Happy)
454 .with_weight("smile_mouth", 0.8)
455 .with_weight("cheeks_raise", 0.6)
456 .with_intensity(0.5);
457
458 let eff = expr.effective_weights();
459 let smile = eff["smile_mouth"];
460 let cheeks = eff["cheeks_raise"];
461 assert!((smile - 0.4).abs() < 1e-5, "smile: {smile}");
462 assert!((cheeks - 0.3).abs() < 1e-5, "cheeks: {cheeks}");
463 }
464
465 #[test]
466 fn test_blend_single() {
467 let blend = EmotionBlend::single(Emotion::Happy, 0.7);
468 assert_eq!(blend.components.len(), 1);
469 assert!((blend.components[&Emotion::Happy] - 0.7).abs() < 1e-5);
470 }
471
472 #[test]
473 fn test_blend_add() {
474 let mut blend = EmotionBlend::new();
475 blend.add(Emotion::Happy, 0.4);
476 blend.add(Emotion::Sad, 0.3);
477 blend.add(Emotion::Happy, 0.2); assert!((blend.components[&Emotion::Happy] - 0.6).abs() < 1e-5);
479 assert!((blend.components[&Emotion::Sad] - 0.3).abs() < 1e-5);
480 }
481
482 #[test]
483 fn test_blend_normalize() {
484 let mut blend = EmotionBlend::new();
485 blend.components.insert(Emotion::Happy, 0.4);
486 blend.components.insert(Emotion::Sad, 0.6);
487 blend.normalize();
488 let sum: f32 = blend.components.values().sum();
489 assert!((sum - 1.0).abs() < 1e-5, "sum after normalize: {sum}");
490 }
491
492 #[test]
493 fn test_blend_dominant() {
494 let mut blend = EmotionBlend::new();
495 blend.components.insert(Emotion::Happy, 0.3);
496 blend.components.insert(Emotion::Angry, 0.7);
497 blend.components.insert(Emotion::Sad, 0.1);
498 let dom = blend.dominant().expect("should have dominant");
499 assert_eq!(*dom, Emotion::Angry);
500 }
501
502 #[test]
503 fn test_blend_is_neutral() {
504 let mut blend = EmotionBlend::new();
505 assert!(blend.is_neutral(), "empty blend is neutral");
506
507 blend.components.insert(Emotion::Happy, 0.04);
508 assert!(blend.is_neutral(), "all weights < 0.05 → neutral");
509
510 blend.components.insert(Emotion::Sad, 0.06);
511 assert!(!blend.is_neutral(), "weight 0.06 → not neutral");
512 }
513
514 #[test]
515 fn test_system_evaluate_single() {
516 let sys = default_emotion_system();
517 let weights = sys.evaluate_single(&Emotion::Happy, 1.0);
518 assert!(
519 weights.contains_key("smile_mouth"),
520 "should contain smile_mouth"
521 );
522 let smile = weights["smile_mouth"];
523 assert!(
524 (smile - 0.8).abs() < 1e-5,
525 "smile_mouth at full intensity: {smile}"
526 );
527
528 let half = sys.evaluate_single(&Emotion::Happy, 0.5);
529 let smile_half = half["smile_mouth"];
530 assert!(
531 (smile_half - 0.4).abs() < 1e-5,
532 "smile_mouth at half intensity: {smile_half}"
533 );
534 }
535
536 #[test]
537 fn test_system_evaluate_blend() {
538 let sys = default_emotion_system();
539 let mut blend = EmotionBlend::new();
540 blend.components.insert(Emotion::Happy, 1.0);
541 let weights = sys.evaluate(&blend);
542 assert!(weights.contains_key("smile_mouth"));
543 let smile = weights["smile_mouth"];
544 assert!((smile - 0.8).abs() < 1e-5, "blended smile_mouth: {smile}");
545 }
546
547 #[test]
548 fn test_from_valence_arousal() {
549 let sys = default_emotion_system();
550
551 let blend = sys.from_valence_arousal(1.0, 0.7);
553 let dom = blend.dominant().expect("should have dominant emotion");
554 assert_eq!(*dom, Emotion::Happy, "nearest to (1,0.7) should be Happy");
555
556 let blend_angry = sys.from_valence_arousal(-0.8, 0.9);
558 let dom_angry = blend_angry.dominant().expect("should have dominant");
559 assert_eq!(*dom_angry, Emotion::Angry);
560 }
561
562 #[test]
563 fn test_default_emotion_system() {
564 let sys = default_emotion_system();
565 for emotion in Emotion::all() {
566 assert!(
567 sys.get_expression(emotion).is_some(),
568 "default system should have expression for {}",
569 emotion.name()
570 );
571 }
572 let happy_expr = sys.get_expression(&Emotion::Happy).expect("should succeed");
574 assert!(
575 happy_expr.weights.len() >= 3,
576 "Happy should have at least 3 morph weights"
577 );
578 }
579
580 #[test]
581 fn test_lerp_emotion_blend() {
582 let a = EmotionBlend::single(Emotion::Happy, 1.0);
583 let b = EmotionBlend::single(Emotion::Sad, 1.0);
584
585 let mid = lerp_emotion_blend(&a, &b, 0.5);
586 let happy_w = mid.components.get(&Emotion::Happy).copied().unwrap_or(0.0);
587 let sad_w = mid.components.get(&Emotion::Sad).copied().unwrap_or(0.0);
588 assert!((happy_w - 0.5).abs() < 1e-5, "happy at t=0.5: {happy_w}");
589 assert!((sad_w - 0.5).abs() < 1e-5, "sad at t=0.5: {sad_w}");
590
591 let at_zero = lerp_emotion_blend(&a, &b, 0.0);
593 assert!((at_zero.components[&Emotion::Happy] - 1.0).abs() < 1e-5);
594 assert!(!at_zero.components.contains_key(&Emotion::Sad));
595
596 let at_one = lerp_emotion_blend(&a, &b, 1.0);
598 assert!(!at_one.components.contains_key(&Emotion::Happy));
599 assert!((at_one.components[&Emotion::Sad] - 1.0).abs() < 1e-5);
600 }
601}