1use std::collections::HashMap;
4
5#[allow(dead_code)]
7#[derive(Clone, Debug)]
8pub struct JawConfig {
9 pub max_open: f32,
11 pub min_open: f32,
13 pub max_lateral: f32,
15 pub smoothing: f32,
17 pub max_velocity: f32,
19}
20
21#[allow(dead_code)]
23#[derive(Clone, Debug)]
24pub struct JawState {
25 pub current_open: f32,
27 pub target_open: f32,
29 pub lateral_offset: f32,
31 pub velocity: f32,
33}
34
35#[allow(dead_code)]
37#[derive(Clone, Debug)]
38pub struct PhonemeJawMap {
39 pub entries: HashMap<String, f32>,
41}
42
43#[allow(dead_code)]
49pub fn default_jaw_config() -> JawConfig {
50 JawConfig {
51 max_open: 1.0,
52 min_open: 0.0,
53 max_lateral: 0.5,
54 smoothing: 10.0,
55 max_velocity: 5.0,
56 }
57}
58
59#[allow(dead_code)]
61pub fn new_jaw_state() -> JawState {
62 JawState {
63 current_open: 0.0,
64 target_open: 0.0,
65 lateral_offset: 0.0,
66 velocity: 0.0,
67 }
68}
69
70#[allow(dead_code)]
76pub fn build_default_phoneme_map() -> PhonemeJawMap {
77 let mut entries = HashMap::new();
78 entries.insert("AA".to_string(), 0.9);
80 entries.insert("AE".to_string(), 0.8);
81 entries.insert("AH".to_string(), 0.7);
82 entries.insert("AO".to_string(), 0.85);
83 entries.insert("AW".to_string(), 0.8);
84 entries.insert("AY".to_string(), 0.75);
85 entries.insert("EH".to_string(), 0.5);
86 entries.insert("ER".to_string(), 0.4);
87 entries.insert("EY".to_string(), 0.45);
88 entries.insert("IH".to_string(), 0.3);
89 entries.insert("IY".to_string(), 0.25);
90 entries.insert("OW".to_string(), 0.6);
91 entries.insert("OY".to_string(), 0.65);
92 entries.insert("UH".to_string(), 0.35);
93 entries.insert("UW".to_string(), 0.3);
94 entries.insert("B".to_string(), 0.05);
96 entries.insert("CH".to_string(), 0.2);
97 entries.insert("D".to_string(), 0.15);
98 entries.insert("DH".to_string(), 0.15);
99 entries.insert("F".to_string(), 0.1);
100 entries.insert("G".to_string(), 0.2);
101 entries.insert("HH".to_string(), 0.3);
102 entries.insert("JH".to_string(), 0.25);
103 entries.insert("K".to_string(), 0.2);
104 entries.insert("L".to_string(), 0.2);
105 entries.insert("M".to_string(), 0.0);
106 entries.insert("N".to_string(), 0.1);
107 entries.insert("NG".to_string(), 0.15);
108 entries.insert("P".to_string(), 0.0);
109 entries.insert("R".to_string(), 0.2);
110 entries.insert("S".to_string(), 0.1);
111 entries.insert("SH".to_string(), 0.15);
112 entries.insert("T".to_string(), 0.1);
113 entries.insert("TH".to_string(), 0.15);
114 entries.insert("V".to_string(), 0.1);
115 entries.insert("W".to_string(), 0.15);
116 entries.insert("Y".to_string(), 0.15);
117 entries.insert("Z".to_string(), 0.1);
118 entries.insert("ZH".to_string(), 0.15);
119 entries.insert("SIL".to_string(), 0.0);
121 PhonemeJawMap { entries }
122}
123
124#[allow(dead_code)]
130pub fn set_jaw_open(state: &mut JawState, config: &JawConfig, amount: f32) {
131 state.target_open = amount.clamp(config.min_open, config.max_open);
132}
133
134#[allow(dead_code)]
137pub fn jaw_open_for_phoneme(map: &PhonemeJawMap, phoneme: &str) -> f32 {
138 map.entries.get(phoneme).copied().unwrap_or(0.0)
139}
140
141#[allow(dead_code)]
143pub fn update_jaw(state: &mut JawState, config: &JawConfig, dt: f32) {
144 if dt <= 0.0 {
145 return;
146 }
147 let diff = state.target_open - state.current_open;
148 let raw_velocity = diff * config.smoothing;
149 let clamped_velocity = raw_velocity.clamp(-config.max_velocity, config.max_velocity);
150 state.velocity = clamped_velocity;
151 let delta = clamped_velocity * dt;
152 state.current_open = (state.current_open + delta).clamp(config.min_open, config.max_open);
153}
154
155#[allow(dead_code)]
157pub fn jaw_open_amount(state: &JawState) -> f32 {
158 state.current_open
159}
160
161#[allow(dead_code)]
163pub fn jaw_lateral_offset(state: &JawState) -> f32 {
164 state.lateral_offset
165}
166
167#[allow(dead_code)]
169pub fn set_jaw_lateral(state: &mut JawState, config: &JawConfig, offset: f32) {
170 state.lateral_offset = offset.clamp(-config.max_lateral, config.max_lateral);
171}
172
173#[allow(dead_code)]
175pub fn clamp_jaw_range(state: &mut JawState, config: &JawConfig) {
176 state.current_open = state.current_open.clamp(config.min_open, config.max_open);
177 state.target_open = state.target_open.clamp(config.min_open, config.max_open);
178 state.lateral_offset = state
179 .lateral_offset
180 .clamp(-config.max_lateral, config.max_lateral);
181 state.velocity = state
182 .velocity
183 .clamp(-config.max_velocity, config.max_velocity);
184}
185
186#[allow(dead_code)]
188pub fn jaw_velocity(state: &JawState) -> f32 {
189 state.velocity
190}
191
192#[allow(dead_code)]
194pub fn reset_jaw(state: &mut JawState) {
195 state.current_open = 0.0;
196 state.target_open = 0.0;
197 state.lateral_offset = 0.0;
198 state.velocity = 0.0;
199}
200
201#[allow(dead_code)]
203pub fn blend_jaw_states(a: &JawState, b: &JawState, t: f32) -> JawState {
204 let t = t.clamp(0.0, 1.0);
205 let inv = 1.0 - t;
206 JawState {
207 current_open: a.current_open * inv + b.current_open * t,
208 target_open: a.target_open * inv + b.target_open * t,
209 lateral_offset: a.lateral_offset * inv + b.lateral_offset * t,
210 velocity: a.velocity * inv + b.velocity * t,
211 }
212}
213
214#[allow(dead_code)]
219pub fn jaw_to_morph_weights(state: &JawState) -> HashMap<String, f32> {
220 let mut weights = HashMap::new();
221 weights.insert("jaw_open".to_string(), state.current_open);
222 weights.insert("jaw_lateral".to_string(), state.lateral_offset);
223 if state.current_open > 0.5 {
225 weights.insert("mouth_wide".to_string(), (state.current_open - 0.5) * 2.0);
226 } else {
227 weights.insert("mouth_wide".to_string(), 0.0);
228 }
229 if state.current_open < 0.2 {
230 weights.insert("lips_together".to_string(), 1.0 - state.current_open * 5.0);
231 } else {
232 weights.insert("lips_together".to_string(), 0.0);
233 }
234 weights
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_default_config() {
247 let cfg = default_jaw_config();
248 assert!(cfg.max_open > 0.0);
249 assert!(cfg.smoothing > 0.0);
250 assert!(cfg.max_velocity > 0.0);
251 }
252
253 #[test]
254 fn test_new_jaw_state() {
255 let s = new_jaw_state();
256 assert_eq!(s.current_open, 0.0);
257 assert_eq!(s.target_open, 0.0);
258 assert_eq!(s.lateral_offset, 0.0);
259 assert_eq!(s.velocity, 0.0);
260 }
261
262 #[test]
263 fn test_set_jaw_open_clamps() {
264 let cfg = default_jaw_config();
265 let mut s = new_jaw_state();
266 set_jaw_open(&mut s, &cfg, 1.5);
267 assert!((s.target_open - 1.0).abs() < 1e-6);
268 set_jaw_open(&mut s, &cfg, -0.5);
269 assert!((s.target_open - 0.0).abs() < 1e-6);
270 }
271
272 #[test]
273 fn test_jaw_open_for_phoneme_found() {
274 let map = build_default_phoneme_map();
275 let val = jaw_open_for_phoneme(&map, "AA");
276 assert!(val > 0.8);
277 }
278
279 #[test]
280 fn test_jaw_open_for_phoneme_missing() {
281 let map = build_default_phoneme_map();
282 let val = jaw_open_for_phoneme(&map, "ZZZZZ");
283 assert_eq!(val, 0.0);
284 }
285
286 #[test]
287 fn test_update_jaw_toward_target() {
288 let cfg = default_jaw_config();
289 let mut s = new_jaw_state();
290 s.target_open = 1.0;
291 update_jaw(&mut s, &cfg, 0.1);
292 assert!(s.current_open > 0.0);
293 assert!(s.current_open < 1.0);
294 }
295
296 #[test]
297 fn test_update_jaw_zero_dt() {
298 let cfg = default_jaw_config();
299 let mut s = new_jaw_state();
300 s.target_open = 1.0;
301 update_jaw(&mut s, &cfg, 0.0);
302 assert_eq!(s.current_open, 0.0);
303 }
304
305 #[test]
306 fn test_jaw_open_amount() {
307 let mut s = new_jaw_state();
308 s.current_open = 0.42;
309 assert!((jaw_open_amount(&s) - 0.42).abs() < 1e-6);
310 }
311
312 #[test]
313 fn test_set_jaw_lateral() {
314 let cfg = default_jaw_config();
315 let mut s = new_jaw_state();
316 set_jaw_lateral(&mut s, &cfg, 0.3);
317 assert!((s.lateral_offset - 0.3).abs() < 1e-6);
318 }
319
320 #[test]
321 fn test_set_jaw_lateral_clamps() {
322 let cfg = default_jaw_config();
323 let mut s = new_jaw_state();
324 set_jaw_lateral(&mut s, &cfg, 10.0);
325 assert!((s.lateral_offset - cfg.max_lateral).abs() < 1e-6);
326 }
327
328 #[test]
329 fn test_clamp_jaw_range() {
330 let cfg = default_jaw_config();
331 let mut s = JawState {
332 current_open: 2.0,
333 target_open: -1.0,
334 lateral_offset: 5.0,
335 velocity: 100.0,
336 };
337 clamp_jaw_range(&mut s, &cfg);
338 assert!(s.current_open <= cfg.max_open);
339 assert!(s.target_open >= cfg.min_open);
340 assert!(s.lateral_offset <= cfg.max_lateral);
341 assert!(s.velocity <= cfg.max_velocity);
342 }
343
344 #[test]
345 fn test_jaw_velocity() {
346 let cfg = default_jaw_config();
347 let mut s = new_jaw_state();
348 s.target_open = 1.0;
349 update_jaw(&mut s, &cfg, 0.01);
350 assert!(jaw_velocity(&s).abs() > 0.0);
351 }
352
353 #[test]
354 fn test_reset_jaw() {
355 let mut s = JawState {
356 current_open: 0.5,
357 target_open: 0.8,
358 lateral_offset: 0.2,
359 velocity: 1.0,
360 };
361 reset_jaw(&mut s);
362 assert_eq!(s.current_open, 0.0);
363 assert_eq!(s.target_open, 0.0);
364 assert_eq!(s.lateral_offset, 0.0);
365 assert_eq!(s.velocity, 0.0);
366 }
367
368 #[test]
369 fn test_blend_jaw_states_zero() {
370 let a = JawState {
371 current_open: 0.0,
372 target_open: 0.0,
373 lateral_offset: 0.0,
374 velocity: 0.0,
375 };
376 let b = JawState {
377 current_open: 1.0,
378 target_open: 1.0,
379 lateral_offset: 0.5,
380 velocity: 2.0,
381 };
382 let r = blend_jaw_states(&a, &b, 0.0);
383 assert!((r.current_open - 0.0).abs() < 1e-6);
384 }
385
386 #[test]
387 fn test_blend_jaw_states_one() {
388 let a = JawState {
389 current_open: 0.0,
390 target_open: 0.0,
391 lateral_offset: 0.0,
392 velocity: 0.0,
393 };
394 let b = JawState {
395 current_open: 1.0,
396 target_open: 1.0,
397 lateral_offset: 0.5,
398 velocity: 2.0,
399 };
400 let r = blend_jaw_states(&a, &b, 1.0);
401 assert!((r.current_open - 1.0).abs() < 1e-6);
402 }
403
404 #[test]
405 fn test_blend_jaw_states_half() {
406 let a = JawState {
407 current_open: 0.0,
408 target_open: 0.0,
409 lateral_offset: 0.0,
410 velocity: 0.0,
411 };
412 let b = JawState {
413 current_open: 1.0,
414 target_open: 1.0,
415 lateral_offset: 0.5,
416 velocity: 2.0,
417 };
418 let r = blend_jaw_states(&a, &b, 0.5);
419 assert!((r.current_open - 0.5).abs() < 1e-6);
420 }
421
422 #[test]
423 fn test_jaw_to_morph_weights_closed() {
424 let s = new_jaw_state();
425 let w = jaw_to_morph_weights(&s);
426 assert_eq!(*w.get("jaw_open").expect("should succeed"), 0.0);
427 assert!(*w.get("lips_together").expect("should succeed") > 0.9);
428 }
429
430 #[test]
431 fn test_jaw_to_morph_weights_wide_open() {
432 let s = JawState {
433 current_open: 0.8,
434 target_open: 0.8,
435 lateral_offset: 0.0,
436 velocity: 0.0,
437 };
438 let w = jaw_to_morph_weights(&s);
439 assert!(*w.get("mouth_wide").expect("should succeed") > 0.0);
440 assert_eq!(*w.get("lips_together").expect("should succeed"), 0.0);
441 }
442
443 #[test]
444 fn test_phoneme_map_has_many_entries() {
445 let map = build_default_phoneme_map();
446 assert!(map.entries.len() >= 30);
447 }
448
449 #[test]
450 fn test_update_jaw_converges() {
451 let cfg = default_jaw_config();
452 let mut s = new_jaw_state();
453 s.target_open = 0.5;
454 for _ in 0..200 {
455 update_jaw(&mut s, &cfg, 0.016);
456 }
457 assert!((s.current_open - 0.5).abs() < 0.01);
458 }
459}