Files
diodiogod-TTS-Audio-Suite/utils/models/fallback_handler.py
diodiogod a8c62f2307 TIER 3: Complete architecture standardization for model loading
Implement standardized factory infrastructure enabling future engines:

1. ModelLoadConfig dataclass - Single canonical config class for all factories
   - Replaces duplicate UnifiedModelConfig
   - Contains device, model_name, language, model_path, repo_id, additional_params
   - Device resolution in __post_init__ for consistency

2. Engine capability registry - Data-driven engine specifications
   - Defines supports_voice_conversion, multilingual_model_switching, corruption recovery
   - Enables generic error handling without hardcoded engine checks
   - Covers all 7 TTS engines + IndexTTS-2

3. Fallback handler utility - Reusable fallback chain for graceful degradation
   - Try local → download → fallback language pattern
   - Eliminates duplicate fallback code across engines
   - Used by future engines automatically

4. Exception hierarchy - Standardized error types
   - TTSModelLoadingError base class
   - Specific types: NotFoundError, DeviceError, FormatError, InitializationError, LanguageNotSupportedError

Architecture benefits:
- All 8 factories now accept config: ModelLoadConfig instead of **kwargs
- Type-safe, clear factory signatures
- Future engines follow identical pattern
- No deprecated code left behind
- ComfyUI model manager handles both old and new patterns

Testing:
- All 7 TTS engines working correctly
- Device switching and caching working
- No syntax errors, all files compile

Note: SmartModelLoader still in use (Tier 2 cleanup pending) causing benign double-loads on some engines.
2025-11-14 10:10:52 -03:00

194 lines
6.6 KiB
Python

"""
Unified Fallback Handler for Model Loading
Provides a generic fallback chain mechanism that any TTS engine can use
to gracefully degrade when primary model loading fails.
Example: If French model not found, try English. If local not available, try HuggingFace.
"""
from typing import Callable, List, Tuple, Optional, Any
from utils.models.exceptions import TTSModelLoadingError
class FallbackHandler:
"""
Generic fallback chain for model loading.
Allows engines to define a sequence of fallback loaders to try.
Each fallback has a condition that determines if it should be attempted.
Example:
handler = FallbackHandler(primary_loader=load_french_model)
handler.add_fallback(load_english_model, condition=lambda e: "French" in str(e))
handler.add_fallback(load_from_huggingface, condition=lambda e: "not found" in str(e))
model = handler.load()
"""
def __init__(self, primary_loader: Callable, name: str = "Model"):
"""
Initialize fallback handler with primary loader.
Args:
primary_loader: Function to call first to load model
name: Name of what's being loaded (for logging/errors)
"""
self.primary_loader = primary_loader
self.name = name
self.fallbacks: List[Tuple[Callable, Optional[Callable]]] = []
self.attempted_loaders: List[str] = []
self.last_error: Optional[Exception] = None
def add_fallback(
self,
fallback_loader: Callable,
condition: Optional[Callable] = None,
name: Optional[str] = None
) -> "FallbackHandler":
"""
Add a fallback loader to the chain.
Args:
fallback_loader: Function to call if previous loading fails
condition: Optional function that returns True if fallback should be attempted
Receives the exception from previous failure
name: Optional name for this fallback (for logging)
Returns:
self for chaining
"""
self.fallbacks.append((fallback_loader, condition))
return self
def load(self) -> Any:
"""
Execute fallback chain.
Tries primary loader first, then fallbacks in order until one succeeds.
Returns:
Loaded model from first successful loader
Raises:
TTSModelLoadingError: If all loaders fail
"""
loaders_to_try = [(self.primary_loader, f"{self.name} (primary)", None)]
loaders_to_try.extend([
(loader, f"{self.name} (fallback {i+1})", condition)
for i, (loader, condition) in enumerate(self.fallbacks)
])
for loader, loader_name, condition in loaders_to_try:
try:
print(f"🔄 Attempting to load: {loader_name}")
result = loader()
if result is not None:
print(f"✅ Successfully loaded {loader_name}")
return result
except Exception as e:
self.last_error = e
self.attempted_loaders.append(loader_name)
# Check if this fallback's condition matches
if condition is not None:
try:
if not condition(e):
# Condition doesn't match, skip this fallback
print(f"⏭️ Skipping {loader_name} (condition not met)")
continue
except Exception:
pass # If condition check fails, continue anyway
print(f"⚠️ Failed to load {loader_name}: {str(e)}")
# All loaders failed
error_msg = f"Failed to load {self.name} after trying {len(self.attempted_loaders)} sources:\n"
error_msg += "\n".join(f" - {loader}" for loader in self.attempted_loaders)
if self.last_error:
error_msg += f"\n\nLast error: {self.last_error}"
raise TTSModelLoadingError(error_msg)
def with_language_fallback(
self,
primary_language: str,
fallback_languages: List[str],
loader_func: Callable
) -> "FallbackHandler":
"""
Convenience method for language-based fallbacks.
Automatically adds fallback loaders for alternative languages.
Args:
primary_language: Primary language to try
fallback_languages: List of languages to try in order
loader_func: Function that takes language and returns loader
e.g., lambda lang: lambda: load_model(language=lang)
Returns:
self for chaining
"""
for lang in fallback_languages:
if lang != primary_language:
self.add_fallback(
fallback_loader=loader_func(lang),
name=f"{lang} model"
)
return self
def with_local_then_remote(
self,
local_loader: Callable,
remote_loader: Callable
) -> "FallbackHandler":
"""
Convenience method for local-first, remote-fallback loading.
Args:
local_loader: Try loading from local filesystem first
remote_loader: Fall back to downloading from remote
Returns:
self for chaining
"""
self.add_fallback(
fallback_loader=remote_loader,
condition=lambda e: "not found" in str(e) or "FileNotFoundError" in type(e).__name__,
name="Remote (HuggingFace)"
)
return self
def create_language_fallback_chain(
primary_language: str,
fallback_languages: List[str],
load_model_func: Callable,
name: str = "Model"
) -> FallbackHandler:
"""
Create a pre-configured fallback handler for language switching.
Args:
primary_language: Primary language to load
fallback_languages: Languages to try if primary fails
load_model_func: Function that takes language parameter and loads model
name: Display name for logging
Returns:
Configured FallbackHandler ready to use
"""
handler = FallbackHandler(
primary_loader=lambda: load_model_func(primary_language),
name=name
)
for lang in fallback_languages:
if lang != primary_language:
handler.add_fallback(
fallback_loader=lambda l=lang: load_model_func(l),
name=f"{lang} model"
)
return handler