mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f72624f8 | ||
|
|
e7a88a513a | ||
|
|
2277f16d0a | ||
|
|
1d50429b87 | ||
|
|
29681a5fbb | ||
|
|
50fa2e5b9e | ||
|
|
5816feb6da |
@@ -1,7 +1,8 @@
|
|||||||
name: pip install
|
name: pip install
|
||||||
run-name: pip install
|
run-name: pip install
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- '**.py'
|
- '**.py'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
@@ -15,18 +16,18 @@ jobs:
|
|||||||
install-and-run:
|
install-and-run:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-14, windows-latest]
|
||||||
python-version: ['3.12']
|
python-version: ['3.12']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
continue-on-error: true
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: pip
|
||||||
- name: Install from repository
|
- name: Install from repository
|
||||||
run: python -m pip install .
|
run: python -m pip install .
|
||||||
#- name: Run abogen
|
#- name: Run abogen
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
name: Build multi-arch Docker Image
|
name: Build multi-arch Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Build and push
|
# Build and push
|
||||||
#release:
|
#release:
|
||||||
# types: [published]
|
# types: [published]
|
||||||
# Build only
|
# Build only
|
||||||
#push: it
|
#push: it
|
||||||
# branches: [main]
|
# branches: [main]
|
||||||
# TODO - enable build on pull requests if build times can be reduced
|
# TODO - enable build on pull requests if build times can be reduced
|
||||||
# pull_request:
|
# pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_REPOSITORY: ghcr.io/denizsafak/abogen
|
IMAGE_REPOSITORY: ghcr.io/denizsafak/abogen
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Login to Github Container Registry
|
- name: Login to Github Container Registry
|
||||||
# Only if we need to push an image
|
# Only if we need to push an image
|
||||||
# if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
# if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Setup for buildx
|
# Setup for buildx
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
# Debugging information
|
# Debugging information
|
||||||
- name: Docker info
|
- name: Docker info
|
||||||
run: docker info
|
run: docker info
|
||||||
|
|
||||||
- name: Buildx inspect
|
- name: Buildx inspect
|
||||||
run: docker buildx inspect
|
run: docker buildx inspect
|
||||||
|
|
||||||
# Build and (optionally) push the image
|
# Build and (optionally) push the image
|
||||||
- name: Build image
|
- name: Build image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: ./abogen
|
context: ./abogen
|
||||||
file: ./abogen/Dockerfile
|
file: ./abogen/Dockerfile
|
||||||
# platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
# platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
# platforms: linux/amd64,linux/arm64
|
# platforms: linux/amd64,linux/arm64
|
||||||
platforms: linux/amd64 # using the solution mentioned in https://github.com/denizsafak/abogen/issues/46
|
platforms: linux/amd64 # using the solution mentioned in https://github.com/denizsafak/abogen/issues/46
|
||||||
# Only push if we are publishing a release
|
# Only push if we are publishing a release
|
||||||
# push: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
# push: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||||
push: true
|
push: true
|
||||||
# Use a 'temp' tag, that won't be pushed, for non-release builds
|
# Use a 'temp' tag, that won't be pushed, for non-release builds
|
||||||
tags: ${{ env.IMAGE_REPOSITORY }}:${{ github.event.release.tag_name || 'latest' }}
|
tags: ${{ env.IMAGE_REPOSITORY }}:${{ github.event.release.tag_name || 'latest' }}
|
||||||
# Use a cache to reduce build times
|
# Use a cache to reduce build times
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ class TTSBackendMetadata:
|
|||||||
id: Unique backend identifier (e.g. ``"kokoro"``, ``"supertonic"``).
|
id: Unique backend identifier (e.g. ``"kokoro"``, ``"supertonic"``).
|
||||||
name: Human-readable display name.
|
name: Human-readable display name.
|
||||||
description: Short description of the backend.
|
description: Short description of the backend.
|
||||||
|
voices: Tuple of supported voice identifiers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
voices: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
class TTSBackend(Protocol):
|
class TTSBackend(Protocol):
|
||||||
|
|||||||
@@ -66,6 +66,25 @@ def register_backend(
|
|||||||
_registry.register(metadata, factory)
|
_registry.register(metadata, factory)
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata(backend_id: str) -> TTSBackendMetadata:
|
||||||
|
"""Get metadata for a specific backend by id.
|
||||||
|
|
||||||
|
Ensures all backends are registered by importing the tts_backends
|
||||||
|
package on first access.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If backend with given id is not registered.
|
||||||
|
"""
|
||||||
|
import abogen.tts_backends # noqa: F401 — triggers backend registration
|
||||||
|
return _registry.get_metadata(backend_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_voice(backend_id: str, fallback: str = "") -> str:
|
||||||
|
"""Return the first voice of a backend, or *fallback* if none."""
|
||||||
|
voices = get_metadata(backend_id).voices
|
||||||
|
return voices[0] if voices else fallback
|
||||||
|
|
||||||
|
|
||||||
def create_backend(backend_id: str, **kwargs: Any) -> TTSBackend:
|
def create_backend(backend_id: str, **kwargs: Any) -> TTSBackend:
|
||||||
"""Create a TTS backend instance by provider id."""
|
"""Create a TTS backend instance by provider id."""
|
||||||
return _registry.create_backend(backend_id, **kwargs)
|
return _registry.create_backend(backend_id, **kwargs)
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ from typing import Any, Dict, Iterator, List, Optional
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from abogen.constants import VOICES_INTERNAL
|
||||||
|
from abogen.tts_backend import TTSBackendMetadata
|
||||||
|
|
||||||
|
_KOKORO_METADATA = TTSBackendMetadata(
|
||||||
|
id="kokoro",
|
||||||
|
name="Kokoro",
|
||||||
|
description="Kokoro TTS engine",
|
||||||
|
voices=tuple(VOICES_INTERNAL),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_kpipeline():
|
def _load_kpipeline():
|
||||||
"""Lazy-load Kokoro dependencies."""
|
"""Lazy-load Kokoro dependencies."""
|
||||||
@@ -39,14 +49,8 @@ class KokoroBackend:
|
|||||||
self._lang_code = lang_code
|
self._lang_code = lang_code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self):
|
def metadata(self) -> TTSBackendMetadata:
|
||||||
from abogen.tts_backend import TTSBackendMetadata
|
return _KOKORO_METADATA
|
||||||
|
|
||||||
return TTSBackendMetadata(
|
|
||||||
id="kokoro",
|
|
||||||
name="Kokoro",
|
|
||||||
description="Kokoro TTS engine",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __call__(
|
def __call__(
|
||||||
self,
|
self,
|
||||||
@@ -89,9 +93,7 @@ class KokoroBackend:
|
|||||||
|
|
||||||
def get_available_voices(self) -> List[str]:
|
def get_available_voices(self) -> List[str]:
|
||||||
"""Return known Kokoro voice identifiers."""
|
"""Return known Kokoro voice identifiers."""
|
||||||
from abogen.constants import VOICES_INTERNAL
|
return list(self.metadata.voices)
|
||||||
|
|
||||||
return list(VOICES_INTERNAL)
|
|
||||||
|
|
||||||
def get_supported_formats(self) -> List[str]:
|
def get_supported_formats(self) -> List[str]:
|
||||||
"""Kokoro outputs raw PCM float32 audio."""
|
"""Kokoro outputs raw PCM float32 audio."""
|
||||||
@@ -111,14 +113,9 @@ def create_kokoro_backend(**kwargs: Any) -> KokoroBackend:
|
|||||||
|
|
||||||
|
|
||||||
# --- Registration ---
|
# --- Registration ---
|
||||||
from abogen.tts_backend import TTSBackendMetadata # noqa: E402
|
|
||||||
from abogen.tts_backend_registry import register_backend # noqa: E402
|
from abogen.tts_backend_registry import register_backend # noqa: E402
|
||||||
|
|
||||||
register_backend(
|
register_backend(
|
||||||
metadata=TTSBackendMetadata(
|
metadata=_KOKORO_METADATA,
|
||||||
id="kokoro",
|
|
||||||
name="Kokoro",
|
|
||||||
description="Kokoro TTS engine",
|
|
||||||
),
|
|
||||||
factory=create_kokoro_backend,
|
factory=create_kokoro_backend,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
DEFAULT_SUPERTONIC_VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
|
DEFAULT_SUPERTONIC_VOICES = ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
|
||||||
|
|
||||||
|
from abogen.tts_backend import TTSBackendMetadata
|
||||||
|
|
||||||
|
_SUPERTONIC_METADATA = TTSBackendMetadata(
|
||||||
|
id="supertonic",
|
||||||
|
name="SuperTonic",
|
||||||
|
description="SuperTonic TTS engine",
|
||||||
|
voices=DEFAULT_SUPERTONIC_VOICES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SupertonicSegment:
|
class SupertonicSegment:
|
||||||
@@ -282,12 +291,8 @@ class SupertonicBackend:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def metadata(self) -> "TTSBackendMetadata":
|
def metadata(self) -> TTSBackendMetadata:
|
||||||
return TTSBackendMetadata(
|
return _SUPERTONIC_METADATA
|
||||||
id="supertonic",
|
|
||||||
name="SuperTonic",
|
|
||||||
description="SuperTonic TTS engine",
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, **kwargs: Any) -> None:
|
def __init__(self, **kwargs: Any) -> None:
|
||||||
self._pipeline = SupertonicPipeline(
|
self._pipeline = SupertonicPipeline(
|
||||||
@@ -333,7 +338,7 @@ class SupertonicBackend:
|
|||||||
|
|
||||||
def get_available_voices(self) -> List[str]:
|
def get_available_voices(self) -> List[str]:
|
||||||
"""Return the list of built-in SuperTonic voice identifiers."""
|
"""Return the list of built-in SuperTonic voice identifiers."""
|
||||||
return list(DEFAULT_SUPERTONIC_VOICES)
|
return list(self.metadata.voices)
|
||||||
|
|
||||||
def get_supported_formats(self) -> List[str]:
|
def get_supported_formats(self) -> List[str]:
|
||||||
return ["wav"]
|
return ["wav"]
|
||||||
@@ -379,14 +384,9 @@ def create_supertonic_backend(**kwargs: Any) -> SupertonicBackend:
|
|||||||
return SupertonicBackend(**kwargs)
|
return SupertonicBackend(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
from abogen.tts_backend import TTSBackendMetadata
|
from abogen.tts_backend_registry import register_backend # noqa: E402
|
||||||
from abogen.tts_backend_registry import register_backend
|
|
||||||
|
|
||||||
register_backend(
|
register_backend(
|
||||||
metadata=TTSBackendMetadata(
|
metadata=_SUPERTONIC_METADATA,
|
||||||
id="supertonic",
|
|
||||||
name="SuperTonic",
|
|
||||||
description="SuperTonic TTS engine",
|
|
||||||
),
|
|
||||||
factory=create_supertonic_backend,
|
factory=create_supertonic_backend,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ if LocalEntryNotFoundError is None: # pragma: no cover - fallback for tests
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
from abogen.constants import VOICES_INTERNAL
|
from abogen.tts_backend_registry import get_metadata
|
||||||
|
|
||||||
_CACHE_LOCK = threading.Lock()
|
_CACHE_LOCK = threading.Lock()
|
||||||
_CACHED_VOICES: Set[str] = set()
|
_CACHED_VOICES: Set[str] = set()
|
||||||
@@ -26,8 +26,9 @@ _BOOTSTRAPPED = False
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_targets(voices: Optional[Iterable[str]]) -> Set[str]:
|
def _normalize_targets(voices: Optional[Iterable[str]]) -> Set[str]:
|
||||||
|
kokoro_voices = get_metadata("kokoro").voices
|
||||||
if not voices:
|
if not voices:
|
||||||
return set(VOICES_INTERNAL)
|
return set(kokoro_voices)
|
||||||
normalized: Set[str] = set()
|
normalized: Set[str] = set()
|
||||||
for voice in voices:
|
for voice in voices:
|
||||||
if not voice:
|
if not voice:
|
||||||
@@ -35,7 +36,7 @@ def _normalize_targets(voices: Optional[Iterable[str]]) -> Set[str]:
|
|||||||
voice_id = str(voice).strip()
|
voice_id = str(voice).strip()
|
||||||
if not voice_id:
|
if not voice_id:
|
||||||
continue
|
continue
|
||||||
if voice_id in VOICES_INTERNAL:
|
if voice_id in kokoro_voices:
|
||||||
normalized.add(voice_id)
|
normalized.add(voice_id)
|
||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from abogen.constants import VOICES_INTERNAL
|
from abogen.tts_backend_registry import get_metadata
|
||||||
|
|
||||||
|
|
||||||
# Calls parsing and loads the voice to gpu or cpu
|
# Calls parsing and loads the voice to gpu or cpu
|
||||||
@@ -22,6 +22,7 @@ def parse_formula_terms(formula: str) -> List[Tuple[str, float]]:
|
|||||||
raise ValueError("Empty voice formula")
|
raise ValueError("Empty voice formula")
|
||||||
|
|
||||||
terms: List[Tuple[str, float]] = []
|
terms: List[Tuple[str, float]] = []
|
||||||
|
kokoro_voices = get_metadata("kokoro").voices
|
||||||
for segment in formula.split("+"):
|
for segment in formula.split("+"):
|
||||||
part = segment.strip()
|
part = segment.strip()
|
||||||
if not part:
|
if not part:
|
||||||
@@ -30,7 +31,7 @@ def parse_formula_terms(formula: str) -> List[Tuple[str, float]]:
|
|||||||
raise ValueError("Each component must be in the form voice*weight")
|
raise ValueError("Each component must be in the form voice*weight")
|
||||||
voice_name, raw_weight = part.split("*", 1)
|
voice_name, raw_weight = part.split("*", 1)
|
||||||
voice_name = voice_name.strip()
|
voice_name = voice_name.strip()
|
||||||
if voice_name not in VOICES_INTERNAL:
|
if voice_name not in kokoro_voices:
|
||||||
raise ValueError(f"Unknown voice: {voice_name}")
|
raise ValueError(f"Unknown voice: {voice_name}")
|
||||||
try:
|
try:
|
||||||
weight = float(raw_weight.strip())
|
weight = float(raw_weight.strip())
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from typing import Any, Dict, Iterable, List, Tuple
|
from typing import Any, Dict, Iterable, List, Tuple
|
||||||
|
|
||||||
from abogen.constants import VOICES_INTERNAL
|
from abogen.tts_backend_registry import get_metadata
|
||||||
from abogen.tts_backends.supertonic import DEFAULT_SUPERTONIC_VOICES
|
|
||||||
from abogen.utils import get_user_config_path
|
from abogen.utils import get_user_config_path
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +69,8 @@ def serialize_profiles() -> Dict[str, Dict[str, Iterable[Tuple[str, float]]]]:
|
|||||||
|
|
||||||
def _normalize_supertonic_voice(value: Any) -> str:
|
def _normalize_supertonic_voice(value: Any) -> str:
|
||||||
raw = str(value or "").strip().upper()
|
raw = str(value or "").strip().upper()
|
||||||
return raw if raw in DEFAULT_SUPERTONIC_VOICES else "M1"
|
supertonic_voices = get_metadata("supertonic").voices
|
||||||
|
return raw if raw in supertonic_voices else "M1"
|
||||||
|
|
||||||
|
|
||||||
def _coerce_supertonic_steps(value: Any) -> int:
|
def _coerce_supertonic_steps(value: Any) -> int:
|
||||||
@@ -135,6 +135,7 @@ def normalize_profile_entry(entry: Any) -> Dict[str, Any]:
|
|||||||
|
|
||||||
def _normalize_voice_entries(entries: Iterable) -> List[Tuple[str, float]]:
|
def _normalize_voice_entries(entries: Iterable) -> List[Tuple[str, float]]:
|
||||||
normalized: List[Tuple[str, float]] = []
|
normalized: List[Tuple[str, float]] = []
|
||||||
|
kokoro_voices = get_metadata("kokoro").voices
|
||||||
for item in entries or []:
|
for item in entries or []:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
voice = item.get("id") or item.get("voice")
|
voice = item.get("id") or item.get("voice")
|
||||||
@@ -143,7 +144,7 @@ def _normalize_voice_entries(entries: Iterable) -> List[Tuple[str, float]]:
|
|||||||
voice, weight = item[0], item[1]
|
voice, weight = item[0], item[1]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
if voice not in VOICES_INTERNAL:
|
if voice not in kokoro_voices:
|
||||||
continue
|
continue
|
||||||
if weight is None:
|
if weight is None:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ RUN python3 -m venv "$VIRTUAL_ENV"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml README.md ./
|
COPY pyproject.toml README.md ./
|
||||||
COPY abogen ./abogen
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip \
|
RUN pip install --upgrade pip \
|
||||||
&& if [ -n "$TORCH_VERSION" ]; then \
|
&& if [ -n "$TORCH_VERSION" ]; then \
|
||||||
pip install torch=="$TORCH_VERSION" torchvision=="$TORCH_VERSION" torchaudio=="$TORCH_VERSION" --index-url "$TORCH_INDEX_URL"; \
|
pip install torch=="$TORCH_VERSION" torchvision=="$TORCH_VERSION" torchaudio=="$TORCH_VERSION" --index-url "$TORCH_INDEX_URL"; \
|
||||||
@@ -39,6 +37,8 @@ RUN pip install --upgrade pip \
|
|||||||
https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \
|
https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \
|
||||||
&& pip install --no-cache-dir "mutagen>=1.47.0"
|
&& pip install --no-cache-dir "mutagen>=1.47.0"
|
||||||
|
|
||||||
|
COPY abogen ./abogen
|
||||||
|
|
||||||
# Install onnxruntime-gpu for CUDA acceleration (supertonic uses ONNX Runtime)
|
# Install onnxruntime-gpu for CUDA acceleration (supertonic uses ONNX Runtime)
|
||||||
# Set USE_GPU=false to skip this for CPU-only deployments
|
# Set USE_GPU=false to skip this for CPU-only deployments
|
||||||
RUN if [ "$USE_GPU" = "true" ]; then \
|
RUN if [ "$USE_GPU" = "true" ]; then \
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import numpy as np
|
|||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
import static_ffmpeg
|
import static_ffmpeg
|
||||||
|
|
||||||
from abogen.constants import VOICES_INTERNAL
|
from abogen.tts_backend_registry import get_metadata
|
||||||
from abogen.epub3.exporter import build_epub3_package
|
from abogen.epub3.exporter import build_epub3_package
|
||||||
from abogen.kokoro_text_normalization import ApostropheConfig, normalize_for_pipeline, HAS_NUM2WORDS
|
from abogen.kokoro_text_normalization import ApostropheConfig, normalize_for_pipeline, HAS_NUM2WORDS
|
||||||
from abogen.normalization_settings import (
|
from abogen.normalization_settings import (
|
||||||
@@ -47,7 +47,7 @@ from abogen.voice_formulas import extract_voice_ids, get_new_voice
|
|||||||
from abogen.voice_profiles import load_profiles, normalize_profile_entry
|
from abogen.voice_profiles import load_profiles, normalize_profile_entry
|
||||||
from abogen.pronunciation_store import increment_usage
|
from abogen.pronunciation_store import increment_usage
|
||||||
from abogen.llm_client import LLMClientError
|
from abogen.llm_client import LLMClientError
|
||||||
from abogen.tts_backends.supertonic import DEFAULT_SUPERTONIC_VOICES
|
|
||||||
|
|
||||||
from .service import Job, JobStatus
|
from .service import Job, JobStatus
|
||||||
|
|
||||||
@@ -68,11 +68,11 @@ def _supertonic_voice_from_spec(spec: Any, fallback: str) -> str:
|
|||||||
raw = "M1"
|
raw = "M1"
|
||||||
|
|
||||||
upper = raw.upper()
|
upper = raw.upper()
|
||||||
if upper in DEFAULT_SUPERTONIC_VOICES:
|
if upper in get_metadata("supertonic").voices:
|
||||||
return upper
|
return upper
|
||||||
|
|
||||||
fallback_upper = fallback_raw.upper() if fallback_raw else ""
|
fallback_upper = fallback_raw.upper() if fallback_raw else ""
|
||||||
if fallback_upper in DEFAULT_SUPERTONIC_VOICES:
|
if fallback_upper in get_metadata("supertonic").voices:
|
||||||
return fallback_upper
|
return fallback_upper
|
||||||
|
|
||||||
return "M1"
|
return "M1"
|
||||||
@@ -123,7 +123,7 @@ def _infer_provider_from_spec(value: Any, fallback: str = "kokoro") -> str:
|
|||||||
if not raw:
|
if not raw:
|
||||||
return fallback
|
return fallback
|
||||||
upper = raw.upper()
|
upper = raw.upper()
|
||||||
if upper in DEFAULT_SUPERTONIC_VOICES:
|
if upper in get_metadata("supertonic").voices:
|
||||||
return "supertonic"
|
return "supertonic"
|
||||||
if "*" in raw or "+" in raw:
|
if "*" in raw or "+" in raw:
|
||||||
return "kokoro"
|
return "kokoro"
|
||||||
@@ -576,7 +576,7 @@ def _spec_to_voice_ids(spec: Any) -> Set[str]:
|
|||||||
return set(extract_voice_ids(text))
|
return set(extract_voice_ids(text))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return set()
|
return set()
|
||||||
if text in VOICES_INTERNAL:
|
if text in get_metadata("kokoro").voices:
|
||||||
return {text}
|
return {text}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
@@ -640,7 +640,7 @@ def _collect_required_voice_ids(job: Job) -> Set[str]:
|
|||||||
for key in ("resolved_voice", "voice_formula", "voice"):
|
for key in ("resolved_voice", "voice_formula", "voice"):
|
||||||
voices.update(_spec_to_voice_ids(payload.get(key)))
|
voices.update(_spec_to_voice_ids(payload.get(key)))
|
||||||
|
|
||||||
voices.update(VOICES_INTERNAL)
|
voices.update(get_metadata("kokoro").voices)
|
||||||
return voices
|
return voices
|
||||||
|
|
||||||
|
|
||||||
@@ -1803,8 +1803,8 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
fallback_key = next(iter(voice_cache.keys()), "")
|
fallback_key = next(iter(voice_cache.keys()), "")
|
||||||
if fallback_key and fallback_key != "__custom_mix":
|
if fallback_key and fallback_key != "__custom_mix":
|
||||||
intro_voice_spec = fallback_key.split(":", 1)[-1]
|
intro_voice_spec = fallback_key.split(":", 1)[-1]
|
||||||
if not intro_voice_spec and VOICES_INTERNAL:
|
if not intro_voice_spec:
|
||||||
intro_voice_spec = VOICES_INTERNAL[0]
|
intro_voice_spec = get_default_voice("kokoro")
|
||||||
|
|
||||||
if intro_voice_spec:
|
if intro_voice_spec:
|
||||||
intro_provider, _, intro_voice_choice, intro_speed, intro_steps = resolve_voice_choice(
|
intro_provider, _, intro_voice_choice, intro_speed, intro_steps = resolve_voice_choice(
|
||||||
@@ -2237,8 +2237,8 @@ def run_conversion_job(job: Job) -> None:
|
|||||||
if fallback_key and fallback_key != "__custom_mix":
|
if fallback_key and fallback_key != "__custom_mix":
|
||||||
# `voice_cache` keys are internal and include provider prefixes.
|
# `voice_cache` keys are internal and include provider prefixes.
|
||||||
outro_voice_spec = fallback_key.split(":", 1)[-1]
|
outro_voice_spec = fallback_key.split(":", 1)[-1]
|
||||||
if not outro_voice_spec and VOICES_INTERNAL:
|
if not outro_voice_spec:
|
||||||
outro_voice_spec = VOICES_INTERNAL[0]
|
outro_voice_spec = get_default_voice("kokoro")
|
||||||
|
|
||||||
if outro_text and outro_voice_spec:
|
if outro_text and outro_voice_spec:
|
||||||
outro_start_time = current_time
|
outro_start_time = current_time
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from abogen.webui.routes.utils.common import split_profile_spec
|
|||||||
from abogen.utils import calculate_text_length
|
from abogen.utils import calculate_text_length
|
||||||
from abogen.voice_profiles import serialize_profiles, normalize_profile_entry
|
from abogen.voice_profiles import serialize_profiles, normalize_profile_entry
|
||||||
from abogen.chunking import ChunkLevel, build_chunks_for_chapters
|
from abogen.chunking import ChunkLevel, build_chunks_for_chapters
|
||||||
from abogen.constants import VOICES_INTERNAL
|
from abogen.tts_backend_registry import get_default_voice
|
||||||
from abogen.speaker_configs import get_config
|
from abogen.speaker_configs import get_config
|
||||||
from abogen.kokoro_text_normalization import normalize_roman_numeral_titles
|
from abogen.kokoro_text_normalization import normalize_roman_numeral_titles
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -616,8 +616,8 @@ def apply_book_step_form(
|
|||||||
custom_formula = ""
|
custom_formula = ""
|
||||||
|
|
||||||
base_voice_spec = resolved_default_voice or narrator_voice_raw
|
base_voice_spec = resolved_default_voice or narrator_voice_raw
|
||||||
if not base_voice_spec and VOICES_INTERNAL:
|
if not base_voice_spec:
|
||||||
base_voice_spec = VOICES_INTERNAL[0]
|
base_voice_spec = get_default_voice("kokoro")
|
||||||
|
|
||||||
voice_choice, resolved_language, selected_profile = resolve_voice_choice(
|
voice_choice, resolved_language, selected_profile = resolve_voice_choice(
|
||||||
pending.language,
|
pending.language,
|
||||||
@@ -796,8 +796,8 @@ def build_pending_job_from_extraction(
|
|||||||
profile_selection = inferred_profile
|
profile_selection = inferred_profile
|
||||||
|
|
||||||
base_voice = base_voice_input or resolved_default_voice or str(default_voice_setting).strip()
|
base_voice = base_voice_input or resolved_default_voice or str(default_voice_setting).strip()
|
||||||
if not base_voice and VOICES_INTERNAL:
|
if not base_voice:
|
||||||
base_voice = VOICES_INTERNAL[0]
|
base_voice = get_default_voice("kokoro")
|
||||||
selected_speaker_config = (form.get("speaker_config") or "").strip()
|
selected_speaker_config = (form.get("speaker_config") or "").strip()
|
||||||
speaker_config_payload = get_config(selected_speaker_config) if selected_speaker_config else None
|
speaker_config_payload = get_config(selected_speaker_config) if selected_speaker_config else None
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from abogen.constants import (
|
|||||||
LANGUAGE_DESCRIPTIONS,
|
LANGUAGE_DESCRIPTIONS,
|
||||||
SUBTITLE_FORMATS,
|
SUBTITLE_FORMATS,
|
||||||
SUPPORTED_SOUND_FORMATS,
|
SUPPORTED_SOUND_FORMATS,
|
||||||
VOICES_INTERNAL,
|
|
||||||
)
|
)
|
||||||
|
from abogen.tts_backend_registry import get_default_voice
|
||||||
from abogen.normalization_settings import (
|
from abogen.normalization_settings import (
|
||||||
DEFAULT_LLM_PROMPT,
|
DEFAULT_LLM_PROMPT,
|
||||||
environment_llm_defaults,
|
environment_llm_defaults,
|
||||||
@@ -174,7 +174,7 @@ def settings_defaults() -> Dict[str, Any]:
|
|||||||
"subtitle_format": "srt",
|
"subtitle_format": "srt",
|
||||||
"save_mode": "default_output" if has_output_override() else "save_next_to_input",
|
"save_mode": "default_output" if has_output_override() else "save_next_to_input",
|
||||||
"default_speaker": "",
|
"default_speaker": "",
|
||||||
"default_voice": VOICES_INTERNAL[0] if VOICES_INTERNAL else "",
|
"default_voice": get_default_voice("kokoro"),
|
||||||
"supertonic_total_steps": 5,
|
"supertonic_total_steps": 5,
|
||||||
"supertonic_speed": 1.0,
|
"supertonic_speed": 1.0,
|
||||||
"replace_single_newlines": False,
|
"replace_single_newlines": False,
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from abogen.constants import (
|
|||||||
SUPPORTED_SOUND_FORMATS,
|
SUPPORTED_SOUND_FORMATS,
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
SAMPLE_VOICE_TEXTS,
|
SAMPLE_VOICE_TEXTS,
|
||||||
VOICES_INTERNAL,
|
|
||||||
)
|
)
|
||||||
|
from abogen.tts_backend_registry import get_metadata
|
||||||
from abogen.speaker_configs import list_configs
|
from abogen.speaker_configs import list_configs
|
||||||
from abogen.tts_backend_registry import create_backend
|
from abogen.tts_backend_registry import create_backend
|
||||||
from abogen.webui.conversion_runner import _select_device, _to_float32, SAMPLE_RATE, SPLIT_PATTERN
|
from abogen.webui.conversion_runner import _select_device, _to_float32, SAMPLE_RATE, SPLIT_PATTERN
|
||||||
@@ -285,7 +285,7 @@ def filter_voice_catalog(
|
|||||||
def build_voice_catalog() -> List[Dict[str, str]]:
|
def build_voice_catalog() -> List[Dict[str, str]]:
|
||||||
catalog: List[Dict[str, str]] = []
|
catalog: List[Dict[str, str]] = []
|
||||||
gender_map = {"f": "Female", "m": "Male"}
|
gender_map = {"f": "Female", "m": "Male"}
|
||||||
for voice_id in VOICES_INTERNAL:
|
for voice_id in get_metadata("kokoro").voices:
|
||||||
prefix, _, rest = voice_id.partition("_")
|
prefix, _, rest = voice_id.partition("_")
|
||||||
language_code = prefix[0] if prefix else "a"
|
language_code = prefix[0] if prefix else "a"
|
||||||
gender_code = prefix[1] if len(prefix) > 1 else ""
|
gender_code = prefix[1] if len(prefix) > 1 else ""
|
||||||
@@ -590,7 +590,7 @@ def template_options() -> Dict[str, Any]:
|
|||||||
voice_catalog = build_voice_catalog()
|
voice_catalog = build_voice_catalog()
|
||||||
return {
|
return {
|
||||||
"languages": LANGUAGE_DESCRIPTIONS,
|
"languages": LANGUAGE_DESCRIPTIONS,
|
||||||
"voices": VOICES_INTERNAL,
|
"voices": get_metadata("kokoro").voices,
|
||||||
"subtitle_formats": SUBTITLE_FORMATS,
|
"subtitle_formats": SUBTITLE_FORMATS,
|
||||||
"supported_langs_for_subs": SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
"supported_langs_for_subs": SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
"output_formats": SUPPORTED_SOUND_FORMATS,
|
"output_formats": SUPPORTED_SOUND_FORMATS,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from abogen.speaker_configs import (
|
|||||||
save_configs,
|
save_configs,
|
||||||
delete_config,
|
delete_config,
|
||||||
)
|
)
|
||||||
from abogen.constants import VOICES_INTERNAL
|
|
||||||
|
|
||||||
voices_bp = Blueprint("voices", __name__)
|
voices_bp = Blueprint("voices", __name__)
|
||||||
|
|
||||||
|
|||||||
+204
-149
@@ -1,149 +1,204 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from abogen.tts_backend import TTSBackendMetadata
|
from abogen.tts_backend import TTSBackendMetadata
|
||||||
from abogen.tts_backend_registry import TTSBackendRegistry
|
from abogen.tts_backend_registry import TTSBackendRegistry
|
||||||
|
|
||||||
|
|
||||||
class TestTTSBackendMetadata:
|
class TestTTSBackendMetadata:
|
||||||
def test_is_frozen_dataclass(self):
|
def test_is_frozen_dataclass(self):
|
||||||
assert dataclass(TTSBackendMetadata)
|
assert dataclass(TTSBackendMetadata)
|
||||||
|
|
||||||
def test_fields_are_present(self):
|
def test_fields_are_present(self):
|
||||||
meta = TTSBackendMetadata(
|
meta = TTSBackendMetadata(
|
||||||
id="test",
|
id="test",
|
||||||
name="Test Backend",
|
name="Test Backend",
|
||||||
description="A test backend",
|
description="A test backend",
|
||||||
)
|
)
|
||||||
assert meta.id == "test"
|
assert meta.id == "test"
|
||||||
assert meta.name == "Test Backend"
|
assert meta.name == "Test Backend"
|
||||||
assert meta.description == "A test backend"
|
assert meta.description == "A test backend"
|
||||||
|
|
||||||
def test_is_immutable(self):
|
def test_voices_field_default_empty(self):
|
||||||
import pytest
|
meta = TTSBackendMetadata(
|
||||||
|
id="test",
|
||||||
meta = TTSBackendMetadata(
|
name="Test",
|
||||||
id="kokoro",
|
description="Test backend",
|
||||||
name="Kokoro",
|
)
|
||||||
description="Test",
|
assert meta.voices == ()
|
||||||
)
|
|
||||||
with pytest.raises(Exception):
|
def test_voices_field_stored(self):
|
||||||
meta.id = "changed"
|
meta = TTSBackendMetadata(
|
||||||
|
id="test",
|
||||||
|
name="Test",
|
||||||
class TestTTSBackendRegistry:
|
description="Test backend",
|
||||||
def test_register_and_list(self):
|
voices=("v1", "v2"),
|
||||||
registry = TTSBackendRegistry()
|
)
|
||||||
meta = TTSBackendMetadata(id="a", name="A", description="Backend A")
|
assert meta.voices == ("v1", "v2")
|
||||||
registry.register(metadata=meta, factory=lambda: None)
|
|
||||||
|
def test_is_immutable(self):
|
||||||
backends = registry.list_backends()
|
import pytest
|
||||||
assert len(backends) == 1
|
|
||||||
assert backends[0].id == "a"
|
meta = TTSBackendMetadata(
|
||||||
|
id="kokoro",
|
||||||
def test_list_multiple(self):
|
name="Kokoro",
|
||||||
registry = TTSBackendRegistry()
|
description="Test",
|
||||||
meta_a = TTSBackendMetadata(id="a", name="A", description="A")
|
)
|
||||||
meta_b = TTSBackendMetadata(id="b", name="B", description="B")
|
with pytest.raises(Exception):
|
||||||
registry.register(metadata=meta_a, factory=lambda: None)
|
meta.id = "changed"
|
||||||
registry.register(metadata=meta_b, factory=lambda: None)
|
|
||||||
|
|
||||||
backends = registry.list_backends()
|
class TestTTSBackendRegistry:
|
||||||
ids = [b.id for b in backends]
|
def test_register_and_list(self):
|
||||||
assert "a" in ids
|
registry = TTSBackendRegistry()
|
||||||
assert "b" in ids
|
meta = TTSBackendMetadata(id="a", name="A", description="Backend A")
|
||||||
|
registry.register(metadata=meta, factory=lambda: None)
|
||||||
def test_get_metadata(self):
|
|
||||||
registry = TTSBackendRegistry()
|
backends = registry.list_backends()
|
||||||
meta = TTSBackendMetadata(id="x", name="X", description="X backend")
|
assert len(backends) == 1
|
||||||
registry.register(metadata=meta, factory=lambda: None)
|
assert backends[0].id == "a"
|
||||||
|
|
||||||
result = registry.get_metadata("x")
|
def test_list_multiple(self):
|
||||||
assert result.id == "x"
|
registry = TTSBackendRegistry()
|
||||||
assert result.name == "X"
|
meta_a = TTSBackendMetadata(id="a", name="A", description="A")
|
||||||
|
meta_b = TTSBackendMetadata(id="b", name="B", description="B")
|
||||||
def test_get_metadata_unknown_raises(self):
|
registry.register(metadata=meta_a, factory=lambda: None)
|
||||||
import pytest
|
registry.register(metadata=meta_b, factory=lambda: None)
|
||||||
|
|
||||||
registry = TTSBackendRegistry()
|
backends = registry.list_backends()
|
||||||
with pytest.raises(KeyError, match="Unknown backend: nope"):
|
ids = [b.id for b in backends]
|
||||||
registry.get_metadata("nope")
|
assert "a" in ids
|
||||||
|
assert "b" in ids
|
||||||
def test_create_backend(self):
|
|
||||||
registry = TTSBackendRegistry()
|
def test_get_metadata(self):
|
||||||
meta = TTSBackendMetadata(id="test", name="Test", description="Test backend")
|
registry = TTSBackendRegistry()
|
||||||
|
meta = TTSBackendMetadata(id="x", name="X", description="X backend")
|
||||||
def factory(**kwargs):
|
registry.register(metadata=meta, factory=lambda: None)
|
||||||
return {"created": True, "kwargs": kwargs}
|
|
||||||
|
result = registry.get_metadata("x")
|
||||||
registry.register(metadata=meta, factory=factory)
|
assert result.id == "x"
|
||||||
result = registry.create_backend("test", foo="bar")
|
assert result.name == "X"
|
||||||
|
|
||||||
assert result == {"created": True, "kwargs": {"foo": "bar"}}
|
def test_get_metadata_unknown_raises(self):
|
||||||
|
import pytest
|
||||||
def test_create_backend_unknown_raises(self):
|
|
||||||
import pytest
|
registry = TTSBackendRegistry()
|
||||||
|
with pytest.raises(KeyError, match="Unknown backend: nope"):
|
||||||
registry = TTSBackendRegistry()
|
registry.get_metadata("nope")
|
||||||
with pytest.raises(KeyError, match="Unknown backend: missing"):
|
|
||||||
registry.create_backend("missing")
|
def test_create_backend(self):
|
||||||
|
registry = TTSBackendRegistry()
|
||||||
def test_register_overwrites(self):
|
meta = TTSBackendMetadata(id="test", name="Test", description="Test backend")
|
||||||
registry = TTSBackendRegistry()
|
|
||||||
meta1 = TTSBackendMetadata(id="x", name="V1", description="First")
|
def factory(**kwargs):
|
||||||
meta2 = TTSBackendMetadata(id="x", name="V2", description="Second")
|
return {"created": True, "kwargs": kwargs}
|
||||||
registry.register(metadata=meta1, factory=lambda: "v1")
|
|
||||||
registry.register(metadata=meta2, factory=lambda: "v2")
|
registry.register(metadata=meta, factory=factory)
|
||||||
|
result = registry.create_backend("test", foo="bar")
|
||||||
result = registry.get_metadata("x")
|
|
||||||
assert result.name == "V2"
|
assert result == {"created": True, "kwargs": {"foo": "bar"}}
|
||||||
assert registry.create_backend("x") == "v2"
|
|
||||||
|
def test_create_backend_unknown_raises(self):
|
||||||
|
import pytest
|
||||||
class TestBackendRegistration:
|
|
||||||
"""Tests that existing backends are auto-registered."""
|
registry = TTSBackendRegistry()
|
||||||
|
with pytest.raises(KeyError, match="Unknown backend: missing"):
|
||||||
def test_import_triggers_registration(self):
|
registry.create_backend("missing")
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
def test_register_overwrites(self):
|
||||||
from abogen.tts_backend_registry import _registry
|
registry = TTSBackendRegistry()
|
||||||
|
meta1 = TTSBackendMetadata(id="x", name="V1", description="First")
|
||||||
backends = _registry.list_backends()
|
meta2 = TTSBackendMetadata(id="x", name="V2", description="Second")
|
||||||
ids = [b.id for b in backends]
|
registry.register(metadata=meta1, factory=lambda: "v1")
|
||||||
assert "kokoro" in ids
|
registry.register(metadata=meta2, factory=lambda: "v2")
|
||||||
assert "supertonic" in ids
|
|
||||||
|
result = registry.get_metadata("x")
|
||||||
def test_kokoro_metadata(self):
|
assert result.name == "V2"
|
||||||
import abogen.tts_backends # noqa: F401
|
assert registry.create_backend("x") == "v2"
|
||||||
|
|
||||||
from abogen.tts_backend_registry import _registry
|
|
||||||
|
class TestBackendRegistration:
|
||||||
meta = _registry.get_metadata("kokoro")
|
"""Tests that existing backends are auto-registered."""
|
||||||
assert meta.id == "kokoro"
|
|
||||||
assert meta.name == "Kokoro"
|
def test_import_triggers_registration(self):
|
||||||
assert "Kokoro" in meta.description
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
def test_supertonic_metadata(self):
|
from abogen.tts_backend_registry import _registry
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
backends = _registry.list_backends()
|
||||||
from abogen.tts_backend_registry import _registry
|
ids = [b.id for b in backends]
|
||||||
|
assert "kokoro" in ids
|
||||||
meta = _registry.get_metadata("supertonic")
|
assert "supertonic" in ids
|
||||||
assert meta.id == "supertonic"
|
|
||||||
assert meta.name == "SuperTonic"
|
def test_kokoro_metadata(self):
|
||||||
assert "SuperTonic" in meta.description
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
def test_kokoro_factory_callable(self):
|
from abogen.tts_backend_registry import _registry
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
meta = _registry.get_metadata("kokoro")
|
||||||
from abogen.tts_backend_registry import _registry
|
assert meta.id == "kokoro"
|
||||||
|
assert meta.name == "Kokoro"
|
||||||
factory = _registry._factories["kokoro"]
|
assert "Kokoro" in meta.description
|
||||||
assert callable(factory)
|
|
||||||
|
def test_supertonic_metadata(self):
|
||||||
def test_supertonic_factory_callable(self):
|
import abogen.tts_backends # noqa: F401
|
||||||
import abogen.tts_backends # noqa: F401
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
from abogen.tts_backend_registry import _registry
|
|
||||||
|
meta = _registry.get_metadata("supertonic")
|
||||||
factory = _registry._factories["supertonic"]
|
assert meta.id == "supertonic"
|
||||||
assert callable(factory)
|
assert meta.name == "SuperTonic"
|
||||||
|
assert "SuperTonic" in meta.description
|
||||||
|
|
||||||
|
def test_kokoro_metadata_has_voices(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
meta = _registry.get_metadata("kokoro")
|
||||||
|
assert isinstance(meta.voices, tuple)
|
||||||
|
assert len(meta.voices) > 0
|
||||||
|
assert all(isinstance(v, str) for v in meta.voices)
|
||||||
|
|
||||||
|
def test_supertonic_metadata_has_voices(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
meta = _registry.get_metadata("supertonic")
|
||||||
|
assert isinstance(meta.voices, tuple)
|
||||||
|
assert len(meta.voices) == 10
|
||||||
|
assert meta.voices == ("M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5")
|
||||||
|
|
||||||
|
def test_kokoro_factory_callable(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
factory = _registry._factories["kokoro"]
|
||||||
|
assert callable(factory)
|
||||||
|
|
||||||
|
def test_supertonic_factory_callable(self):
|
||||||
|
import abogen.tts_backends # noqa: F401
|
||||||
|
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
factory = _registry._factories["supertonic"]
|
||||||
|
assert callable(factory)
|
||||||
|
|
||||||
|
def test_kokoro_metadata_voices_match_registry(self):
|
||||||
|
"""Ensure the metadata property on the instance shares voices with registry."""
|
||||||
|
from abogen.tts_backends.kokoro import _KOKORO_METADATA
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
registry_meta = _registry.get_metadata("kokoro")
|
||||||
|
assert _KOKORO_METADATA is registry_meta
|
||||||
|
assert _KOKORO_METADATA.voices == registry_meta.voices
|
||||||
|
|
||||||
|
def test_supertonic_metadata_voices_match_registry(self):
|
||||||
|
"""Ensure the metadata property on the instance shares voices with registry."""
|
||||||
|
from abogen.tts_backends.supertonic import _SUPERTONIC_METADATA
|
||||||
|
from abogen.tts_backend_registry import _registry
|
||||||
|
|
||||||
|
registry_meta = _registry.get_metadata("supertonic")
|
||||||
|
assert _SUPERTONIC_METADATA is registry_meta
|
||||||
|
assert _SUPERTONIC_METADATA.voices == registry_meta.voices
|
||||||
|
|||||||
Reference in New Issue
Block a user