Files
diodiogod-TTS-Audio-Suite/utils/text/character_parser/segment_processor.py
T
diodiogod e129c5314b IndexTTS-2 implementation with modular character parser
MAJOR: IndexTTS-2 TTS engine integration
- Core IndexTTS engine with unified model interface integration
- Auto-downloader for HuggingFace models + GitHub source code
- Engine adapter with emotion support [Alice:angry_bob] syntax
- Cache integration for IndexTTS-2 parameters
- Factory registration for proper model management

MAJOR: Character parser modularization (930 → 300 lines each)
- Split into base_parser.py, language_resolver.py, segment_processor.py, validation.py
- Added emotion support with backward compatibility
- New split_by_character_with_emotions() method for advanced engines
- Flexible tag parsing: [de:Alice:angry_bob] support
- Language disambiguation to avoid Alice vs language code conflicts
- Full backward compatibility maintained

FEATURES:
- IndexTTS-2 emotion disentanglement (speaker vs emotion separation)
- Duration control and precise voice cloning
- Multi-modal emotion control (audio refs, text, vectors)
- Character switching with emotion references
- All official IndexTTS-2 parameters exposed

DOCS: Implementation plan and architectural TODO items added
2025-09-11 20:26:51 -03:00

253 lines
11 KiB
Python

