whisper-apr 0.3.3

WASM-first automatic speech recognition engine implementing OpenAI Whisper
Documentation
//! Tests for word boundary detection

use super::*;

// =========================================================================
// BoundaryConfig Tests
// =========================================================================

#[test]
fn test_boundary_config_default() {
    let config = BoundaryConfig::default();
    assert!((config.min_silence_duration - 0.05).abs() < f32::EPSILON);
    assert!(config.use_audio_energy);
}

#[test]
fn test_boundary_config_precise() {
    let config = BoundaryConfig::precise();
    assert!((config.min_silence_duration - 0.03).abs() < f32::EPSILON);
    assert!((config.min_word_duration - 0.03).abs() < f32::EPSILON);
}

#[test]
fn test_boundary_config_fast() {
    let config = BoundaryConfig::fast();
    assert!(!config.use_audio_energy);
    assert!((config.min_silence_duration - 0.1).abs() < f32::EPSILON);
}

#[test]
fn test_boundary_config_with_min_silence() {
    let config = BoundaryConfig::default().with_min_silence(0.1);
    assert!((config.min_silence_duration - 0.1).abs() < f32::EPSILON);
}

#[test]
fn test_boundary_config_with_min_word_duration() {
    let config = BoundaryConfig::default().with_min_word_duration(0.2);
    assert!((config.min_word_duration - 0.2).abs() < f32::EPSILON);
}

// =========================================================================
// WordBoundary Tests
// =========================================================================

#[test]
fn test_word_boundary_new() {
    let boundary = WordBoundary::new(1.0, 2.0);
    assert!((boundary.start - 1.0).abs() < f32::EPSILON);
    assert!((boundary.end - 2.0).abs() < f32::EPSILON);
    assert!((boundary.start_confidence - 0.5).abs() < f32::EPSILON);
    assert!(!boundary.audio_refined);
}

#[test]
fn test_word_boundary_duration() {
    let boundary = WordBoundary::new(1.0, 2.5);
    assert!((boundary.duration() - 1.5).abs() < f32::EPSILON);
}

#[test]
fn test_word_boundary_confidence() {
    let boundary = WordBoundary::new(1.0, 2.0).with_confidence(0.8, 0.6);
    assert!((boundary.confidence() - 0.7).abs() < f32::EPSILON);
}

#[test]
fn test_word_boundary_is_high_confidence() {
    let high = WordBoundary::new(1.0, 2.0).with_confidence(0.9, 0.9);
    let low = WordBoundary::new(1.0, 2.0).with_confidence(0.3, 0.3);
    assert!(high.is_high_confidence());
    assert!(!low.is_high_confidence());
}

#[test]
fn test_word_boundary_with_tokens() {
    let boundary = WordBoundary::new(1.0, 2.0).with_tokens(vec![0, 1, 2]);
    assert_eq!(boundary.token_indices, vec![0, 1, 2]);
}

#[test]
fn test_word_boundary_with_audio_refined() {
    let boundary = WordBoundary::new(1.0, 2.0).with_audio_refined(true);
    assert!(boundary.audio_refined);
}

// =========================================================================
// BoundaryDetector Tests
// =========================================================================

#[test]
fn test_boundary_detector_new() {
    let detector = BoundaryDetector::new(BoundaryConfig::default());
    assert!(detector.config.use_audio_energy);
}

#[test]
fn test_boundary_detector_default() {
    let detector = BoundaryDetector::default();
    assert!((detector.config.min_silence_duration - 0.05).abs() < f32::EPSILON);
}

#[test]
fn test_detect_boundaries_empty() {
    let detector = BoundaryDetector::default();
    let result = detector
        .detect_boundaries(&[], &[])
        .expect("should succeed");
    assert!(result.is_empty());
}

#[test]
fn test_detect_boundaries_single_word() {
    let detector = BoundaryDetector::default();

    let mut alignments = vec![
        TokenAlignment::new(0, 100, 50, 0.9),
        TokenAlignment::new(1, 101, 60, 0.8),
    ];
    alignments[0].set_end_time(60);
    alignments[1].set_end_time(80);

    let word_starts = vec![0];

    let result = detector
        .detect_boundaries(&alignments, &word_starts)
        .expect("should succeed");

    assert_eq!(result.len(), 1);
    assert!((result[0].start - 1.0).abs() < f32::EPSILON); // frame 50 / 50fps
}

