aurum_core/
postprocess.rs1use crate::providers::{Segment, TranscriptionResult};
7
8const SPECIAL_MARKERS: &[&str] = &[
10 "[BLANK_AUDIO]",
11 "[blank_audio]",
12 "[MUSIC]",
13 "[Music]",
14 "[music]",
15 "[SILENCE]",
16 "[Silence]",
17 "[silence]",
18 "[NOISE]",
19 "[Noise]",
20 "[noise]",
21 "[INAUDIBLE]",
22 "[Inaudible]",
23 "[CLICK]",
24 "[Click]",
25 "[APPLAUSE]",
26 "[Applause]",
27 "[LAUGHTER]",
28 "[Laughter]",
29 "[COUGH]",
30 "[Cough]",
31 "♪",
32 "♫",
33];
34
35pub fn normalize_result(mut result: TranscriptionResult) -> TranscriptionResult {
37 let mut cleaned_segments = Vec::with_capacity(result.segments.len());
39 for mut seg in result.segments.drain(..) {
40 seg.text = strip_markers(&seg.text);
41 let trimmed = seg.text.trim();
42 if trimmed.is_empty() || is_only_marker(trimmed) {
43 continue;
44 }
45 seg.text = trimmed.to_string();
46 if !seg.start.is_finite() || !seg.end.is_finite() {
48 continue;
49 }
50 if result.duration_secs > 0.0 {
52 seg.start = seg.start.clamp(0.0, result.duration_secs);
53 seg.end = seg.end.clamp(0.0, result.duration_secs);
54 } else {
55 seg.start = seg.start.max(0.0);
56 seg.end = seg.end.max(0.0);
57 }
58 if seg.end < seg.start {
59 std::mem::swap(&mut seg.start, &mut seg.end);
60 }
61 cleaned_segments.push(seg);
62 }
63 result.segments = cleaned_segments;
64
65 if !result.segments.is_empty() {
67 result.text = join_segment_text(&result.segments);
68 } else {
69 result.text = strip_markers(&result.text);
70 result.text = result.text.trim().to_string();
71 if is_only_marker(&result.text) {
72 result.text.clear();
73 }
74 }
75
76 result
77}
78
79fn join_segment_text(segments: &[Segment]) -> String {
80 let mut out = String::new();
81 for seg in segments {
82 let t = seg.text.trim();
83 if t.is_empty() {
84 continue;
85 }
86 if !out.is_empty() {
87 out.push(' ');
88 }
89 out.push_str(t);
90 }
91 out
92}
93
94fn strip_markers(text: &str) -> String {
95 let mut out = text.to_string();
96 for marker in SPECIAL_MARKERS {
97 out = out.replace(marker, " ");
98 }
99 out.split_whitespace().collect::<Vec<_>>().join(" ")
101}
102
103fn is_only_marker(text: &str) -> bool {
104 let t = text.trim();
105 if t.is_empty() {
106 return false;
107 }
108 SPECIAL_MARKERS
110 .iter()
111 .any(|m| t.eq_ignore_ascii_case(m.trim_matches(|c| c == '[' || c == ']')))
112 || SPECIAL_MARKERS.iter().any(|m| t.eq_ignore_ascii_case(m))
113}
114
115pub fn truncate_chars(s: &str, max_chars: usize) -> String {
117 if s.chars().count() <= max_chars {
118 return s.to_string();
119 }
120 let truncated: String = s.chars().take(max_chars).collect();
121 format!("{truncated}…")
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn strips_blank_audio() {
130 let r = normalize_result(TranscriptionResult::local(
131 "[BLANK_AUDIO]".into(),
132 vec![Segment {
133 start: 0.0,
134 end: 10.0,
135 text: "[BLANK_AUDIO]".into(),
136 }],
137 Some("en".into()),
138 "tiny".into(),
139 3.0,
140 ));
141 assert!(r.text.is_empty(), "got {:?}", r.text);
142 assert!(r.segments.is_empty());
143 }
144
145 #[test]
146 fn clamps_timestamps() {
147 let r = normalize_result(TranscriptionResult::local(
148 "Hello".into(),
149 vec![Segment {
150 start: -1.0,
151 end: 99.0,
152 text: "Hello".into(),
153 }],
154 None,
155 "tiny".into(),
156 5.0,
157 ));
158 assert_eq!(r.segments[0].start, 0.0);
159 assert_eq!(r.segments[0].end, 5.0);
160 }
161
162 #[test]
163 fn truncate_multibyte_safe() {
164 let s = "hello 你好世界";
165 let t = truncate_chars(s, 8);
166 assert!(t.ends_with('…'));
167 assert!(t.is_char_boundary(t.len() - '…'.len_utf8()) || t.ends_with('…'));
168 let _ = t.len();
170 }
171}