Spaces:
Running
Running
| """ | |
| Plugin SDK β abstract interface for third-party TTS engines. | |
| Allows community contributors to add support for ElevenLabs, XTTS, Bark, | |
| Fish TTS, etc. without modifying core OmniVoice code. | |
| Usage: | |
| 1. Create a Python file in backend/plugins/ (e.g. elevenlabs.py) | |
| 2. Subclass `TTSPlugin` and implement the 4 abstract methods | |
| 3. Register via `@register_plugin` decorator or add to PLUGINS dict | |
| 4. The engine will appear in the frontend Settings β TTS Engine picker | |
| Example: | |
| from services.plugin_sdk import TTSPlugin, register_plugin | |
| @register_plugin | |
| class ElevenLabsPlugin(TTSPlugin): | |
| id = "elevenlabs" | |
| display_name = "ElevenLabs" | |
| ... | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from abc import ABC, abstractmethod | |
| from typing import Optional | |
| logger = logging.getLogger("omnivoice.plugins") | |
| # ββ Plugin registry ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PLUGINS: dict[str, type["TTSPlugin"]] = {} | |
| def register_plugin(cls: type["TTSPlugin"]) -> type["TTSPlugin"]: | |
| """Decorator: register a TTS plugin class by its `id`.""" | |
| if not hasattr(cls, "id") or not cls.id: | |
| raise ValueError(f"Plugin class {cls.__name__} must define a non-empty `id`.") | |
| PLUGINS[cls.id] = cls | |
| logger.info("Registered TTS plugin: %s (%s)", cls.id, cls.display_name) | |
| return cls | |
| def get_plugin(plugin_id: str) -> "TTSPlugin": | |
| """Instantiate and return a plugin by id.""" | |
| cls = PLUGINS.get(plugin_id) | |
| if cls is None: | |
| available = ", ".join(sorted(PLUGINS.keys())) or "none" | |
| raise KeyError(f"Unknown TTS plugin '{plugin_id}'. Available: {available}") | |
| return cls() | |
| def list_plugins() -> list[dict]: | |
| """Return metadata for all registered plugins (for the frontend).""" | |
| out = [] | |
| for pid, cls in sorted(PLUGINS.items()): | |
| ok, msg = cls.is_available() | |
| out.append({ | |
| "id": pid, | |
| "display_name": cls.display_name, | |
| "requires_api_key": cls.requires_api_key, | |
| "is_local": cls.is_local, | |
| "available": ok, | |
| "availability_message": msg, | |
| "supported_languages": cls.supported_languages_hint, | |
| }) | |
| return out | |
| # ββ Abstract base class βββββββββββββββββββββββββββββββββββββββββββββ | |
| class TTSPlugin(ABC): | |
| """Base class for all TTS engine plugins. | |
| Subclass this and implement the abstract methods to add support for | |
| a new TTS engine (cloud API or local model). | |
| """ | |
| #: Unique identifier (lowercase, no spaces). Used in API requests. | |
| id: str = "" | |
| #: Human-readable name for the UI. | |
| display_name: str = "Unnamed Plugin" | |
| #: Whether this engine needs an API key (cloud providers). | |
| requires_api_key: bool = False | |
| #: Whether this engine runs locally (no network calls). | |
| is_local: bool = False | |
| #: Hint for the UI β list of commonly supported languages. | |
| supported_languages_hint: list[str] = ["en"] | |
| def is_available(cls) -> tuple[bool, str]: | |
| """Check if the engine can run in the current environment. | |
| Returns: | |
| (True, "Ready") if available. | |
| (False, "pip install ...") with actionable fix instructions. | |
| """ | |
| def generate( | |
| self, | |
| text: str, | |
| *, | |
| voice_id: Optional[str] = None, | |
| language: Optional[str] = None, | |
| speed: float = 1.0, | |
| **kwargs, | |
| ) -> bytes: | |
| """Generate speech from text. | |
| Args: | |
| text: The text to synthesize. | |
| voice_id: Provider-specific voice identifier. | |
| language: ISO 639 language code. | |
| speed: Speech speed multiplier. | |
| Returns: | |
| Raw audio bytes (WAV or MP3, depending on provider). | |
| """ | |
| def list_voices(self) -> list[dict]: | |
| """Return available voices for this engine. | |
| Returns: | |
| List of dicts with at least: {"id": str, "name": str, "language": str} | |
| """ | |
| def get_sample_rate(self) -> int: | |
| """Output sample rate. Override if not 24000.""" | |
| return 24000 | |
| # ββ Built-in plugin: ElevenLabs (example) ββββββββββββββββββββββββββββ | |
| class ElevenLabsPlugin(TTSPlugin): | |
| """ElevenLabs cloud TTS β high-quality voice synthesis. | |
| Requires: ELEVENLABS_API_KEY environment variable. | |
| Install: pip install elevenlabs | |
| """ | |
| id = "elevenlabs" | |
| display_name = "ElevenLabs" | |
| requires_api_key = True | |
| is_local = False | |
| supported_languages_hint = [ | |
| "en", "es", "fr", "de", "it", "pt", "pl", "hi", "ar", "zh", | |
| "ja", "ko", "nl", "tr", "ru", "sv", "id", "fil", "ms", "ro", | |
| "uk", "el", "cs", "da", "fi", "bg", "hr", "sk", "ta", | |
| ] | |
| def is_available(cls) -> tuple[bool, str]: | |
| import os | |
| if not os.environ.get("ELEVENLABS_API_KEY"): | |
| return False, "Set ELEVENLABS_API_KEY environment variable." | |
| try: | |
| import elevenlabs # noqa: F401 | |
| return True, "Ready" | |
| except ImportError: | |
| return False, "pip install elevenlabs" | |
| def generate(self, text, *, voice_id=None, language=None, speed=1.0, **kw) -> bytes: | |
| import os | |
| from elevenlabs import ElevenLabs | |
| client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"]) | |
| audio_iter = client.text_to_speech.convert( | |
| text=text, | |
| voice_id=voice_id or "JBFqnCBsd6RMkjVDRZzb", # George default | |
| model_id="eleven_multilingual_v2", | |
| output_format="mp3_44100_128", | |
| ) | |
| return b"".join(audio_iter) | |
| def list_voices(self) -> list[dict]: | |
| import os | |
| try: | |
| from elevenlabs import ElevenLabs | |
| client = ElevenLabs(api_key=os.environ.get("ELEVENLABS_API_KEY", "")) | |
| voices = client.voices.get_all() | |
| return [ | |
| {"id": v.voice_id, "name": v.name, "language": "multi"} | |
| for v in voices.voices | |
| ] | |
| except Exception as e: | |
| logger.warning("ElevenLabs list_voices failed: %s", e) | |
| return [] | |
| def get_sample_rate(self) -> int: | |
| return 44100 | |
| # ββ Built-in plugin: Bark (local) ββββββββββββββββββββββββββββββββββββ | |
| class BarkPlugin(TTSPlugin): | |
| """Suno Bark β open-source local TTS with music/effects support. | |
| Install: pip install suno-bark | |
| """ | |
| id = "bark" | |
| display_name = "Bark (Suno)" | |
| requires_api_key = False | |
| is_local = True | |
| supported_languages_hint = ["en", "es", "fr", "de", "it", "pt", "ru", "zh", "ja", "ko"] | |
| def is_available(cls) -> tuple[bool, str]: | |
| try: | |
| from bark import SAMPLE_RATE # noqa: F401 | |
| return True, "Ready" | |
| except ImportError: | |
| return False, "pip install suno-bark" | |
| def generate(self, text, *, voice_id=None, language=None, speed=1.0, **kw) -> bytes: | |
| import io | |
| import numpy as np | |
| from bark import generate_audio, SAMPLE_RATE | |
| import scipy.io.wavfile | |
| speaker = voice_id or "v2/en_speaker_6" | |
| audio_array = generate_audio(text, history_prompt=speaker) | |
| buf = io.BytesIO() | |
| scipy.io.wavfile.write(buf, SAMPLE_RATE, (audio_array * 32767).astype(np.int16)) | |
| return buf.getvalue() | |
| def list_voices(self) -> list[dict]: | |
| return [ | |
| {"id": f"v2/en_speaker_{i}", "name": f"English Speaker {i}", "language": "en"} | |
| for i in range(10) | |
| ] | |
| def get_sample_rate(self) -> int: | |
| return 24000 | |
| # ββ Auto-discover plugins from backend/plugins/ directory ββββββββββββ | |
| def discover_plugins(): | |
| """Import all .py files in backend/plugins/ to trigger @register_plugin.""" | |
| import importlib | |
| import pathlib | |
| plugins_dir = pathlib.Path(__file__).parent.parent / "plugins" | |
| if not plugins_dir.exists(): | |
| return | |
| for path in plugins_dir.glob("*.py"): | |
| if path.name.startswith("_"): | |
| continue | |
| module_name = f"plugins.{path.stem}" | |
| try: | |
| importlib.import_module(module_name) | |
| logger.info("Loaded plugin module: %s", module_name) | |
| except Exception as e: | |
| logger.warning("Failed to load plugin %s: %s", module_name, e) | |
| # Run discovery on import | |
| discover_plugins() | |