#[test]
fn test_detect_boundaries_multiple_words() {
    let detector = BoundaryDetector::default();

    let mut alignments = vec![
        TokenAlignment::new(0, 100, 0, 0.9),
        TokenAlignment::new(1, 101, 25, 0.8),
        TokenAlignment::new(2, 102, 50, 0.85),
        TokenAlignment::new(3, 103, 75, 0.9),
    ];
    for (i, a) in alignments.iter_mut().enumerate() {
        a.set_end_time((i + 1) * 25);
    }

    let word_starts = vec![0, 2];

    let result = detector
        .detect_boundaries(&alignments, &word_starts)
        .expect("should succeed");

    assert_eq!(result.len(), 2);
}

#[test]
fn test_validate_boundary_min_duration() {
    let detector = BoundaryDetector::new(BoundaryConfig::default().with_min_word_duration(0.1));

    let boundary = WordBoundary::new(1.0, 1.01); // Very short
    let validated = detector.validate_boundary(boundary);

    assert!(validated.duration() >= 0.1);
}

#[test]
fn test_validate_boundary_max_duration() {
    let mut config = BoundaryConfig::default();
    config.max_word_duration = 2.0;
    let detector = BoundaryDetector::new(config);

    let boundary = WordBoundary::new(0.0, 10.0); // Too long
    let validated = detector.validate_boundary(boundary);

    assert!((validated.duration() - 2.0).abs() < f32::EPSILON);
}

#[test]
fn test_compute_boundary_confidence_empty() {
    let detector = BoundaryDetector::default();
    let confidence = detector.compute_boundary_confidence(&[]);
    assert!((confidence - 0.0).abs() < f32::EPSILON);
}

#[test]
fn test_compute_boundary_confidence_monotonic() {
    let detector = BoundaryDetector::default();

    let alignments = vec![
        TokenAlignment::new(0, 100, 10, 0.8),
        TokenAlignment::new(1, 101, 20, 0.8),
        TokenAlignment::new(2, 102, 30, 0.8),
    ];

    let confidence = detector.compute_boundary_confidence(&alignments);
    assert!((confidence - 0.8).abs() < f32::EPSILON); // No penalty for monotonic
}

#[test]
fn test_compute_boundary_confidence_non_monotonic() {
    let detector = BoundaryDetector::default();

    let alignments = vec![
        TokenAlignment::new(0, 100, 30, 0.8),
        TokenAlignment::new(1, 101, 20, 0.8), // Out of order
        TokenAlignment::new(2, 102, 40, 0.8),
    ];

    let confidence = detector.compute_boundary_confidence(&alignments);
    assert!(confidence < 0.8); // Penalty for non-monotonic
}

#[test]
fn test_detect_silence_gaps() {
    let detector = BoundaryDetector::default();

    let boundaries = vec![
        WordBoundary::new(0.0, 1.0),
        WordBoundary::new(1.5, 2.5), // 0.5s gap
        WordBoundary::new(2.6, 3.5), // 0.1s gap
    ];

    let gaps = detector.detect_silence_gaps(&boundaries);

    assert_eq!(gaps.len(), 2);
    assert!((gaps[0].0 - 1.0).abs() < f32::EPSILON);
    assert!((gaps[0].1 - 1.5).abs() < f32::EPSILON);
}

#[test]
fn test_detect_silence_gaps_no_gaps() {
    let mut config = BoundaryConfig::default();
    config.min_silence_duration = 1.0; // High threshold
    let detector = BoundaryDetector::new(config);

    let boundaries = vec![
        WordBoundary::new(0.0, 1.0),
        WordBoundary::new(1.1, 2.0), // Only 0.1s gap
    ];

    let gaps = detector.detect_silence_gaps(&boundaries);
    assert!(gaps.is_empty());
}

#[test]
fn test_refine_with_audio_disabled() {
    let config = BoundaryConfig::fast(); // Audio refinement disabled
    let detector = BoundaryDetector::new(config);

    let boundaries = vec![WordBoundary::new(0.0, 1.0)];
    let audio_energy = vec![0.5; 100];

    let refined = detector
        .refine_with_audio(&boundaries, &audio_energy, 50.0)
        .expect("should succeed");

    assert!(!refined[0].audio_refined);
}

