sail / sail_scripts /kernel /src /text_cleaner.rs
muterornament's picture
Industrialize: Backup sovereign training pipeline and native kernel
0f6a233 verified
Raw
History Blame Contribute Delete
5.68 kB
//! Text Cleaner β€” Safe UTF-8 text normalization kernel
//!
//! All operations use bounded buffers and validated UTF-8.
//! No unsafe code. No raw pointer arithmetic.
//!
//! Operations:
//! 1. Strip HTML tags
//! 2. Unicode NFC normalization
//! 3. Remove non-printable characters
//! 4. Collapse whitespace
//! 5. De-duplicate newlines
//! 6. Trim
use pyo3::prelude::*;
use rayon::prelude::*;
use regex::Regex;
use unicode_normalization::UnicodeNormalization;
use once_cell::sync::Lazy;
// ── Compiled regexes (compiled once, used many times safely) ──────────────────
/// Matches any HTML/XML tag
static HTML_TAG: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"<[^>]{0,2000}>").expect("HTML_TAG regex is valid")
});
/// Matches HTML entities like &amp; &lt; &nbsp;
static HTML_ENTITY: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"&(?:[a-zA-Z]{1,10}|#\d{1,6});").expect("HTML_ENTITY regex is valid")
});
/// Matches sequences of 3+ newlines
static EXCESS_NEWLINES: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\n{3,}").expect("EXCESS_NEWLINES regex is valid")
});
/// Matches sequences of 2+ spaces/tabs (but not newlines)
static EXCESS_SPACES: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"[ \t]{2,}").expect("EXCESS_SPACES regex is valid")
});
/// Matches non-printable ASCII control characters (except \n, \t)
static CONTROL_CHARS: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]").expect("CONTROL_CHARS regex is valid")
});
// ── Core cleaning function (runs in Rust, no GIL held) ───────────────────────
/// Clean a single text string.
///
/// Returns a cleaned version of the text with:
/// - HTML stripped
/// - Unicode normalized to NFC
/// - Control characters removed
/// - Excess whitespace collapsed
fn clean_text_internal(text: &str) -> String {
// 1. Bound input size (prevents pathological inputs from blowing the stack)
let input = if text.len() > 10_000_000 {
&text[..10_000_000]
} else {
text
};
// 2. Strip HTML
let no_html = HTML_TAG.replace_all(input, " ");
let no_entities = HTML_ENTITY.replace_all(&no_html, " ");
// 3. Unicode NFC normalization (safe: returns owned String)
let normalized: String = no_entities.nfc().collect();
// 4. Remove non-printable control characters
let no_ctrl = CONTROL_CHARS.replace_all(&normalized, "");
// 5. Collapse excess whitespace
let single_spaces = EXCESS_SPACES.replace_all(&no_ctrl, " ");
let clean_newlines = EXCESS_NEWLINES.replace_all(&single_spaces, "\n\n");
// 6. Trim
clean_newlines.trim().to_string()
}
// ── Python-exposed functions ───────────────────────────────────────────────────
/// Clean a single text string. Releases GIL during heavy processing.
///
/// Args:
/// text (str): Raw text to clean
///
/// Returns:
/// str: Cleaned text
#[pyfunction]
pub fn clean_text(py: Python<'_>, text: String) -> String {
// Release GIL β€” this is pure CPU work, no Python objects touched inside
py.allow_threads(|| clean_text_internal(&text))
}
/// Clean a batch of text strings in parallel using Rayon.
///
/// This is 4–8x faster than calling clean_text() in a Python loop because:
/// - All threads run without the GIL
/// - Rayon work-steals across cores
/// - No Python object allocation per item
///
/// Args:
/// texts (list[str]): List of raw text strings
///
/// Returns:
/// list[str]: List of cleaned strings, same order as input
#[pyfunction]
pub fn clean_batch(py: Python<'_>, texts: Vec<String>) -> Vec<String> {
py.allow_threads(|| {
texts.par_iter()
.map(|t| clean_text_internal(t))
.collect()
})
}
// ── Unit Tests ────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_html() {
let result = clean_text_internal("<p>Hello <b>world</b></p>");
assert!(!result.contains('<'), "HTML tags should be removed");
assert!(result.contains("Hello"), "Text content preserved");
}
#[test]
fn test_unicode_nfc() {
// CafΓ© with decomposed 'e + combining accent' vs precomposed 'Γ©'
let decomposed = "Cafe\u{0301}"; // decomposed
let result = clean_text_internal(decomposed);
let expected = "Caf\u{00E9}"; // NFC precomposed
assert_eq!(result, expected, "Unicode should be NFC normalized");
}
#[test]
fn test_collapse_whitespace() {
let result = clean_text_internal("hello world\n\n\n\nfoo");
assert!(!result.contains(" "), "Excess spaces collapsed");
assert!(!result.contains("\n\n\n"), "Excess newlines collapsed");
}
#[test]
fn test_control_chars_removed() {
let result = clean_text_internal("hello\x00\x01\x07world");
assert!(!result.contains('\x00'), "Null bytes removed");
assert!(result.contains("helloworld"), "Text content preserved");
}
#[test]
fn test_empty_string() {
assert_eq!(clean_text_internal(""), "");
}
#[test]
fn test_large_input_bounded() {
// Should not panic or OOM on huge inputs
let large = "a".repeat(20_000_000);
let result = clean_text_internal(&large);
assert!(result.len() <= 10_000_000, "Large input is bounded safely");
}
}