Source code for cursus.processing.text.dialogue_processor

import re
from bs4 import BeautifulSoup
from typing import List, Union, Dict, Optional

from ..processors import Processor


# Processor 1: Text Normalization
[docs] class TextNormalizationProcessor(Processor): def __init__(self): super().__init__() self.processor_name = "text_normalization_processor"
[docs] def process(self, input_text: Union[str, List[str]]) -> Union[str, List[str]]: def _norm(s: str) -> str: s = s.strip().lower() return re.sub(r"\s+", " ", s) if isinstance(input_text, list): return [_norm(msg) for msg in input_text] else: return _norm(input_text)
# Processor 1: Text Normalization
[docs] class TextUpperProcessor(Processor): def __init__(self): super().__init__() self.processor_name = "text_upper_processor"
[docs] def process(self, input_text: str): # Basic normalization: trim and lowercase. normalized = input_text.strip().upper() # Collapse multiple spaces into one. normalized = re.sub(r"\s+", " ", normalized) return normalized
# ===================================================================================== # Processor 2: Dialogue Splitting
[docs] class DialogueSplitterProcessor(Processor): def __init__(self, min_length: int = 1): """ Args: min_length: Minimum number of non-whitespace characters required to keep a message. """ super().__init__() self.processor_name = "dialogue_splitter_processor" self.min_length = min_length
[docs] def process(self, input_text: str): """ Splits the dialogue into individual messages based on [bom] and [eom] delimiters. Returns: List of message strings. """ pattern = r"\[bom\](.*?)\[eom\]" raw_messages = re.findall(pattern, input_text, flags=re.DOTALL) # Strip whitespace and filter out short/empty messages messages = [ msg.strip() for msg in raw_messages if msg.strip() and len(msg.strip()) >= self.min_length ] return messages
# ===================================================================================== # Processor 3: Dialogue Chunker
[docs] class DialogueChunkerProcessor(Processor): def __init__( self, tokenizer, max_tokens=512, truncate: bool = False, # Added truncate parameter max_total_chunks: Optional[int] = 5, ): """ Args: tokenizer: A Hugging Face AutoTokenizer instance. max_tokens: Maximum token count per chunk. """ super().__init__() self.processor_name = "dialogue_chunker_processor" self.tokenizer = tokenizer self.max_tokens = max_tokens self.max_total_chunks = max_total_chunks self.truncate = truncate
[docs] def process(self, messages: List[str]): """ Chunks a list of messages into groups such that each chunk's token count (without special tokens) does not exceed the max_tokens limit. Returns: List of dialogue chunks (each chunk is a concatenated string of messages). """ chunks = [] current_chunk = [] current_tokens = 0 num_chunks = 0 # Track the number of chunks created for msg in messages: # Count tokens using the HF AutoTokenizer; avoid adding special tokens here tokens = self.tokenizer.encode(msg, add_special_tokens=False) token_count = len(tokens) # CRITICAL FIX: Truncate individual messages that exceed max_tokens # This prevents OOM errors from oversized sequences reaching the model if token_count > self.max_tokens: # Truncate the message to max_tokens tokens = tokens[: self.max_tokens] msg = self.tokenizer.decode(tokens, skip_special_tokens=True) # Recompute from the decoded text: decode→re-encode is not # round-trip-stable, so the post-truncation msg may tokenize to a # DIFFERENT count than max_tokens. Assuming max_tokens here would # desync token_count from the msg actually used downstream. token_count = len(self.tokenizer.encode(msg, add_special_tokens=False)) # If adding this message would exceed limit, save current chunk and start a new one. if current_tokens + token_count > self.max_tokens: if current_chunk: chunks.append(" ".join(current_chunk).strip()) num_chunks += 1 # Increment chunk count if ( self.max_total_chunks is not None and self.truncate and num_chunks >= self.max_total_chunks ): break # Stop if max_total_chunks is reached current_chunk = [msg] current_tokens = token_count else: current_chunk.append(msg) current_tokens += token_count if ( self.max_total_chunks is not None and self.truncate and num_chunks >= self.max_total_chunks ): break # Stop if max_total_chunks is reached if current_chunk and (not self.truncate or num_chunks < self.max_total_chunks): chunks.append(" ".join(current_chunk).strip()) num_chunks += 1 # Ensure at least one non-empty chunk exists if not chunks: chunks = ["."] elif all(not chunk.strip() for chunk in chunks): chunks = ["."] return chunks
# ==================================================================================== # --- Processor 4: Emoji Remover ---
[docs] class EmojiRemoverProcessor(Processor): def __init__(self): super().__init__() self.processor_name = "emoji_remover_processor" self.emoji_pattern = re.compile( "[" "\U0001f600-\U0001f64f" # emoticons "\U0001f300-\U0001f5ff" # symbols & pictographs "\U0001f680-\U0001f6ff" # transport & map symbols "\U0001f1e0-\U0001f1ff" # flags "\U00002702-\U000027b0" # dingbats "\U000024c2-\U0001f251" "]+", flags=re.UNICODE, )
[docs] def process(self, input_text: Union[str, List[str]]) -> Union[str, List[str]]: def _remove(s: str) -> str: return self.emoji_pattern.sub("", s) if isinstance(input_text, list): return [_remove(msg) for msg in input_text] else: return _remove(input_text)
# ===================================================================================== # --- Processor 2: HTML Normalization ---
[docs] class HTMLNormalizerProcessor(Processor): def __init__(self): super().__init__() self.processor_name = "html_normalizer_processor"
[docs] def process(self, input_text: Union[str, List[str]]) -> Union[str, List[str]]: """ If given a list of dialogue messages, normalize each one; otherwise normalize the single HTML string. """ def _norm_single(text: str) -> str: soup = BeautifulSoup(text, "html.parser") # collapse whitespace and strip return soup.get_text(separator=" ", strip=True) if isinstance(input_text, list): # apply to each chunk/message return [_norm_single(msg) for msg in input_text] else: return _norm_single(input_text)