#[test]
fn test_refine_with_audio_empty_energy() {
    let detector = BoundaryDetector::default();

    let boundaries = vec![WordBoundary::new(0.0, 1.0)];

    let refined = detector
        .refine_with_audio(&boundaries, &[], 50.0)
        .expect("should succeed");

    assert!(!refined[0].audio_refined);
}

// =========================================================================
// Additional Coverage Tests (WAPR-QA)
// =========================================================================

#[test]
fn test_boundary_config_silence_builder() {
    let config = BoundaryConfig::default().with_min_silence(0.1);
    assert!((config.min_silence_duration - 0.1).abs() < f32::EPSILON);
}

#[test]
fn test_boundary_config_word_duration_builder() {
    let config = BoundaryConfig::default().with_min_word_duration(0.2);
    assert!((config.min_word_duration - 0.2).abs() < f32::EPSILON);
}

#[test]
fn test_word_boundary_with_both_confidences() {
    let boundary = WordBoundary::new(0.0, 1.0).with_confidence(0.9, 0.8);

    assert!((boundary.start_confidence - 0.9).abs() < f32::EPSILON);
    assert!((boundary.end_confidence - 0.8).abs() < f32::EPSILON);
}

#[test]
fn test_word_boundary_tokens_builder() {
    let boundary = WordBoundary::new(0.0, 1.0).with_tokens(vec![1, 2, 3]);
    assert_eq!(boundary.token_indices, vec![1, 2, 3]);
}

#[test]
fn test_word_boundary_duration_calculation() {
    let boundary = WordBoundary::new(1.5, 3.5);
    assert!((boundary.duration() - 2.0).abs() < f32::EPSILON);
}

#[test]
fn test_word_boundary_avg_confidence() {
    let boundary = WordBoundary::new(0.0, 1.0).with_confidence(0.8, 0.6);
    // confidence() returns average
    let conf = boundary.confidence();
    assert!((conf - 0.7).abs() < f32::EPSILON);
}

#[test]
fn test_word_boundary_is_high_confidence_both() {
    let high = WordBoundary::new(0.0, 1.0).with_confidence(0.9, 0.85);
    let low = WordBoundary::new(0.0, 1.0).with_confidence(0.5, 0.6);

    assert!(high.is_high_confidence());
    assert!(!low.is_high_confidence());
}

#[test]
fn test_word_boundary_audio_refined_flag() {
    let boundary = WordBoundary::new(0.0, 1.0).with_audio_refined(true);
    assert!(boundary.audio_refined);
}

#[test]
fn test_detect_boundaries_empty_alignments() {
    let detector = BoundaryDetector::default();
    let boundaries = detector
        .detect_boundaries(&[], &[])
        .expect("should succeed");
    assert!(boundaries.is_empty());
}

#[test]
fn test_detect_boundaries_single_alignment() {
    let detector = BoundaryDetector::default();
    let alignments = vec![TokenAlignment::new(0, 100, 30, 0.9)];
    let word_starts = vec![0];
    let boundaries = detector
        .detect_boundaries(&alignments, &word_starts)
        .expect("should succeed");
    assert_eq!(boundaries.len(), 1);
}

#[test]
fn test_refine_with_audio_boundaries() {
    let detector = BoundaryDetector::default();

    let boundaries = vec![WordBoundary::new(0.0, 0.5), WordBoundary::new(0.6, 1.0)];

    // Create audio energy with some variation
    let audio_energy: Vec<f32> = (0..100).map(|i| if i < 50 { 0.5 } else { 0.02 }).collect();

    let refined = detector
        .refine_with_audio(&boundaries, &audio_energy, 100.0)
        .expect("should succeed");

    assert_eq!(refined.len(), 2);
}

#[test]
fn test_detect_silence_gaps_multiple() {
    let detector = BoundaryDetector::default();

    let boundaries = vec![
        WordBoundary::new(0.0, 0.5),
        WordBoundary::new(1.0, 1.5),
        WordBoundary::new(2.0, 2.5),
    ];

    let gaps = detector.detect_silence_gaps(&boundaries);
    assert_eq!(gaps.len(), 2);
}

#[test]
fn test_compute_boundary_confidence_single() {
    let detector = BoundaryDetector::default();
    let alignments = vec![TokenAlignment::new(0, 100, 30, 0.8)];
    let conf = detector.compute_boundary_confidence(&alignments);
    assert!(conf > 0.0 && conf <= 1.0);
}