""" Pause Tag Processing Module Handles parsing and processing of pause tags in text for TTS generation. Supports formats: - [pause:2] - 2 seconds - [pause:1.5] - 1.5 seconds - [pause:500ms] - 500 milliseconds - [pause:2s] - 2 seconds (explicit) Aliases supported: - [wait:2] - same as [pause:2] - [stop:1.5] - same as [pause:1.5] """ import re import torch from typing import List, Tuple, Union, Optional from utils.audio.processing import AudioProcessingUtils class PauseTagProcessor: """Handles pause tag parsing and audio generation with pauses""" # Regex pattern for flexible pause tag matching (supports pause, wait, stop aliases) PAUSE_PATTERN = r'\[(?:pause|wait|stop):(\d+(?:\.\d+)?)(s|ms)?\]' @staticmethod def parse_pause_tags(text: str) -> Tuple[List[Tuple[str, Union[str, float]]], str]: """ Parse pause tags from text and return segments with pause information. Args: text: Input text with pause tags Returns: Tuple of (segments, clean_text) where segments contain ('text', content) or ('pause', duration_seconds) """ def normalize_duration(duration_str: str, unit: Optional[str] = None) -> float: """Convert pause duration to seconds""" duration = float(duration_str) if unit and unit.lower() == 'ms': return duration / 1000.0 return duration segments = [] last_end = 0 for match in re.finditer(PauseTagProcessor.PAUSE_PATTERN, text, re.IGNORECASE): if match.start() > last_end: text_content = text[last_end:match.start()].strip() if text_content: segments.append(('text', text_content)) duration = normalize_duration(match.group(1), match.group(2)) segments.append(('pause', duration)) last_end = match.end() if last_end < len(text): remaining_text = text[last_end:].strip() if remaining_text: segments.append(('text', remaining_text)) # Create clean text without pause tags clean_text = re.sub(PauseTagProcessor.PAUSE_PATTERN, ' ', text, flags=re.IGNORECASE) clean_text = re.sub(r'\s+', ' ', clean_text).strip() return segments, clean_text @staticmethod def has_pause_tags(text: str) -> bool: """Check if text contains pause tags""" return bool(re.search(PauseTagProcessor.PAUSE_PATTERN, text, re.IGNORECASE)) @staticmethod def create_silence_segment(duration_seconds: float, sample_rate: int, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> torch.Tensor: """ Create silence tensor for pause segments. Args: duration_seconds: Duration of silence sample_rate: Audio sample rate device: Target device for tensor dtype: Target dtype for tensor Returns: Silence tensor with shape (1, num_samples) """ # Clamp duration to reasonable limits duration_seconds = max(0.0, min(duration_seconds, 30.0)) # 0 to 30 seconds max num_samples = int(duration_seconds * sample_rate) silence = torch.zeros(1, num_samples, device=device, dtype=dtype) return silence @staticmethod def generate_audio_with_pauses(segments: List[Tuple[str, Union[str, float]]], tts_generate_func, sample_rate: int, **generation_kwargs) -> torch.Tensor: """ Generate audio from parsed segments with pauses. Args: segments: List of ('text', content) or ('pause', duration) tuples tts_generate_func: Function to call for text generation sample_rate: Audio sample rate **generation_kwargs: Additional arguments for TTS generation Returns: Combined audio tensor with pauses """ if not segments: return torch.zeros(1, 0) audio_segments = [] for segment_type, content in segments: if segment_type == 'text': # Generate audio for text segment audio = tts_generate_func(content, **generation_kwargs) # Ensure correct shape (1, num_samples) if audio.dim() == 1: audio = audio.unsqueeze(0) elif audio.dim() > 2: audio = audio.squeeze() if audio.dim() == 1: audio = audio.unsqueeze(0) audio_segments.append(audio) elif segment_type == 'pause': # Create silence segment device = audio_segments[-1].device if audio_segments else None dtype = audio_segments[-1].dtype if audio_segments else None silence = PauseTagProcessor.create_silence_segment( content, sample_rate, device, dtype ) audio_segments.append(silence) # Concatenate all segments if audio_segments: return torch.cat(audio_segments, dim=-1) else: return torch.zeros(1, 0) @staticmethod def preprocess_text_with_pause_tags(text: str, enable_pause_tags: bool = True) -> Tuple[str, Optional[List]]: """ Preprocess text for TTS generation, handling pause tags if enabled. Args: text: Input text enable_pause_tags: Whether to process pause tags Returns: Tuple of (processed_text, segments_or_none) - If pause tags disabled or no tags found: (original_text, None) - If pause tags enabled and found: (clean_text, segments) """ if not enable_pause_tags or not PauseTagProcessor.has_pause_tags(text): return text, None segments, clean_text = PauseTagProcessor.parse_pause_tags(text) return clean_text, segments