Skip to main content

aria_kit/transcription/layer/
disfluencies.rs

1use 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/// Drops the second of two identical spoken words in a row, even valid
22/// English such as "had had". Punctuation between them keeps both, as in
23/// "very, very good".
24///
25/// Apply it after [`RemoveHesitations`]. A filler between two copies, as in
26/// "the um the", hides the repeat.
27#[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/// Whether a dropped capitalized word passes its capital to the word after
44/// it. A filler does; a repeat leaves its first copy capitalized.
45#[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}