"""
Segment Processor - Text segmentation and parsing functionality
Handles the core text processing logic for character tag parsing.
Extracted from character parser for better modularity.
"""
import re
from typing import List, Optional, Tuple, Pattern
from .base_parser import CharacterSegment
class SegmentProcessor:
"""
Handles text segmentation and character tag processing.
Responsible for:
- Line-by-line text processing
- Character tag detection and parsing
- Text segment creation with proper positioning
- Speaker format detection ("Speaker 1:", etc.)
"""
def __init__(self, character_tag_pattern: Pattern, default_character: str):
"""
Initialize segment processor.
Args:
character_tag_pattern: Regex pattern for detecting character tags
default_character: Default character for untagged text
"""
self.CHARACTER_TAG_PATTERN = character_tag_pattern
self.default_character = default_character
def parse_text_segments(self, text: str, language_resolver, character_parser) -> List[CharacterSegment]:
"""
Parse text into character-specific segments with proper line-by-line processing.
Args:
text: Input text with [Character] tags
language_resolver: LanguageResolver instance for language handling
character_parser: Main CharacterParser instance for character normalization
Returns:
List of CharacterSegment objects
"""
segments = []
# Split text by lines and process each line completely independently
lines = text.split('\n')
global_pos = 0
for line in lines:
line_start_pos = global_pos
original_line = line
line = line.strip()
if not line:
global_pos += len(original_line) + 1 # Account for empty line + newline
continue
# Each line is processed independently - no character state carries over
line_segments = self._parse_single_line(
line, line_start_pos, language_resolver, character_parser
)
segments.extend(line_segments)
global_pos += len(original_line) + 1 # +1 for newline
# If no segments were created, treat entire text as default character
if not segments and text.strip():
segments.append(CharacterSegment(
character=self.default_character,
text=text.strip(),
start_pos=0,
end_pos=len(text),
language=language_resolver.resolve_character_language(
self.default_character, None,
character_parser.character_language_defaults,
character_parser._character_language_cache,
character_parser._logged_characters
),
original_character=self.default_character,
explicit_language=False
))
return segments
def _parse_single_line(self, line: str, line_start_pos: int, language_resolver, character_parser) -> List[CharacterSegment]:
"""
Parse a single line for character tags, treating it completely independently.
Args:
line: Single line of text (no newlines)
line_start_pos: Starting position of this line in the original text
language_resolver: LanguageResolver instance
character_parser: Main CharacterParser instance
Returns:
List of CharacterSegment objects for this line only
"""
segments = []
current_pos = 0
current_character = self.default_character
current_language = language_resolver.default_language
current_emotion = None
# IMPORTANT: Each line starts fresh with narrator as default
# If the line doesn't start with a character tag, everything is narrator
# Check for manual "Speaker N:" format before processing character tags
speaker_match = self._parse_speaker_format_line(line)
if speaker_match:
speaker_name, speaker_text = speaker_match
if speaker_text.strip():
segments.append(CharacterSegment(
character=speaker_name, # Use "speaker 1", "speaker 2", etc.
text=speaker_text.strip(),
start_pos=line_start_pos,
end_pos=line_start_pos + len(line),
language=language_resolver.resolve_character_language(
speaker_name, None,
character_parser.character_language_defaults,
character_parser._character_language_cache,
character_parser._logged_characters
),
original_character=speaker_name,
explicit_language=False
))
return segments
# Quick check: if line doesn't contain any character tags, it's all narrator
if not self.CHARACTER_TAG_PATTERN.search(line):
if line.strip():
segments.append(CharacterSegment(
character=self.default_character,
text=line.strip(),
start_pos=line_start_pos,
end_pos=line_start_pos + len(line),
language=language_resolver.resolve_character_language(
self.default_character, None,
character_parser.character_language_defaults,
character_parser._character_language_cache,
character_parser._logged_characters
),
original_character=self.default_character,
explicit_language=False
))
return segments
# Find all character tags in this line
for match in self.CHARACTER_TAG_PATTERN.finditer(line):
# Add text before this tag (if any) with current character (narrator)
before_tag = line[current_pos:match.start()].strip()
if before_tag:
segments.append(CharacterSegment(
character=current_character,
text=before_tag,
start_pos=line_start_pos + current_pos,
end_pos=line_start_pos + match.start(),
language=current_language,
original_character=current_character, # Before this tag, it's already resolved
explicit_language=False, # Text before tags doesn't have explicit language
emotion=current_emotion
))
# Parse language, character, and emotion from the tag using flexible parser
raw_tag_content = match.group(1)
# Use flexible tag parser to handle emotion syntax
tag_info = language_resolver.parse_flexible_tag(raw_tag_content)
# Extract individual components
explicit_language = tag_info.get('language')
raw_character = tag_info.get('character') or self.default_character
emotion_reference = tag_info.get('emotion')
# Update current character for text after this tag
# IMPORTANT: Resolve language using original alias name before character normalization
current_language = language_resolver.resolve_character_language(
raw_character, explicit_language,
character_parser.character_language_defaults,
character_parser._character_language_cache,
character_parser._logged_characters
)
original_character = raw_character # Store original before normalization
current_explicit_language = explicit_language is not None # Track if language was explicit
# Detect language-only tags: if the original tag had empty character part and raw_character
# was defaulted to narrator, skip alias resolution to preserve narrator voice priority
is_language_only_tag = (raw_character == self.default_character and
':' in raw_tag_content and
raw_tag_content.split(':', 1)[1].strip() == '')
current_character = character_parser.normalize_character_name(
raw_character, skip_narrator_alias=is_language_only_tag
)
# Update emotion state if provided in tag
if emotion_reference is not None:
current_emotion = emotion_reference
current_pos = match.end()
# Add remaining text after last tag (or entire line if no tags)
remaining_text = line[current_pos:].strip()
if remaining_text:
segments.append(CharacterSegment(
character=current_character,
text=remaining_text,
start_pos=line_start_pos + current_pos,
end_pos=line_start_pos + len(line),
language=current_language,
original_character=original_character,
explicit_language=current_explicit_language if 'current_explicit_language' in locals() else False,
emotion=current_emotion
))
elif not segments and line.strip():
# Line with only tags and no text after - still need a segment for the line
# This handles edge cases like a line that is just "[character]"
segments.append(CharacterSegment(
character=current_character,
text="",
start_pos=line_start_pos,
end_pos=line_start_pos + len(line),
language=current_language,
original_character=original_character if 'original_character' in locals() else current_character,
explicit_language=current_explicit_language if 'current_explicit_language' in locals() else False,
emotion=current_emotion
))
return segments
def _parse_speaker_format_line(self, line: str) -> Optional[Tuple[str, str]]:
"""
Parse a line for manual "Speaker N:" format.
Args:
line: Single line to check
Returns:
Tuple of (speaker_name, text) if found, None otherwise
"""
import re
# Match "Speaker N: text" (case insensitive)
match = re.match(r'^(speaker\s*\d+)\s*:\s*(.*)$', line.strip(), re.IGNORECASE)
if match:
speaker_name = match.group(1).lower().strip() # Normalize to "speaker 1", "speaker 2", etc.
speaker_text = match.group(2)
# Normalize speaker name format
speaker_name = re.sub(r'\s+', ' ', speaker_name) # "speaker 1" -> "speaker 1"
return speaker_name, speaker_text
return None