Replace all instances of the legacy product names "Kon" and "Corbie" with "Magnotia" across user-facing copy, code identifiers, package names, bundle ids, file paths, and documentation. Preserves the unrelated "konsole" (KDE terminal) reference and the parent CORBEL company name. - Renames 10 Rust crates (kon-* → magnotia-*) and the tauri binary - Updates package.json, tauri.conf.json (productName + identifier) - Renames CSS classes (kon-rh-* → magnotia-rh-*) and animations - Renames brand and roadmap docs - Regenerates Cargo.lock and package-lock.json Verified: svelte-check passes; pure-rust crates compile under new names.
224 lines
7.7 KiB
Rust
224 lines
7.7 KiB
Rust
//! Plain-text pre-formatter for LLM cleanup.
|
|
//!
|
|
//! Brief item #29: before sending transcription segments to the LLM,
|
|
//! join them into a single natural-language string with timestamps
|
|
//! stripped and whitespace normalised. Source: Scriberr PR #288 —
|
|
//! feeding raw Whisper JSON (with its timestamps and per-segment
|
|
//! structure) degraded cleanup quality materially; plain-text input
|
|
//! raised it back.
|
|
//!
|
|
//! `Segment.text` in Magnotia already holds just the spoken text (the
|
|
//! `start`/`end` f64 fields carry the timing), so "timestamp
|
|
//! stripping" falls out of using the text field alone. The work here
|
|
//! is the whitespace pass and empty-segment filter, plus a single
|
|
//! public function the pipeline can depend on.
|
|
|
|
use magnotia_core::types::Segment;
|
|
|
|
/// Join transcription segments into a single plain-text string
|
|
/// suitable for feeding to an LLM cleanup prompt.
|
|
///
|
|
/// Rules:
|
|
/// - each segment's text is whitespace-normalised (any run of
|
|
/// whitespace — spaces, tabs, newlines, non-breaking spaces —
|
|
/// collapses to a single ASCII space),
|
|
/// - segments that are empty or whitespace-only are dropped,
|
|
/// - the remaining segments are joined with a single ASCII space,
|
|
/// - the final string is whitespace-normalised again (so a segment
|
|
/// ending in a space and the next beginning with one do not produce
|
|
/// a double space) and trimmed of leading/trailing whitespace.
|
|
///
|
|
/// Pure function. No panics. Returns an empty string if every segment
|
|
/// filters out.
|
|
pub fn to_plain_text(segments: &[Segment]) -> String {
|
|
let joined = segments
|
|
.iter()
|
|
.map(|s| normalise_whitespace(&s.text))
|
|
.map(|s| s.trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
normalise_whitespace(&joined).trim().to_string()
|
|
}
|
|
|
|
/// Collapse any run of unicode whitespace into a single ASCII space,
|
|
/// and strip zero-width format characters entirely.
|
|
///
|
|
/// Zero-width chars (U+200B/C/D, U+2060, U+FEFF) are handled as a
|
|
/// separate class from whitespace: `char::is_whitespace()` returns
|
|
/// false for them, so the standard whitespace pass would let them
|
|
/// through to the LLM where they waste tokens without contributing
|
|
/// any natural-language content. Treating them as "strip entirely"
|
|
/// rather than "collapse to a space" avoids silently inserting word
|
|
/// breaks where the source had none.
|
|
///
|
|
/// Kept private; the module's contract is `to_plain_text`.
|
|
fn normalise_whitespace(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len());
|
|
let mut prev_was_space = false;
|
|
for ch in s.chars() {
|
|
if is_zero_width_format(ch) {
|
|
// Strip without emitting anything. prev_was_space unchanged
|
|
// so a space on either side of a zero-width char still
|
|
// collapses correctly.
|
|
continue;
|
|
}
|
|
if ch.is_whitespace() {
|
|
if !prev_was_space {
|
|
out.push(' ');
|
|
prev_was_space = true;
|
|
}
|
|
} else {
|
|
out.push(ch);
|
|
prev_was_space = false;
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Zero-width format characters the transcription pipeline should
|
|
/// never feed to an LLM. Sourced from common "invisible" codepoints:
|
|
/// - U+200B ZERO WIDTH SPACE
|
|
/// - U+200C ZERO WIDTH NON-JOINER
|
|
/// - U+200D ZERO WIDTH JOINER
|
|
/// - U+2060 WORD JOINER
|
|
/// - U+FEFF ZERO WIDTH NO-BREAK SPACE (also BOM)
|
|
fn is_zero_width_format(ch: char) -> bool {
|
|
matches!(
|
|
ch,
|
|
'\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{2060}' | '\u{FEFF}'
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn seg(text: &str) -> Segment {
|
|
Segment {
|
|
start: 0.0,
|
|
end: 1.0,
|
|
text: text.into(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn empty_input_is_empty_output() {
|
|
assert_eq!(to_plain_text(&[]), "");
|
|
}
|
|
|
|
#[test]
|
|
fn single_segment_returns_its_text_trimmed() {
|
|
let out = to_plain_text(&[seg(" hello world ")]);
|
|
assert_eq!(out, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_segments_are_joined_with_single_space() {
|
|
let out = to_plain_text(&[seg("the cat"), seg("sat on the mat")]);
|
|
assert_eq!(out, "the cat sat on the mat");
|
|
}
|
|
|
|
#[test]
|
|
fn empty_and_whitespace_segments_are_filtered() {
|
|
let out = to_plain_text(&[
|
|
seg("hello"),
|
|
seg(""),
|
|
seg(" "),
|
|
seg("\n\t "),
|
|
seg("world"),
|
|
]);
|
|
assert_eq!(out, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn internal_whitespace_runs_collapse_to_single_space() {
|
|
let out = to_plain_text(&[seg("hello\t\t \nworld")]);
|
|
assert_eq!(out, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn join_boundary_does_not_produce_double_spaces() {
|
|
// First segment ends with whitespace, next starts with it —
|
|
// naive join would produce "foo bar".
|
|
let out = to_plain_text(&[seg("foo "), seg(" bar")]);
|
|
assert_eq!(out, "foo bar");
|
|
}
|
|
|
|
#[test]
|
|
fn non_breaking_space_is_treated_as_whitespace() {
|
|
// \u{00A0} is NBSP — char::is_whitespace returns true for it.
|
|
// LLM cleanup should not see NBSP leaked in.
|
|
let out = to_plain_text(&[seg("hello\u{00A0}world")]);
|
|
assert_eq!(out, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn zero_width_format_chars_strip_entirely() {
|
|
// char::is_whitespace returns false for all of these, so the
|
|
// default whitespace pass would let them through. They carry
|
|
// no natural-language content — stripping them saves LLM
|
|
// tokens without changing meaning.
|
|
let cases = [
|
|
("hello\u{200B}world", "helloworld"), // ZERO WIDTH SPACE
|
|
("hello\u{200C}world", "helloworld"), // ZWNJ
|
|
("hello\u{200D}world", "helloworld"), // ZWJ
|
|
("hello\u{2060}world", "helloworld"), // WORD JOINER
|
|
("hello\u{FEFF}world", "helloworld"), // ZWNBSP / BOM
|
|
];
|
|
for (input, expected) in cases {
|
|
let out = to_plain_text(&[seg(input)]);
|
|
assert_eq!(
|
|
out, expected,
|
|
"input {input:?} should strip to {expected:?}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn zero_width_chars_do_not_break_adjacent_whitespace_collapsing() {
|
|
// "hello \u{FEFF} world" — the zero-width char between two
|
|
// spaces should strip, leaving a single collapsed space.
|
|
let out = to_plain_text(&[seg("hello \u{FEFF} world")]);
|
|
assert_eq!(out, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn leading_bom_is_stripped() {
|
|
// BOM at start of segment — common artifact when Whisper
|
|
// consumes a file whose encoding pass inserted one.
|
|
let out = to_plain_text(&[seg("\u{FEFF}hello world")]);
|
|
assert_eq!(out, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn newlines_inside_segments_collapse() {
|
|
let out = to_plain_text(&[seg("line one\nline two\n\nline three")]);
|
|
assert_eq!(out, "line one line two line three");
|
|
}
|
|
|
|
#[test]
|
|
fn idempotent_on_already_normalised_text() {
|
|
// If the pipeline ever calls us twice, the second call must
|
|
// not mangle the result.
|
|
let once = to_plain_text(&[seg("hello world"), seg("foo bar")]);
|
|
let twice = to_plain_text(&[seg(&once)]);
|
|
assert_eq!(once, twice);
|
|
}
|
|
|
|
#[test]
|
|
fn only_empty_segments_yields_empty_string() {
|
|
let out = to_plain_text(&[seg(""), seg(" "), seg("\t")]);
|
|
assert_eq!(out, "");
|
|
}
|
|
|
|
#[test]
|
|
fn no_panic_on_pathological_whitespace_runs() {
|
|
// A segment that is 10k spaces long normalises in linear time
|
|
// without panicking on capacity guesses.
|
|
let big_spaces = " ".repeat(10_000);
|
|
let out = to_plain_text(&[seg(&format!("a{big_spaces}b"))]);
|
|
assert_eq!(out, "a b");
|
|
}
|
|
}
|