Fix ChatterBox Official 23-Lang language resolution and narrator input Technical details: - Fix resolve_language_alias AttributeError across multiple engines - Fix ChatterBox Official 23-Lang not using opt_narrator audio input - Fix phonemizer_utils language alias resolution method call - Fix character_parser validation module language resolution
220 lines
8.6 KiB
Python
220 lines
8.6 KiB
Python
"""
|
|
Validation and Statistics - Character parser validation functionality
|
|
|
|
Extracted from character parser for better modularity.
|
|
Handles tag validation, statistics, and error reporting.
|
|
"""
|
|
|
|
import re
|
|
from typing import Dict, List, Tuple, Any, Pattern
|
|
|
|
|
|
class ValidationMixin:
|
|
"""
|
|
Mixin class providing validation and statistics functionality for character parsers.
|
|
|
|
Extracted to separate concerns and maintain modularity.
|
|
"""
|
|
|
|
def validate_character_tags(self, text: str) -> Tuple[bool, List[str]]:
|
|
"""
|
|
Validate character tags in text and return any issues.
|
|
|
|
Args:
|
|
text: Input text with [Character] tags
|
|
|
|
Returns:
|
|
Tuple of (is_valid, list_of_issues)
|
|
"""
|
|
issues = []
|
|
|
|
# Use the character tag pattern from the main parser
|
|
character_tag_pattern = getattr(self, 'CHARACTER_TAG_PATTERN', None)
|
|
if not character_tag_pattern:
|
|
# Fallback pattern if not available
|
|
character_tag_pattern = re.compile(r'\[(?!(?:pause|wait|stop):)(?!(?:it|IT|italian|Italian)\])([^\]]+)\]')
|
|
|
|
# Find all character tags
|
|
tags = character_tag_pattern.findall(text)
|
|
|
|
# Check for empty tags
|
|
empty_tags = [tag for tag in tags if not tag.strip()]
|
|
if empty_tags:
|
|
issues.append(f"Found {len(empty_tags)} empty character tag(s)")
|
|
|
|
# Check for unmatched brackets
|
|
open_brackets = text.count('[')
|
|
close_brackets = text.count(']')
|
|
if open_brackets != close_brackets:
|
|
issues.append(f"Unmatched brackets: {open_brackets} '[' vs {close_brackets} ']'")
|
|
|
|
# Check for characters not in available list (if set)
|
|
available_characters = getattr(self, 'available_characters', set())
|
|
if available_characters:
|
|
unknown_chars = []
|
|
for tag in tags:
|
|
normalized = self.normalize_character_name(tag)
|
|
default_character = getattr(self, 'default_character', 'narrator')
|
|
if normalized == default_character and tag.strip().lower() != default_character:
|
|
unknown_chars.append(tag)
|
|
|
|
if unknown_chars:
|
|
issues.append(f"Unknown characters (will use fallback): {', '.join(unknown_chars)}")
|
|
|
|
return len(issues) == 0, issues
|
|
|
|
def get_statistics(self, text: str) -> Dict[str, Any]:
|
|
"""
|
|
Get statistics about character usage in text.
|
|
|
|
Args:
|
|
text: Input text with [Character] tags
|
|
|
|
Returns:
|
|
Dictionary with statistics
|
|
"""
|
|
segments = self.parse_text_segments(text)
|
|
|
|
character_counts = {}
|
|
character_lengths = {}
|
|
emotion_usage = {}
|
|
|
|
for segment in segments:
|
|
char = segment.character
|
|
character_counts[char] = character_counts.get(char, 0) + 1
|
|
character_lengths[char] = character_lengths.get(char, 0) + len(segment.text)
|
|
|
|
# Track emotion usage if present
|
|
if hasattr(segment, 'emotion') and segment.emotion:
|
|
if char not in emotion_usage:
|
|
emotion_usage[char] = set()
|
|
emotion_usage[char].add(segment.emotion)
|
|
|
|
total_chars = sum(character_counts.values())
|
|
total_length = sum(character_lengths.values())
|
|
|
|
stats = {
|
|
"total_segments": len(segments),
|
|
"unique_characters": len(character_counts),
|
|
"character_counts": character_counts,
|
|
"character_lengths": character_lengths,
|
|
"total_character_switches": total_chars - 1,
|
|
"total_text_length": total_length,
|
|
"average_segment_length": total_length / len(segments) if segments else 0
|
|
}
|
|
|
|
# Add emotion statistics if any emotions were found
|
|
if emotion_usage:
|
|
stats["emotion_usage"] = {char: list(emotions) for char, emotions in emotion_usage.items()}
|
|
stats["characters_with_emotions"] = len(emotion_usage)
|
|
stats["total_emotion_references"] = sum(len(emotions) for emotions in emotion_usage.values())
|
|
|
|
return stats
|
|
|
|
def get_character_language_summary(self) -> str:
|
|
"""
|
|
Get a consolidated summary of character language mappings for logging.
|
|
|
|
Returns:
|
|
Formatted summary string of character→language mappings
|
|
"""
|
|
character_language_cache = getattr(self, '_character_language_cache', {})
|
|
|
|
if hasattr(self, 'language_resolver'):
|
|
return self.language_resolver.get_character_language_summary(character_language_cache)
|
|
|
|
# Fallback implementation if language resolver not available
|
|
if not character_language_cache:
|
|
return ""
|
|
|
|
# Group characters by language
|
|
lang_groups = {}
|
|
for char, lang in character_language_cache.items():
|
|
if lang not in lang_groups:
|
|
lang_groups[lang] = []
|
|
lang_groups[lang].append(char)
|
|
|
|
# Format as: Alice(de), Bob(fr→en fallback), Others(en)
|
|
summary_parts = []
|
|
for lang, chars in sorted(lang_groups.items()):
|
|
if len(chars) == 1:
|
|
summary_parts.append(f"{chars[0]}({lang})")
|
|
else:
|
|
summary_parts.append(f"{', '.join(chars)}({lang})")
|
|
|
|
return ', '.join(summary_parts)
|
|
|
|
def convert_to_language_hints_for_higgs(self, text: str) -> str:
|
|
"""
|
|
Convert character tags with language prefixes to language hints for Higgs Audio 2.
|
|
|
|
Converts [En:Alice] Hello there -> [English] Hello there
|
|
This preserves language context for the smart Higgs model while removing character switching.
|
|
|
|
Args:
|
|
text: Input text with [language:character] tags
|
|
|
|
Returns:
|
|
Text with language hints for Higgs Audio 2
|
|
"""
|
|
# Use the character tag pattern
|
|
character_tag_pattern = getattr(self, 'CHARACTER_TAG_PATTERN', None)
|
|
if not character_tag_pattern:
|
|
character_tag_pattern = re.compile(r'\[(?!(?:pause|wait|stop):)(?!(?:it|IT|italian|Italian)\])([^\]]+)\]')
|
|
|
|
# Language-character pattern
|
|
language_character_pattern = re.compile(r'^([a-zA-Z0-9\-_À-ÿ\s]+):(.*)$')
|
|
|
|
def replace_tag(match):
|
|
tag_content = match.group(1)
|
|
|
|
# Parse language:character format
|
|
lang_match = language_character_pattern.match(tag_content.strip())
|
|
if lang_match:
|
|
raw_language = lang_match.group(1).strip()
|
|
|
|
# Get language resolver if available
|
|
language_resolver = getattr(self, 'language_resolver', None)
|
|
if language_resolver:
|
|
# Resolve to canonical form first, then get display name
|
|
from utils.models.language_mapper import resolve_language_alias
|
|
canonical_lang = resolve_language_alias(raw_language)
|
|
display_name = language_resolver.get_language_display_name(canonical_lang)
|
|
else:
|
|
# Fallback - just use the raw language
|
|
display_name = raw_language.title()
|
|
|
|
return f'[{display_name}]'
|
|
else:
|
|
# Not a language:character tag, remove entirely
|
|
return ''
|
|
|
|
# Apply conversion
|
|
converted_text = character_tag_pattern.sub(replace_tag, text)
|
|
|
|
# Clean up any double spaces
|
|
converted_text = ' '.join(converted_text.split())
|
|
|
|
return converted_text
|
|
|
|
|
|
def validate_character_text(text: str) -> Tuple[bool, List[str]]:
|
|
"""
|
|
Convenience function to validate character text.
|
|
|
|
Args:
|
|
text: Input text with [Character] tags
|
|
|
|
Returns:
|
|
Tuple of (is_valid, list_of_issues)
|
|
"""
|
|
# Create a minimal validator instance
|
|
class BasicValidator(ValidationMixin):
|
|
def normalize_character_name(self, name):
|
|
return name.lower().strip()
|
|
|
|
def parse_text_segments(self, text):
|
|
return [] # Minimal implementation for validation only
|
|
|
|
validator = BasicValidator()
|
|
return validator.validate_character_tags(text) |