use elara_core::StateTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FacialRegion {
LeftEye,
RightEye,
LeftEyebrow,
RightEyebrow,
Nose,
UpperLip,
LowerLip,
LeftCheek,
RightCheek,
Jaw,
Forehead,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EmotionVector {
pub joy: f32,
pub sadness: f32,
pub anger: f32,
pub fear: f32,
pub surprise: f32,
pub disgust: f32,
pub contempt: f32,
}
impl EmotionVector {
pub fn neutral() -> Self {
Self::default()
}
pub fn dominant(&self) -> (&'static str, f32) {
let emotions = [
("joy", self.joy),
("sadness", self.sadness),
("anger", self.anger),
("fear", self.fear),
("surprise", self.surprise),
("disgust", self.disgust),
("contempt", self.contempt),
];
emotions
.iter()
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(name, val)| (*name, *val))
.unwrap_or(("neutral", 0.0))
}
pub fn blend(&self, other: &EmotionVector, factor: f32) -> EmotionVector {
let f = factor.clamp(0.0, 1.0);
let inv = 1.0 - f;
EmotionVector {
joy: self.joy * inv + other.joy * f,
sadness: self.sadness * inv + other.sadness * f,
anger: self.anger * inv + other.anger * f,
fear: self.fear * inv + other.fear * f,
surprise: self.surprise * inv + other.surprise * f,
disgust: self.disgust * inv + other.disgust * f,
contempt: self.contempt * inv + other.contempt * f,
}
}
pub fn normalize(&self) -> EmotionVector {
let sum = self.joy
+ self.sadness
+ self.anger
+ self.fear
+ self.surprise
+ self.disgust
+ self.contempt;
if sum < 0.001 {
return EmotionVector::neutral();
}
EmotionVector {
joy: self.joy / sum,
sadness: self.sadness / sum,
anger: self.anger / sum,
fear: self.fear / sum,
surprise: self.surprise / sum,
disgust: self.disgust / sum,
contempt: self.contempt / sum,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GazeState {
pub yaw: f32,
pub pitch: f32,
pub looking_at_camera: bool,
pub blink: f32,
}
impl GazeState {
pub fn forward() -> Self {
Self {
yaw: 0.0,
pitch: 0.0,
looking_at_camera: true,
blink: 0.0,
}
}
pub fn lerp(&self, other: &GazeState, t: f32) -> GazeState {
let t = t.clamp(0.0, 1.0);
GazeState {
yaw: self.yaw + (other.yaw - self.yaw) * t,
pitch: self.pitch + (other.pitch - self.pitch) * t,
looking_at_camera: if t < 0.5 {
self.looking_at_camera
} else {
other.looking_at_camera
},
blink: self.blink + (other.blink - self.blink) * t,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MouthState {
pub openness: f32,
pub smile: f32,
pub viseme: Viseme,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Viseme {
#[default]
Neutral, AA, AO, EH, IY, UW, OW, AE, AW, EY, ER, PP, FF, TH, DD, KK, CH, SS, RR, NN, }
impl Viseme {
pub fn from_phoneme(phoneme: &str) -> Self {
match phoneme.to_lowercase().as_str() {
"aa" | "ah" => Viseme::AA,
"ao" | "aw" => Viseme::AO,
"eh" | "e" => Viseme::EH,
"iy" | "ee" | "i" => Viseme::IY,
"uw" | "oo" | "u" => Viseme::UW,
"ow" | "oh" | "o" => Viseme::OW,
"ae" | "a" => Viseme::AE,
"p" | "b" | "m" => Viseme::PP,
"f" | "v" => Viseme::FF,
"th" => Viseme::TH,
"d" | "t" | "n" => Viseme::DD,
"k" | "g" => Viseme::KK,
"ch" | "j" | "sh" => Viseme::CH,
"s" | "z" => Viseme::SS,
"r" => Viseme::RR,
_ => Viseme::Neutral,
}
}
}
#[derive(Debug, Clone)]
pub struct FaceState {
pub timestamp: StateTime,
pub present: bool,
pub head_rotation: (f32, f32, f32),
pub emotion: EmotionVector,
pub gaze: GazeState,
pub mouth: MouthState,
pub speaking: bool,
pub confidence: f32,
}
impl FaceState {
pub fn new(timestamp: StateTime) -> Self {
Self {
timestamp,
present: true,
head_rotation: (0.0, 0.0, 0.0),
emotion: EmotionVector::neutral(),
gaze: GazeState::forward(),
mouth: MouthState::default(),
speaking: false,
confidence: 1.0,
}
}
pub fn absent(timestamp: StateTime) -> Self {
Self {
timestamp,
present: false,
head_rotation: (0.0, 0.0, 0.0),
emotion: EmotionVector::neutral(),
gaze: GazeState::forward(),
mouth: MouthState::default(),
speaking: false,
confidence: 0.0,
}
}
pub fn reduce_to_minimal(&mut self) {
self.emotion = EmotionVector::neutral();
self.head_rotation = (0.0, 0.0, 0.0);
}
pub fn to_latent(self) -> FaceState {
FaceState {
timestamp: self.timestamp,
present: self.present,
head_rotation: (0.0, 0.0, 0.0),
emotion: EmotionVector::neutral(),
gaze: GazeState::forward(),
mouth: MouthState::default(),
speaking: false,
confidence: 0.1,
}
}
pub fn lerp(&self, other: &FaceState, t: f32) -> FaceState {
let t = t.clamp(0.0, 1.0);
FaceState {
timestamp: other.timestamp,
present: if t < 0.5 { self.present } else { other.present },
head_rotation: (
self.head_rotation.0 + (other.head_rotation.0 - self.head_rotation.0) * t,
self.head_rotation.1 + (other.head_rotation.1 - self.head_rotation.1) * t,
self.head_rotation.2 + (other.head_rotation.2 - self.head_rotation.2) * t,
),
emotion: self.emotion.blend(&other.emotion, t),
gaze: self.gaze.lerp(&other.gaze, t),
mouth: MouthState {
openness: self.mouth.openness + (other.mouth.openness - self.mouth.openness) * t,
smile: self.mouth.smile + (other.mouth.smile - self.mouth.smile) * t,
viseme: if t < 0.5 {
self.mouth.viseme
} else {
other.mouth.viseme
},
},
speaking: if t < 0.5 {
self.speaking
} else {
other.speaking
},
confidence: self.confidence + (other.confidence - self.confidence) * t,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emotion_vector() {
let mut emotion = EmotionVector::neutral();
emotion.joy = 0.8;
emotion.surprise = 0.2;
let (dominant, value) = emotion.dominant();
assert_eq!(dominant, "joy");
assert_eq!(value, 0.8);
}
#[test]
fn test_emotion_blend() {
let happy = EmotionVector {
joy: 1.0,
..Default::default()
};
let sad = EmotionVector {
sadness: 1.0,
..Default::default()
};
let blended = happy.blend(&sad, 0.5);
assert!((blended.joy - 0.5).abs() < 0.01);
assert!((blended.sadness - 0.5).abs() < 0.01);
}
#[test]
fn test_face_state_lerp() {
let time1 = StateTime::from_millis(0);
let time2 = StateTime::from_millis(100);
let mut face1 = FaceState::new(time1);
face1.mouth.openness = 0.0;
let mut face2 = FaceState::new(time2);
face2.mouth.openness = 1.0;
let interpolated = face1.lerp(&face2, 0.5);
assert!((interpolated.mouth.openness - 0.5).abs() < 0.01);
}
#[test]
fn test_viseme_from_phoneme() {
assert_eq!(Viseme::from_phoneme("aa"), Viseme::AA);
assert_eq!(Viseme::from_phoneme("p"), Viseme::PP);
assert_eq!(Viseme::from_phoneme("s"), Viseme::SS);
}
}