aria_kit/transcription/layer/
disfluencies.rs1use crate::cancel::CancelToken;
2
3use super::{Layer, TranscriptionError};
4
5const FILLERS: &[&str] = &[
6 "um", "umm", "uh", "uhh", "uhm", "er", "erm", "ah", "ahh", "hmm", "hm", "mm", "mhm",
7];
8
9#[derive(Debug, Clone, Copy, Default)]
10pub struct RemoveHesitations;
11
12impl Layer for RemoveHesitations {
13 fn apply(&self, text: &mut String, _cancel: &CancelToken) -> Result<(), TranscriptionError> {
14 remove_words(text, DroppedCapital::Carry, |_, word| {
15 FILLERS.contains(&squeezed(&word.core()).as_str())
16 });
17 Ok(())
18 }
19}
20
21#[derive(Debug, Clone, Copy, Default)]
28pub struct RemoveRepeats;
29
30impl Layer for RemoveRepeats {
31 fn apply(&self, text: &mut String, _cancel: &CancelToken) -> Result<(), TranscriptionError> {
32 remove_words(text, DroppedCapital::Discard, |previous, word| {
33 let core = word.core();
34 is_spoken(&core)
35 && previous.is_some_and(|previous| {
36 !previous.ends_with_punctuation() && previous.core() == core
37 })
38 });
39 Ok(())
40 }
41}
42
43#[derive(Clone, Copy, PartialEq, Eq)]
46enum DroppedCapital {
47 Carry,
48 Discard,
49}
50
51fn remove_words(
52 text: &mut String,
53 capital: DroppedCapital,
54 should_drop: impl Fn(Option<&Word>, &Word) -> bool,
55) {
56 let mut kept: Vec<Word> = Vec::new();
57 let mut opening = String::new();
58 let mut capitalize_next = false;
59 let mut dropped = false;
60 for mut word in words(text) {
61 if !should_drop(kept.last(), &word) {
62 if capitalize_next {
63 word.capitalize();
64 capitalize_next = false;
65 }
66 word.text.insert_str(0, &opening);
67 opening.clear();
68 kept.push(word);
69 continue;
70 }
71 dropped = true;
72 let spoken = word.text.trim_matches(|c: char| c.is_ascii_punctuation());
73 capitalize_next |=
74 capital == DroppedCapital::Carry && spoken.starts_with(char::is_uppercase);
75 let start = word.text.len()
76 - word
77 .text
78 .trim_start_matches(|c: char| c.is_ascii_punctuation())
79 .len();
80 let end = word
81 .text
82 .trim_end_matches(|c: char| c.is_ascii_punctuation())
83 .len();
84 opening.extend(
85 word.text[..start]
86 .chars()
87 .filter(|c| matches!(c, '(' | '[' | '{' | '\"' | '\'')),
88 );
89 let mut punctuation = String::new();
90 for character in word.text[end..].chars() {
91 if matches!(
92 (opening.chars().next_back(), character),
93 (Some('('), ')')
94 | (Some('['), ']')
95 | (Some('{'), '}')
96 | (Some('\"'), '\"')
97 | (Some('\''), '\'')
98 ) {
99 opening.pop();
100 } else {
101 punctuation.push(character);
102 }
103 }
104 if let Some(last) = kept.last_mut() {
105 last.absorb(&punctuation, &word.trailing_space);
106 }
107 }
108 if !dropped {
109 return;
110 }
111 *text = kept.iter().map(Word::render).collect();
112}
113
114struct Word {
115 text: String,
116 trailing_space: String,
117}
118
119impl Word {
120 fn core(&self) -> String {
121 self.text
122 .trim_matches(|c: char| c.is_ascii_punctuation())
123 .to_lowercase()
124 }
125
126 fn ends_with_punctuation(&self) -> bool {
127 self.text.ends_with(|c: char| c.is_ascii_punctuation())
128 }
129
130 fn capitalize(&mut self) {
131 let start = self.text.len()
132 - self
133 .text
134 .trim_start_matches(|c: char| c.is_ascii_punctuation())
135 .len();
136 let mut chars = self.text[start..].chars();
137 if let Some(first) = chars.next() {
138 self.text.replace_range(
139 start..start + first.len_utf8(),
140 &first.to_uppercase().to_string(),
141 );
142 }
143 }
144
145 fn absorb(&mut self, punctuation: &str, trailing_space: &str) {
146 if punctuation.contains(['.', '!', '?', ')', ']', '}', '"', '\'']) {
147 self.text
148 .truncate(self.text.trim_end_matches([',', ';', ':']).len());
149 }
150 if !self.ends_with_punctuation() {
151 self.text.push_str(punctuation);
152 } else {
153 self.text.extend(
154 punctuation
155 .chars()
156 .filter(|c| matches!(c, ')' | ']' | '}' | '"' | '\'')),
157 );
158 }
159 if !self.trailing_space.contains('\n') {
160 trailing_space.clone_into(&mut self.trailing_space);
161 }
162 }
163
164 fn render(&self) -> String {
165 format!("{}{}", self.text, self.trailing_space)
166 }
167}
168
169fn squeezed(core: &str) -> String {
170 let mut out = String::with_capacity(core.len());
171 let mut run = 0;
172 let mut last = None;
173 for c in core.chars() {
174 run = if last == Some(c) { run + 1 } else { 1 };
175 last = Some(c);
176 if run <= 2 {
177 out.push(c);
178 }
179 }
180 out
181}
182
183fn is_spoken(core: &str) -> bool {
184 !core.is_empty() && core.chars().all(|c| c.is_alphabetic() || c == '\'')
185}
186
187fn words(text: &str) -> Vec<Word> {
188 let mut words = Vec::new();
189 let mut rest = text.trim_start();
190 while !rest.is_empty() {
191 let end = rest.find(char::is_whitespace).unwrap_or(rest.len());
192 let (word, after) = rest.split_at(end);
193 let space_end = after
194 .find(|c: char| !c.is_whitespace())
195 .unwrap_or(after.len());
196 let (space, next) = after.split_at(space_end);
197 words.push(Word {
198 text: word.to_owned(),
199 trailing_space: space.to_owned(),
200 });
201 rest = next;
202 }
203 words
204}