mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-11-14 08:05:40 +00:00
114 lines
3.6 KiB
Rust
114 lines
3.6 KiB
Rust
//! A title taken from what was said, for a chapter nobody has named yet.
|
|
|
|
use std::sync::LazyLock;
|
|
|
|
use regex::Regex;
|
|
use textkit::pattern::{SPACE, WORD_END, WORD_START};
|
|
|
|
/// Hesitations. A chapter called "Um Yeah So I Think" is worse than no title.
|
|
static FILLER: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
|
|
fancy_regex::Regex::new(&format!(r"(?i){WORD_START}(um+|uh+|ah+|er+){WORD_END}"))
|
|
.expect("a valid pattern")
|
|
});
|
|
|
|
/// The most common discourse marker in recorded speech.
|
|
static YOU_KNOW: LazyLock<fancy_regex::Regex> = LazyLock::new(|| {
|
|
fancy_regex::Regex::new(&format!(r"(?i){WORD_START}you know{WORD_END}"))
|
|
.expect("a valid pattern")
|
|
});
|
|
|
|
static SPACE_RUN: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(&format!(r"{SPACE}+")).expect("a valid pattern"));
|
|
|
|
/// Anything that does not belong in a title.
|
|
static NOT_TITLE: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"[^a-zA-Z0-9'-]").expect("a valid pattern"));
|
|
|
|
/// How many words a title gets.
|
|
const WORDS: usize = 8;
|
|
|
|
/// Take a title from the first sentence of a chapter.
|
|
///
|
|
/// Extractive, and no cleverer than it looks: the first thing said in a
|
|
/// chapter is usually what the chapter is about, and where it is not, a
|
|
/// reader can see that at a glance and a model can replace it later.
|
|
pub fn extract(excerpt: &str) -> String {
|
|
let cleaned = FILLER.replace_all(excerpt, " ");
|
|
let cleaned = YOU_KNOW.replace_all(&cleaned, " ");
|
|
let cleaned = SPACE_RUN.replace_all(&cleaned, " ");
|
|
let cleaned = cleaned.trim_matches(textkit::is_space);
|
|
|
|
let sentence = textkit::sentences(cleaned).first().copied().unwrap_or("");
|
|
let title: Vec<String> = sentence
|
|
.split(textkit::is_space)
|
|
.filter(|word| !word.is_empty())
|
|
.take(WORDS)
|
|
.map(|word| NOT_TITLE.replace_all(word, "").into_owned())
|
|
.filter(|word| !word.is_empty())
|
|
.map(|word| capitalise(&word))
|
|
.collect();
|
|
|
|
if title.is_empty() {
|
|
"Topic".to_string()
|
|
} else {
|
|
title.join(" ")
|
|
}
|
|
}
|
|
|
|
/// First letter up, the rest down — so that a transcript's shouting and its
|
|
/// mid-sentence capitals come out looking like a title either way.
|
|
fn capitalise(word: &str) -> String {
|
|
let mut chars = word.chars();
|
|
match chars.next() {
|
|
None => String::new(),
|
|
Some(first) => first.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn the_first_sentence_becomes_the_title() {
|
|
assert_eq!(
|
|
extract("The mill stood at the northern end. Its wheel was undershot."),
|
|
"The Mill Stood At The Northern End"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_title_is_at_most_eight_words() {
|
|
let title = extract("one two three four five six seven eight nine ten.");
|
|
assert_eq!(title.split(' ').count(), 8);
|
|
}
|
|
|
|
#[test]
|
|
fn fillers_and_discourse_markers_go() {
|
|
assert_eq!(
|
|
extract("Um, so, you know, uh the mill stood there."),
|
|
"So The Mill Stood There"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn punctuation_inside_a_word_is_kept_and_around_it_is_not() {
|
|
assert_eq!(
|
|
extract("(the) mill's well-built wheel."),
|
|
"The Mill's Well-built Wheel"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn shouting_comes_out_as_a_title() {
|
|
assert_eq!(extract("THE MILL STOOD."), "The Mill Stood");
|
|
}
|
|
|
|
#[test]
|
|
fn nothing_usable_still_gives_a_title() {
|
|
assert_eq!(extract(""), "Topic");
|
|
assert_eq!(extract("... !!! ???"), "Topic");
|
|
assert_eq!(extract("um uh ah"), "Topic");
|
|
}
|
|
}
|