243 lines
7.3 KiB
Rust
243 lines
7.3 KiB
Rust
//! Complete tokenization pipeline
|
|
//!
|
|
//! This module provides a unified tokenization pipeline that can combine
|
|
//! multiple tokenizers and preprocessing steps.
|
|
|
|
use crate::{Result, TokenId, TokenizationError, TokenizationStats, Tokenizer};
|
|
use parking_lot::RwLock;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::sync::Arc;
|
|
|
|
/// Pipeline configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PipelineConfig {
|
|
/// Name of the pipeline
|
|
pub name: String,
|
|
/// Version of the pipeline
|
|
pub version: String,
|
|
/// Maximum input length
|
|
pub max_input_length: usize,
|
|
/// Whether to truncate long inputs
|
|
pub truncate: bool,
|
|
/// Padding token ID
|
|
pub pad_token_id: Option<TokenId>,
|
|
}
|
|
|
|
/// Preprocessing step
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum PreprocessingStep {
|
|
/// Lowercase the text
|
|
Lowercase,
|
|
/// Normalize unicode
|
|
NormalizeUnicode,
|
|
/// Remove extra whitespace
|
|
NormalizeWhitespace,
|
|
/// Apply custom regex replacement
|
|
RegexReplace {
|
|
/// Regular expression pattern to match
|
|
pattern: String,
|
|
/// Replacement string
|
|
replacement: String,
|
|
},
|
|
}
|
|
|
|
/// Tokenization pipeline
|
|
#[derive(Debug)]
|
|
pub struct TokenizationPipeline {
|
|
config: PipelineConfig,
|
|
tokenizer: Arc<dyn Tokenizer>,
|
|
preprocessing_steps: Vec<PreprocessingStep>,
|
|
stats: Arc<RwLock<TokenizationStats>>,
|
|
}
|
|
|
|
impl TokenizationPipeline {
|
|
/// Create a new tokenization pipeline
|
|
pub fn new(
|
|
config: PipelineConfig,
|
|
tokenizer: Arc<dyn Tokenizer>,
|
|
preprocessing_steps: Vec<PreprocessingStep>,
|
|
) -> Self {
|
|
Self {
|
|
config,
|
|
tokenizer,
|
|
preprocessing_steps,
|
|
stats: Arc::new(RwLock::new(TokenizationStats::default())),
|
|
}
|
|
}
|
|
|
|
/// Apply preprocessing steps to text
|
|
pub fn preprocess(&self, text: &str) -> Result<String> {
|
|
let mut processed = text.to_string();
|
|
|
|
for step in &self.preprocessing_steps {
|
|
processed = self.apply_preprocessing_step(&processed, step)?;
|
|
}
|
|
|
|
Ok(processed)
|
|
}
|
|
|
|
/// Apply a single preprocessing step
|
|
fn apply_preprocessing_step(&self, text: &str, step: &PreprocessingStep) -> Result<String> {
|
|
match step {
|
|
PreprocessingStep::Lowercase => Ok(text.to_lowercase()),
|
|
PreprocessingStep::NormalizeUnicode => {
|
|
use unicode_normalization::UnicodeNormalization;
|
|
Ok(text.nfc().collect())
|
|
}
|
|
PreprocessingStep::NormalizeWhitespace => {
|
|
Ok(text.split_whitespace().collect::<Vec<_>>().join(" "))
|
|
}
|
|
PreprocessingStep::RegexReplace {
|
|
pattern,
|
|
replacement,
|
|
} => {
|
|
let regex = regex::Regex::new(pattern)
|
|
.map_err(|e| TokenizationError::InvalidInput(format!("Invalid regex: {e}")))?;
|
|
Ok(regex.replace_all(text, replacement).to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Process text through the full pipeline
|
|
pub async fn process(&self, text: &str) -> Result<Vec<TokenId>> {
|
|
let start_time = std::time::Instant::now();
|
|
|
|
// Apply preprocessing
|
|
let preprocessed = self.preprocess(text)?;
|
|
|
|
// Check length limits
|
|
if preprocessed.len() > self.config.max_input_length {
|
|
if self.config.truncate {
|
|
let truncated = preprocessed
|
|
.chars()
|
|
.take(self.config.max_input_length)
|
|
.collect::<String>();
|
|
return self.tokenizer.encode(&truncated).await;
|
|
}
|
|
return Err(TokenizationError::InvalidInput(format!(
|
|
"Input length {} exceeds maximum {}",
|
|
preprocessed.len(),
|
|
self.config.max_input_length
|
|
)));
|
|
}
|
|
|
|
// Tokenize
|
|
let mut token_ids = self.tokenizer.encode(&preprocessed).await?;
|
|
|
|
// Apply padding if configured
|
|
if let Some(pad_token_id) = self.config.pad_token_id {
|
|
// This is a simple example - real padding would be more sophisticated
|
|
if token_ids.is_empty() {
|
|
token_ids.push(pad_token_id);
|
|
}
|
|
}
|
|
|
|
// Update statistics
|
|
{
|
|
let mut stats = self.stats.write();
|
|
stats.token_count = token_ids.len();
|
|
stats.processing_time_ms = start_time.elapsed().as_millis() as u64;
|
|
}
|
|
|
|
Ok(token_ids)
|
|
}
|
|
|
|
/// Decode tokens back to text
|
|
pub async fn decode_tokens(&self, token_ids: &[TokenId]) -> Result<String> {
|
|
self.tokenizer.decode(token_ids).await
|
|
}
|
|
|
|
/// Get pipeline configuration
|
|
#[must_use]
|
|
pub fn get_config(&self) -> &PipelineConfig {
|
|
&self.config
|
|
}
|
|
|
|
/// Get pipeline statistics
|
|
#[must_use]
|
|
pub fn get_stats(&self) -> TokenizationStats {
|
|
let mut combined_stats = self.stats.read().clone();
|
|
let tokenizer_stats = self.tokenizer.get_stats();
|
|
|
|
// Combine statistics
|
|
combined_stats.unique_tokens = tokenizer_stats.unique_tokens;
|
|
combined_stats.avg_token_length = tokenizer_stats.avg_token_length;
|
|
|
|
combined_stats
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::bpe::{BpeConfig, BpeTokenizer};
|
|
|
|
#[tokio::test]
|
|
async fn pipeline_creation() {
|
|
let bpe_config = BpeConfig::default();
|
|
let tokenizer = Arc::new(BpeTokenizer::new(bpe_config).unwrap());
|
|
|
|
let config = PipelineConfig {
|
|
name: "test_pipeline".to_string(),
|
|
version: "1.0.0".to_string(),
|
|
max_input_length: 1000,
|
|
truncate: true,
|
|
pad_token_id: Some(1),
|
|
};
|
|
|
|
let steps = vec![
|
|
PreprocessingStep::Lowercase,
|
|
PreprocessingStep::NormalizeWhitespace,
|
|
];
|
|
|
|
let pipeline = TokenizationPipeline::new(config, tokenizer, steps);
|
|
assert_eq!(pipeline.get_config().name, "test_pipeline");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pipeline_preprocessing() {
|
|
let bpe_config = BpeConfig::default();
|
|
let tokenizer = Arc::new(BpeTokenizer::new(bpe_config).unwrap());
|
|
|
|
let config = PipelineConfig {
|
|
name: "test_pipeline".to_string(),
|
|
version: "1.0.0".to_string(),
|
|
max_input_length: 1000,
|
|
truncate: true,
|
|
pad_token_id: None,
|
|
};
|
|
|
|
let steps = vec![
|
|
PreprocessingStep::Lowercase,
|
|
PreprocessingStep::NormalizeWhitespace,
|
|
];
|
|
|
|
let pipeline = TokenizationPipeline::new(config, tokenizer, steps);
|
|
|
|
let processed = pipeline.preprocess(" HELLO WORLD ").unwrap();
|
|
assert_eq!(processed, "hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn preprocessing_step_serialization() {
|
|
let step = PreprocessingStep::RegexReplace {
|
|
pattern: r"\d+".to_string(),
|
|
replacement: "[NUM]".to_string(),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&step).unwrap();
|
|
let deserialized: PreprocessingStep = serde_json::from_str(&serialized).unwrap();
|
|
|
|
match deserialized {
|
|
PreprocessingStep::RegexReplace {
|
|
pattern,
|
|
replacement,
|
|
} => {
|
|
assert_eq!(pattern, r"\d+");
|
|
assert_eq!(replacement, "[NUM]");
|
|
}
|
|
_ => panic!("Wrong preprocessing step type"),
|
|
}
|
|
}
|
|
}
|