#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum AccessibilityProfile {
SDH,
HoH,
VisuallyImpaired,
Cognitive,
}
impl AccessibilityProfile {
#[allow(dead_code)]
pub fn description(&self) -> &str {
match self {
Self::SDH => "Subtitles for Deaf and Hard of Hearing (includes non-speech sounds)",
Self::HoH => "Hard of Hearing — simplified sound descriptions",
Self::VisuallyImpaired => "Visually Impaired — audio descriptions and enhanced text",
Self::Cognitive => "Cognitive accessibility — simplified language and pacing",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(dead_code)]
pub enum SoundType {
Music,
Applause,
Laughter,
Door,
Phone,
Alarm,
Thunder,
Footsteps,
}
impl SoundType {
#[allow(dead_code)]
pub fn label(&self) -> &str {
match self {
Self::Music => "MUSIC",
Self::Applause => "APPLAUSE",
Self::Laughter => "LAUGHTER",
Self::Door => "DOOR",
Self::Phone => "PHONE",
Self::Alarm => "ALARM",
Self::Thunder => "THUNDER",
Self::Footsteps => "FOOTSTEPS",
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SoundDescription {
pub sound_type: SoundType,
pub description: String,
pub confidence: f32,
}
impl SoundDescription {
#[allow(dead_code)]
pub fn new(sound_type: SoundType, description: impl Into<String>, confidence: f32) -> Self {
Self {
sound_type,
description: description.into(),
confidence: confidence.clamp(0.0, 1.0),
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct SdhEvent {
pub timestamp_ms: u64,
pub duration_ms: u64,
pub sound: SoundDescription,
pub speaker: Option<String>,
}
impl SdhEvent {
#[allow(dead_code)]
pub fn new(
timestamp_ms: u64,
duration_ms: u64,
sound: SoundDescription,
speaker: Option<String>,
) -> Self {
Self {
timestamp_ms,
duration_ms,
sound,
speaker,
}
}
}
pub struct SdhFormatter;
impl SdhFormatter {
#[allow(dead_code)]
pub fn format(event: &SdhEvent) -> String {
let label = event.sound.sound_type.label();
let desc = &event.sound.description;
match event.sound.sound_type {
SoundType::Music => {
if desc.is_empty() {
format!("[{label} PLAYING]")
} else {
format!("[{label}: {desc}]")
}
}
SoundType::Laughter => {
if let Some(ref speaker) = event.speaker {
format!("({speaker} laughing)")
} else {
format!("({desc})")
}
}
SoundType::Applause => format!("[{label}]"),
_ => {
if desc.is_empty() {
format!("[{label}]")
} else {
format!("[{desc}]")
}
}
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ReadingSpeedResult {
pub chars_per_sec: f32,
pub words_per_min: f32,
pub is_too_fast: bool,
pub recommended_duration_ms: u64,
}
pub struct ReadingSpeed;
pub const MAX_CHARS_PER_SEC_UK: f32 = 17.0;
pub const MAX_CHARS_PER_SEC_FAST: f32 = 25.0;
impl ReadingSpeed {
#[allow(dead_code)]
pub fn check(text: &str, duration_ms: u64) -> ReadingSpeedResult {
let char_count = text.chars().count() as f32;
let duration_secs = duration_ms as f32 / 1000.0;
let chars_per_sec = if duration_secs > 0.0 {
char_count / duration_secs
} else {
f32::INFINITY
};
let words_per_min = (char_count / 5.0) / duration_secs * 60.0;
let is_too_fast = chars_per_sec > MAX_CHARS_PER_SEC_UK;
let recommended_duration_ms = ((char_count / MAX_CHARS_PER_SEC_UK) * 1000.0).ceil() as u64;
ReadingSpeedResult {
chars_per_sec,
words_per_min,
is_too_fast,
recommended_duration_ms,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_accessibility_profile_description_not_empty() {
assert!(!AccessibilityProfile::SDH.description().is_empty());
assert!(!AccessibilityProfile::HoH.description().is_empty());
assert!(!AccessibilityProfile::VisuallyImpaired
.description()
.is_empty());
assert!(!AccessibilityProfile::Cognitive.description().is_empty());
}
#[test]
fn test_sound_type_labels() {
assert_eq!(SoundType::Music.label(), "MUSIC");
assert_eq!(SoundType::Laughter.label(), "LAUGHTER");
assert_eq!(SoundType::Alarm.label(), "ALARM");
assert_eq!(SoundType::Thunder.label(), "THUNDER");
}
#[test]
fn test_sound_description_confidence_clamped() {
let sd = SoundDescription::new(SoundType::Music, "dramatic", 1.5);
assert!(sd.confidence <= 1.0);
let sd2 = SoundDescription::new(SoundType::Door, "slam", -0.5);
assert!(sd2.confidence >= 0.0);
}
#[test]
fn test_sdh_formatter_music_no_desc() {
let ev = SdhEvent::new(
0,
2000,
SoundDescription::new(SoundType::Music, "", 1.0),
None,
);
let fmt = SdhFormatter::format(&ev);
assert_eq!(fmt, "[MUSIC PLAYING]");
}
#[test]
fn test_sdh_formatter_music_with_desc() {
let ev = SdhEvent::new(
0,
2000,
SoundDescription::new(SoundType::Music, "soft piano", 1.0),
None,
);
let fmt = SdhFormatter::format(&ev);
assert!(fmt.contains("soft piano"));
}
#[test]
fn test_sdh_formatter_laughter_with_speaker() {
let ev = SdhEvent::new(
0,
1500,
SoundDescription::new(SoundType::Laughter, "laughing", 0.9),
Some("Alice".to_string()),
);
let fmt = SdhFormatter::format(&ev);
assert!(fmt.contains("Alice"));
assert!(fmt.contains("laughing"));
}
#[test]
fn test_sdh_formatter_applause() {
let ev = SdhEvent::new(
0,
3000,
SoundDescription::new(SoundType::Applause, "", 1.0),
None,
);
let fmt = SdhFormatter::format(&ev);
assert_eq!(fmt, "[APPLAUSE]");
}
#[test]
fn test_sdh_formatter_alarm_with_desc() {
let ev = SdhEvent::new(
0,
2000,
SoundDescription::new(SoundType::Alarm, "fire alarm sounding", 0.95),
None,
);
let fmt = SdhFormatter::format(&ev);
assert!(fmt.contains("fire alarm sounding"));
}
#[test]
fn test_reading_speed_check_slow() {
let result = ReadingSpeed::check("Hello, World 17c", 2_000);
assert!(!result.is_too_fast);
assert!(result.chars_per_sec < MAX_CHARS_PER_SEC_UK);
}
#[test]
fn test_reading_speed_check_too_fast() {
let result = ReadingSpeed::check("This text is way too fast to read!", 1_000);
assert!(result.is_too_fast);
}
#[test]
fn test_reading_speed_recommended_duration() {
let result = ReadingSpeed::check("Hello, World 17c", 500);
assert!(result.recommended_duration_ms >= 900);
}
#[test]
fn test_sdh_event_construction() {
let ev = SdhEvent::new(
1000,
500,
SoundDescription::new(SoundType::Footsteps, "footsteps on gravel", 0.8),
Some("Bob".to_string()),
);
assert_eq!(ev.timestamp_ms, 1000);
assert_eq!(ev.duration_ms, 500);
assert!(ev.speaker.is_some());
}
}