use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tokio::io::{AsyncWriteExt, BufWriter};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SentenceLengthStats {
pub min_length: u64,
pub max_length: u64,
pub mean_length: f64,
pub median_length: f64,
pub p25_length: f64,
pub p75_length: f64,
pub p90_length: f64,
pub std_dev: f64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FileStats {
pub path: String,
pub chars_processed: u64,
pub sentences_detected: u64,
pub sentence_length_stats: Option<SentenceLengthStats>,
pub processing_time_ms: u64,
pub sentence_detection_time_ms: u64,
pub chars_per_sec: f64,
pub status: String,
pub error: Option<String>,
}
pub async fn write_auxiliary_file_borrowed(
aux_path: &Path,
sentences: &[crate::sentence_detector::DetectedSentenceBorrowed<'_>],
_detector: &crate::sentence_detector::dialog_detector::SentenceDetectorDialog,
debug_seams: bool,
debug_info: Option<&[crate::sentence_detector::dialog_detector::DebugTransitionInfo]>,
) -> Result<()> {
let file = tokio::fs::File::create(aux_path).await
.map_err(|e| anyhow::anyhow!(
"Cannot create output file: {}\nError: {}\n\nSUGGESTIONS:\n• Check write permissions for the directory\n• Ensure sufficient disk space is available\n• Verify the directory exists and is writable",
aux_path.display(), e
))?;
let mut writer = BufWriter::new(file);
for sentence in sentences {
let formatted_line = format!("{}\t{}\t({},{},{},{})",
sentence.index,
sentence.normalize(),
sentence.span.start_line,
sentence.span.start_col,
sentence.span.end_line,
sentence.span.end_col
);
writer.write_all(formatted_line.as_bytes()).await
.map_err(|e| anyhow::anyhow!(
"Cannot write to output file: {}\nError: {}\n\nSUGGESTIONS:\n• Check available disk space\n• Ensure write permissions are maintained\n• File system may be full or read-only",
aux_path.display(), e
))?;
writer.write_all(b"\n").await
.map_err(|e| anyhow::anyhow!(
"Cannot write to output file: {}\nError: {}\n\nSUGGESTIONS:\n• Check available disk space\n• Ensure write permissions are maintained\n• File system may be full or read-only",
aux_path.display(), e
))?;
}
writer.flush().await
.map_err(|e| anyhow::anyhow!(
"Cannot finalize output file: {}\nError: {}\n\nSUGGESTIONS:\n• Check available disk space\n• Ensure write permissions are maintained\n• File system may be full or read-only",
aux_path.display(), e
))?;
if debug_seams {
write_debug_file(aux_path, sentences, debug_info).await?;
}
Ok(())
}
async fn write_debug_file(
aux_path: &Path,
sentences: &[crate::sentence_detector::DetectedSentenceBorrowed<'_>],
debug_info: Option<&[crate::sentence_detector::dialog_detector::DebugTransitionInfo]>,
) -> Result<()> {
let debug_path = if let Some(parent) = aux_path.parent() {
if let Some(filename) = aux_path.file_name().and_then(|n| n.to_str()) {
if filename.ends_with("_seams.txt") {
let debug_filename = filename.replace("_seams.txt", "_seams-debug.txt");
parent.join(debug_filename)
} else {
let debug_filename = filename.replace(".txt", "-debug.txt");
parent.join(debug_filename)
}
} else {
aux_path.with_extension("debug.txt")
}
} else {
aux_path.with_extension("debug.txt")
};
let file = tokio::fs::File::create(&debug_path).await
.map_err(|e| anyhow::anyhow!(
"Cannot create debug output file: {}\nError: {}\n\nSUGGESTIONS:\n• Check write permissions for the directory\n• Ensure sufficient disk space is available\n• Verify the directory exists and is writable",
debug_path.display(), e
))?;
let mut writer = BufWriter::new(file);
let header = "index\tsentence\tspan\tstate_before\tstate_after\ttransition_type\tmatched_pattern\tpattern_name\tseam_text\n";
writer.write_all(header.as_bytes()).await
.map_err(|e| anyhow::anyhow!(
"Cannot write debug header: {}\nError: {}",
debug_path.display(), e
))?;
if let Some(debug_transitions) = debug_info {
for (sentence_idx, sentence) in sentences.iter().enumerate() {
let sentence_transitions: Vec<_> = debug_transitions.iter()
.filter(|t| t.sentence_index == sentence_idx)
.collect();
if sentence_transitions.is_empty() {
let debug_line = format!(
"{}\t{}\t({},{},{},{})\t{}\t{}\t{}\t{}\t{}\t{}\n",
sentence.index,
sentence.normalize(),
sentence.span.start_line,
sentence.span.start_col,
sentence.span.end_line,
sentence.span.end_col,
"Unknown", "Unknown", "Unknown", "no_pattern", "no_pattern_name", "no_seam_text", );
writer.write_all(debug_line.as_bytes()).await
.map_err(|e| anyhow::anyhow!(
"Cannot write debug line: {}\nError: {}",
debug_path.display(), e
))?;
} else {
for transition in sentence_transitions {
let debug_line = format!(
"{}\t{}\t({},{},{},{})\t{:?}\t{:?}\t{:?}\t{}\t{}\t{}\n",
sentence.index,
sentence.normalize(),
sentence.span.start_line,
sentence.span.start_col,
sentence.span.end_line,
sentence.span.end_col,
transition.state_before,
transition.state_after,
transition.transition_type,
transition.matched_pattern.replace('\n', "\\n").replace('\t', "\\t"), transition.pattern_name,
transition.seam_text.replace('\n', "\\n").replace('\t', "\\t"), );
writer.write_all(debug_line.as_bytes()).await
.map_err(|e| anyhow::anyhow!(
"Cannot write debug line: {}\nError: {}",
debug_path.display(), e
))?;
}
}
}
} else {
for sentence in sentences {
let debug_line = format!(
"{}\t{}\t({},{},{},{})\t{}\t{}\t{}\t{}\t{}\t{}\n",
sentence.index,
sentence.normalize(),
sentence.span.start_line,
sentence.span.start_col,
sentence.span.end_line,
sentence.span.end_col,
"placeholder_state_before", "placeholder_state_after", "placeholder_transition_type", "placeholder_pattern", "placeholder_name", "placeholder_seam", );
writer.write_all(debug_line.as_bytes()).await
.map_err(|e| anyhow::anyhow!(
"Cannot write debug line: {}\nError: {}",
debug_path.display(), e
))?;
}
}
writer.flush().await
.map_err(|e| anyhow::anyhow!(
"Cannot finalize debug file: {}\nError: {}",
debug_path.display(), e
))?;
Ok(())
}
pub fn calculate_sentence_length_stats(
sentences: &[crate::sentence_detector::DetectedSentenceBorrowed<'_>]
) -> Option<SentenceLengthStats> {
if sentences.is_empty() {
return None;
}
let mut lengths: Vec<u64> = sentences.iter()
.map(|s| s.normalize().chars().count() as u64)
.collect();
if lengths.is_empty() {
return None;
}
lengths.sort_unstable();
let min_length = *lengths.first().unwrap();
let max_length = *lengths.last().unwrap();
let sum: u64 = lengths.iter().sum();
let mean_length = sum as f64 / lengths.len() as f64;
let median_length = if lengths.len() % 2 == 0 {
let mid = lengths.len() / 2;
(lengths[mid - 1] + lengths[mid]) as f64 / 2.0
} else {
lengths[lengths.len() / 2] as f64
};
let p25_idx = (lengths.len() as f64 * 0.25) as usize;
let p75_idx = (lengths.len() as f64 * 0.75) as usize;
let p90_idx = (lengths.len() as f64 * 0.90) as usize;
let p25_length = lengths[p25_idx.min(lengths.len() - 1)] as f64;
let p75_length = lengths[p75_idx.min(lengths.len() - 1)] as f64;
let p90_length = lengths[p90_idx.min(lengths.len() - 1)] as f64;
let variance = lengths.iter()
.map(|&x| {
let diff = x as f64 - mean_length;
diff * diff
})
.sum::<f64>() / lengths.len() as f64;
let std_dev = variance.sqrt();
Some(SentenceLengthStats {
min_length,
max_length,
mean_length,
median_length,
p25_length,
p75_length,
p90_length,
std_dev,
})
}
pub fn calculate_aggregate_sentence_length_stats(file_stats: &[FileStats]) -> Option<SentenceLengthStats> {
let mut all_lengths: Vec<u64> = Vec::new();
for stats in file_stats {
if let Some(ref length_stats) = stats.sentence_length_stats {
let count = stats.sentences_detected;
if count > 0 {
for _ in 0..count {
all_lengths.push(length_stats.mean_length as u64);
}
}
}
}
if all_lengths.is_empty() {
return None;
}
all_lengths.sort_unstable();
let min_length = *all_lengths.first().unwrap();
let max_length = *all_lengths.last().unwrap();
let sum: u64 = all_lengths.iter().sum();
let mean_length = sum as f64 / all_lengths.len() as f64;
let median_length = if all_lengths.len() % 2 == 0 {
let mid = all_lengths.len() / 2;
(all_lengths[mid - 1] + all_lengths[mid]) as f64 / 2.0
} else {
all_lengths[all_lengths.len() / 2] as f64
};
let p25_idx = (all_lengths.len() as f64 * 0.25) as usize;
let p75_idx = (all_lengths.len() as f64 * 0.75) as usize;
let p90_idx = (all_lengths.len() as f64 * 0.90) as usize;
let p25_length = all_lengths[p25_idx.min(all_lengths.len() - 1)] as f64;
let p75_length = all_lengths[p75_idx.min(all_lengths.len() - 1)] as f64;
let p90_length = all_lengths[p90_idx.min(all_lengths.len() - 1)] as f64;
let variance = all_lengths.iter()
.map(|&x| {
let diff = x as f64 - mean_length;
diff * diff
})
.sum::<f64>() / all_lengths.len() as f64;
let std_dev = variance.sqrt();
Some(SentenceLengthStats {
min_length,
max_length,
mean_length,
median_length,
p25_length,
p75_length,
p90_length,
std_dev,
})
}