From 21cdc65ade29dbf21459d4ba7b4a5ff276146e4b Mon Sep 17 00:00:00 2001 From: stephan Date: Thu, 4 Dec 2025 11:58:36 +0100 Subject: [PATCH] first commit --- .dockerignore | 34 + .env.example | 7 + .gitignore | 52 ++ Dockerfile | 42 ++ Makefile | 150 +++++ README.md | 222 +++++++ app/__init__.py | 0 app/main.py | 204 ++++++ config.py | 25 + docker-compose.yml | 44 ++ engines/chattts.py | 48 ++ engines/engine_base.py | 45 ++ engines/f5-tts-voices/default.txt | 1 + engines/f5_tts.py | 143 +++++ engines/piper.py | 163 +++++ engines/styletts.py | 48 ++ guide.md | 296 +++++++++ .../de_DE-thorsten-high.onnx.json | 493 ++++++++++++++ .../de_DE-thorsten_emotional-medium.onnx.json | 502 +++++++++++++++ .../en_GB-cori-high/en_GB-cori-high.onnx.json | 502 +++++++++++++++ .../en_GB-vctk-medium.onnx.json | 603 ++++++++++++++++++ .../en_US-hfc_female-medium.onnx.json | 508 +++++++++++++++ .../en_US-kristin-medium.onnx.json | 502 +++++++++++++++ .../de_DE-thorsten-high.onnx.json | 493 ++++++++++++++ .../de_DE-thorsten_emotional-medium.onnx.json | 502 +++++++++++++++ .../en_GB-cori-high/en_GB-cori-high.onnx.json | 502 +++++++++++++++ .../en_GB-vctk-medium.onnx.json | 603 ++++++++++++++++++ .../en_US-hfc_female-medium.onnx.json | 508 +++++++++++++++ .../en_US-kristin-medium.onnx.json | 502 +++++++++++++++ requirements.txt | 22 + run.sh | 21 + setup_env.sh | 34 + tests/conftest.py | 34 + tests/test_api.py | 123 ++++ tests/test_f5_tts.py | 70 ++ utils/audio.py | 76 +++ utils/cache.py | 22 + utils/text.py | 30 + 38 files changed, 8176 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 engines/chattts.py create mode 100644 engines/engine_base.py create mode 100644 engines/f5-tts-voices/default.txt create mode 100644 engines/f5_tts.py create mode 100644 engines/piper.py create mode 100644 engines/styletts.py create mode 100644 guide.md create mode 100644 models/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json create mode 100644 models/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json create mode 100644 models/en_GB-cori-high/en_GB-cori-high.onnx.json create mode 100644 models/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json create mode 100644 models/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json create mode 100644 models/en_US-kristin-medium/en_US-kristin-medium.onnx.json create mode 100644 models/piper/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json create mode 100644 models/piper/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json create mode 100644 models/piper/en_GB-cori-high/en_GB-cori-high.onnx.json create mode 100644 models/piper/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json create mode 100644 models/piper/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json create mode 100644 models/piper/en_US-kristin-medium/en_US-kristin-medium.onnx.json create mode 100644 requirements.txt create mode 100755 run.sh create mode 100644 setup_env.sh create mode 100644 tests/conftest.py create mode 100644 tests/test_api.py create mode 100644 tests/test_f5_tts.py create mode 100644 utils/audio.py create mode 100644 utils/cache.py create mode 100644 utils/text.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4cd526a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,34 @@ +# Git / version control +.git +.gitignore +.dockerignore + +# Virtual environment +.venv +venv/ +.env/ + +# IDE / Editor folders +.idea/ +.vscode/ + +# Python cache +__pycache__/ +*.pyc +*.pyo + +# Build artifacts +build/ +dist/ +*.egg-info + +# Test artifacts +.pytest_cache/ +.coverage + +# Docker files +Dockerfile +docker-compose.yml + +# Local environment settings +.env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cced38a --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Comma-separated list of engines to activate. +# Available options (potentially): piper, styletts, f5_tts, chattts +ACTIVE_ENGINES='["piper", "styletts"]' + +# Server configuration +HOST=0.0.0.0 +PORT=8000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85c8c4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# .gitignore für Python-Projekt +# Version: v1.1 +# Erstellt: 2025-07-25 + +# Bytecode & Caches +__pycache__/ +*.py[cod] +*$py.class + +# Virtuelle Umgebungen +venv/ +.env/ +.virtualenv/ +.venv/ + +# IDE/Editor +.vscode/ +.idea/ + +# Test- & Coverage-Dateien +htmlcov/ +.coverage +.coverage.* +.pytest_cache/ +nosetests.xml +coverage.xml + +# Build & Distribution +build/ +dist/ +*.egg-info/ +.eggs/ +pip-wheel-metadata/ + +# Logs & Temp +*.log +*.tmp +*.bak +*.swp +.DS_Store + +# Umgebungsvariablen +.env + +# Sprachmodelle (TTS) +tts_models/ +voice_models/ +*.pt +*.onnx +*.bin +*.safetensors +asset/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ee5ebce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Builder +FROM python:3.11 as builder +WORKDIR /opt/venv + +# Create a virtual environment and install dependencies +COPY requirements.txt . +RUN python -m venv . && . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt + +# Stage 2: Runner (The final image) +FROM python:3.11-slim + +# Install system dependencies needed at runtime +# ffmpeg is required for audio conversion +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg \ + && rm -rf /var/lib/apt/lists/* + +# Create an unprivileged user +RUN useradd --create-home --shell /bin/bash appuser + +# Set working directory for the application code +WORKDIR /home/appuser + +# Copy the virtual environment from the builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copy the application code into the container +COPY app/ ./app + +# Set the PATH to include the virtual environment's bin directory +ENV PATH="/opt/venv/bin:$PATH" + +# Expose the application port +EXPOSE 8000 + +# Run as the unprivileged user +USER appuser + +# Command to run the application using gunicorn for production +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "2", "-b", "0.0.0.0:8000", "app.main:app"] + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..59e4da7 --- /dev/null +++ b/Makefile @@ -0,0 +1,150 @@ +# --- Configuration --- +IMAGE_NAME := audio-engine-hub +TAG := latest +# Default internal application port (exposed to Traefik) +PORT := 8000 +# Traefik's external entrypoints +TRAEFIK_WEB_PORT := 80 +TRAEFIK_DASHBOARD_PORT := 8080 + +# --- Port Checking Functions --- + +# Function to check if required Traefik ports are free +# Exits if any are busy, does not suggest alternatives. +define check_traefik_ports_free + @echo "Checking if Traefik ports ($(TRAEFIK_WEB_PORT}, $(TRAEFIK_DASHBOARD_PORT}) are free..." >&2 + @local busy_ports=""; \ + if ss -tulnp | grep ":$(TRAEFIK_WEB_PORT) " > /dev/null; then \ + busy_ports="$$busy_ports $(TRAEFIK_WEB_PORT)"; \ + fi; \ + if ss -tulnp | grep ":$(TRAEFIK_DASHBOARD_PORT) " > /dev/null; then \ + busy_ports="$$busy_ports $(TRAEFIK_DASHBOARD_PORT)"; \ + fi; \ + if [ -n "$$busy_ports" ]; then \ + echo "ERROR: The following Traefik ports are already in use: $$busy_ports. Please free them or stop Traefik if already running." >&2; \ + exit 1; \ + fi; \ + @echo "Traefik ports are free." >&2 +endef + +# Function to find next free host port for the app and ask user +# This function will echo the chosen port if successful, or exit with an error. +define check_app_port_free + @local start_port=$(PORT); \ + local found_port=$$start_port; \ + local is_free=false; \ + \ + if ! ss -tulnp | grep ":$$start_port " > /dev/null; then \ + echo "$$start_port"; \ + exit 0; \ + fi; \ + \ + echo "Port $$start_port is busy. Searching for a free port for the app..." >&2; \ + while ! $$is_free; do \ + if ! ss -tulnp | grep ":$$found_port " > /dev/null; then \ + is_free=true; \ + else \ + ((found_port++)); \ + if [ "$$found_port" -gt 65535 ]; then \ + echo "ERROR: No free ports found up to 65535. Aborting." >&2; \ + exit 1; \ + fi; \ + fi; \ + done; \ + \ + echo "Port $$start_port is busy. I found port $$found_port to be free for the app." >&2; \ + read -p "Do you want to use port $$found_port for the app? (y/N): " choice; \ + case "$$choice" in \ + y|Y ) \ + echo "$$found_port"; \ + ;; \ + * ) \ + echo "Operation cancelled by user." >&2; \ + exit 1; \ + ;; \ + esac; +endef + +# --- Docker Commands --- + +.PHONY: build +build: + @echo "Building Docker image: $(IMAGE_NAME):$(TAG)" + docker build -t $(IMAGE_NAME):$(TAG) . + +.PHONY: run +run: + @export SELECTED_HOST_PORT=$$(bash -c 'func() { $(check_app_port_free) }; func') && \ + echo "Using host port $$SELECTED_HOST_PORT for single app container" && \ + docker run -d -p $$SELECTED_HOST_PORT:$(PORT) --name $(IMAGE_NAME) $(IMAGE_NAME):$(TAG) + +.PHONY: stop +stop: + @echo "Stopping Docker container: $(IMAGE_NAME)" + docker stop $(IMAGE_NAME) || true + docker rm $(IMAGE_NAME) || true + +.PHONY: logs +logs: + @echo "Showing logs for container: $(IMAGE_NAME)" + docker logs -f $(IMAGE_NAME) + +.PHONY: shell +shell: + @echo "Accessing shell in container: $(IMAGE_NAME)" + docker exec -it $(IMAGE_NAME) /bin/bash + +# --- Docker Compose Commands --- + +.PHONY: up +up: + $(call check_traefik_ports_free) # Check Traefik ports before starting + @echo "Starting development environment with Docker Compose (Traefik enabled)..." + docker-compose up --build -d + +.PHONY: down +down: + @echo "Stopping development environment with Docker Compose..." + docker-compose down + +# --- Image Management --- + +.PHONY: tag +tag: + @echo "Tagging image $(IMAGE_NAME):$(TAG) as $(REGISTRY)/$(IMAGE_NAME):$(TAG)" + docker tag $(IMAGE_NAME):$(TAG) $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +.PHONY: push +push: tag + @echo "Pushing image $(REGISTRY)/$(IMAGE_NAME):$(TAG) to registry..." + docker push $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +.PHONY: test +test: + @echo "Running tests with coverage..." + pytest --cov=. app/ tests/ + +# --- Cleanup --- + +.PHONY: clean +clean: + @echo "Cleaning up stopped containers and dangling images..." + docker container prune -f + docker image prune -f + +.PHONY: help +help: + @echo "Available commands:" + @echo " build - Build the Docker image" + @echo " run - Run the Docker container (single app, no Traefik)" + @echo " stop - Stop and remove the Docker container" + @echo " logs - Follow the logs of the container" + @echo " shell - Get a shell inside the running container" + @echo " up - Start the dev environment with docker-compose (with Traefik)" + @echo " down - Stop the dev environment with docker-compose" + @echo " tag - Tag the image for a registry" + @echo " push - Push the image to a registry (after tagging)" + @echo " clean - Clean up unused containers and images" + @echo " help - Show this help message" + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md new file mode 100644 index 0000000..55a8731 --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +""" +NovaAi – TTS-Engine-Hub +main.py +Version: v0.0.7 + +Description: + Adds /speakers endpoint to list speakers for a given engine/model. + Returns list of available speakers from engine.list_voices(model). + All previous endpoints and logic included. + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: main.py +""" + +from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import JSONResponse, FileResponse +from pydantic import BaseModel +import os +import base64 +from engines.piper import PiperEngine +from engines.styletts import StyleTTSEngine +from engines.chattts import ChatTTSEngine +import shutil +import uuid +import hashlib +import tempfile +import ffmpeg + +app = FastAPI( + title="NovaAi – TTS-Engine-Hub", + version="0.0.7", + description="Local-first, modular multi-engine TTS server for your homelab and automation." +) + +ENGINE_REGISTRY = { + "piper": PiperEngine(), + "styletts": StyleTTSEngine(), + "chattts": ChatTTSEngine(), +} + +AUDIO_OUT_DIR = "/tmp/tts_output" +CACHE_DIR = "/tmp/tts_cache" +os.makedirs(AUDIO_OUT_DIR, exist_ok=True) +os.makedirs(CACHE_DIR, exist_ok=True) + +class TTSRequest(BaseModel): + text: str + engine: str + model: str = None + speaker: str = None + format: str = "ogg" + chunking: bool = False + + +def build_cache_key(req: TTSRequest) -> str: + data = f"{req.text}|{req.engine}|{req.model}|{req.speaker}|{req.format}|{req.chunking}" + return hashlib.sha256(data.encode()).hexdigest() + +def chunk_text(text, maxlen=250): + import re + sentences = re.split(r'([.!?]\s)', text) + chunks = [] + buf = "" + for s in sentences: + if len(buf) + len(s) > maxlen: + if buf: + chunks.append(buf.strip()) + buf = "" + buf += s + if buf.strip(): + chunks.append(buf.strip()) + return [c for c in chunks if c.strip()] + +def concat_audio(files, fmt): + if len(files) == 1: + return files[0] + output_file = tempfile.mktemp(suffix=f'.{fmt}', prefix="chunked_", dir="/tmp") + if fmt == "wav": + import wave + data = [] + params = None + for f in files: + with wave.open(f, 'rb') as wf: + if params is None: + params = wf.getparams() + data.append(wf.readframes(wf.getnframes())) + with wave.open(output_file, 'wb') as wf: + wf.setparams(params) + for d in data: + wf.writeframes(d) + else: + with tempfile.NamedTemporaryFile("w", delete=False) as tf: + for f in files: + tf.write(f"file '{f}'\n") + tf.flush() + ( + ffmpeg + .input(tf.name, format='concat', safe=0) + .output(output_file, acodec='copy') + .run(overwrite_output=True, quiet=True) + ) + os.unlink(tf.name) + return output_file + +@app.post("/tts") +def tts_endpoint(req: TTSRequest, as_base64: bool = Query(False, alias="as")): + cache_key = build_cache_key(req) + ext = f'.{req.format.lower()}' + cached_file = os.path.join(CACHE_DIR, f"tts_{cache_key}{ext}") + if os.path.isfile(cached_file): + fname = f"tts_{cache_key}{ext}" + dest = os.path.join(AUDIO_OUT_DIR, fname) + shutil.copy(cached_file, dest) + if as_base64: + with open(cached_file, "rb") as f: + audio_b64 = base64.b64encode(f.read()).decode("utf-8") + return JSONResponse({ + "engine": req.engine, + "model": req.model, + "speaker": req.speaker, + "format": req.format, + "audio_base64": audio_b64, + "chunking": req.chunking, + "message": "Audio from cache, base64 included" + }) + return JSONResponse({ + "engine": req.engine, + "model": req.model, + "speaker": req.speaker, + "format": req.format, + "audio_url": f"/audio/{fname}", + "cached": True, + "chunking": req.chunking, + "message": "Audio served from cache. Download from audio_url" + }) + engine = ENGINE_REGISTRY.get(req.engine.lower()) + if not engine: + raise HTTPException(status_code=404, detail=f"Engine '{req.engine}' not found.") + if req.chunking and len(req.text) > 250: + chunks = chunk_text(req.text, maxlen=250) + chunk_files = [engine.synthesize(c, speaker=req.speaker, model=req.model, fmt=req.format) for c in chunks] + audio_path = concat_audio(chunk_files, req.format.lower()) + else: + audio_path = engine.synthesize(req.text, speaker=req.speaker, model=req.model, fmt=req.format) + shutil.copy(audio_path, cached_file) + fname = f"tts_{cache_key}{ext}" + dest = os.path.join(AUDIO_OUT_DIR, fname) + shutil.copy(audio_path, dest) + if as_base64: + with open(cached_file, "rb") as f: + audio_b64 = base64.b64encode(f.read()).decode("utf-8") + return JSONResponse({ + "engine": req.engine, + "model": req.model, + "speaker": req.speaker, + "format": req.format, + "audio_base64": audio_b64, + "chunking": req.chunking, + "message": "Audio from synth, base64 included" + }) + return JSONResponse({ + "engine": req.engine, + "model": req.model, + "speaker": req.speaker, + "format": req.format, + "audio_url": f"/audio/{fname}", + "cached": False, + "chunking": req.chunking, + "message": "Synthesized new audio. Download from audio_url" + }) + +@app.get("/audio/{filename}") +def audio_file(filename: str): + fpath = os.path.join(AUDIO_OUT_DIR, filename) + if not os.path.isfile(fpath): + raise HTTPException(status_code=404, detail="Audio file not found") + media_type = "audio/wav" if filename.endswith(".wav") else ( + "audio/ogg" if filename.endswith(".ogg") else "audio/mpeg" + ) + return FileResponse(fpath, media_type=media_type, filename=filename) + +@app.get("/engines") +def engines_endpoint(): + engines = {} + for name, engine in ENGINE_REGISTRY.items(): + engines[name] = engine.healthcheck() + return engines + +@app.get("/models") +def models_endpoint(): + result = {} + for name, engine in ENGINE_REGISTRY.items(): + try: + result[name] = engine.list_models() + except Exception as e: + result[name] = [] + return result + +@app.get("/speakers") +def speakers_endpoint(engine: str, model: str = None): + e = ENGINE_REGISTRY.get(engine.lower()) + if not e: + raise HTTPException(status_code=404, detail=f"Engine '{engine}' not found.") + try: + speakers = e.list_voices(model) + except Exception as err: + speakers = [] + return {"engine": engine, "model": model, "speakers": speakers} + +@app.get("/version") +def version(): + return {"version": app.version} + +@app.get("/health") +def health(): + status = {name: engine.healthcheck()["status"] for name, engine in ENGINE_REGISTRY.items()} + return {"status": status, "detail": "API and engines loaded"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..318bc7b --- /dev/null +++ b/app/main.py @@ -0,0 +1,204 @@ +""" +NovaAi – TTS-Engine-Hub +main.py +Version: v0.1.0 + +Description: + Refactored main.py: uses utils modules for chunking, concat, and cache key generation. + All endpoints, features, and logic as before, but cleaner and more modular. + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: main.py +""" + +import asyncio +from fastapi import FastAPI, HTTPException, Query +from fastapi.responses import JSONResponse, FileResponse +from pydantic import BaseModel +import os +import base64 +import shutil +import uvicorn + +from config import settings +from engines.piper import PiperEngine +from engines.styletts import StyleTTSEngine +from engines.chattts import ChatTTSEngine +from engines.f5_tts import F5TTSEngine +from utils.text import chunk_text +from utils.audio import concat_audio +from utils.cache import build_cache_key + +# --- Master list of all possible engine classes. --- +ALL_ENGINES = { + "piper": PiperEngine, + "styletts": StyleTTSEngine, + "chattts": ChatTTSEngine, + "f5-tts": F5TTSEngine, +} + +def create_app(): + app = FastAPI( + title="NovaAi – TTS-Engine-Hub", + version="0.3.0", + description="Local-first, modular multi-engine TTS server for your homelab and automation." + ) + + # Dynamically build the registry of active engines based on settings. + # This registry is local to the app instance created by this function. + app.ENGINE_REGISTRY = {} + for engine_name in settings.ACTIVE_ENGINES: + if engine_name in ALL_ENGINES: + print(f"Activating engine: {engine_name}") + app.ENGINE_REGISTRY[engine_name] = ALL_ENGINES[engine_name]() + else: + print(f"Warning: Engine '{engine_name}' requested in config but not found in ALL_ENGINES.") + + # Ensure the audio asset/cache directory exists. + os.makedirs(settings.AUDIO_CACHE_DIR, exist_ok=True) + + class TTSRequest(BaseModel): + text: str + engine: str + model: str = None + speaker: str = None + format: str = "ogg" + chunking: bool = False + + @app.post("/tts") + async def tts_endpoint(req: TTSRequest, as_base64: bool = Query(False, alias="as")): + # --- 1. Check for engine and handle health --- + engine = app.ENGINE_REGISTRY.get(req.engine.lower()) + if not engine: + raise HTTPException(status_code=404, detail=f"Engine '{req.engine}' not found.") + + health = engine.healthcheck() + if health.get("status") != "ok": + raise HTTPException(status_code=503, detail=f"Engine '{req.engine}' is not available. Status: {health.get('status')}") + + # --- 2. Input validation --- + available_models = engine.list_models() + if req.model and available_models and req.model not in available_models: + raise HTTPException(status_code=400, detail=f"Model '{req.model}' not found for engine '{req.engine}'. Available models: {available_models}") + + available_voices = engine.list_voices(req.model) + if req.speaker and available_voices and req.speaker not in available_voices: + raise HTTPException(status_code=400, detail=f"Speaker '{req.speaker}' not found for model '{req.model}'. Available speakers: {available_voices}") + + # --- 3. Check cache --- + cache_key = build_cache_key(req) + ext = f'.{req.format.lower()}' + output_filename = f"tts_{cache_key}{ext}" + output_filepath = os.path.join(settings.AUDIO_CACHE_DIR, output_filename) + + is_cached = await asyncio.to_thread(os.path.isfile, output_filepath) + if is_cached: + if as_base64: + audio_bytes = await asyncio.to_thread(lambda: open(output_filepath, "rb").read()) + audio_b64 = base64.b64encode(audio_bytes).decode("utf-8") + return JSONResponse({ + "engine": req.engine, "model": req.model, "speaker": req.speaker, "format": req.format, + "audio_base64": audio_b64, "chunking": req.chunking, "message": "Audio from cache, base64 included" + }) + return JSONResponse({ + "engine": req.engine, "model": req.model, "speaker": req.speaker, "format": req.format, + "audio_url": f"/audio/{output_filename}", "cached": True, "chunking": req.chunking, + "message": "Audio served from cache. Download from audio_url" + }) + + # --- 4. Synthesize audio --- + try: + if req.chunking and len(req.text) > 250: + chunks = chunk_text(req.text, maxlen=250) + synthesis_tasks = [engine.synthesize(c, speaker=req.speaker, model=req.model, fmt=req.format) for c in chunks] + chunk_files = await asyncio.gather(*synthesis_tasks) + synthesized_path = await asyncio.to_thread(concat_audio, chunk_files, req.format.lower()) + else: + synthesized_path = await engine.synthesize(req.text, speaker=req.speaker, model=req.model, fmt=req.format) + except (RuntimeError, ValueError, FileNotFoundError) as e: + raise HTTPException(status_code=500, detail=f"Error during synthesis: {e}") + except Exception as e: + raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {e}") + + # --- 5. Cache and return result --- + await asyncio.to_thread(shutil.copy, synthesized_path, output_filepath) + + # If the synth created a temp file in a different directory, clean it up + if settings.AUDIO_CACHE_DIR not in os.path.abspath(synthesized_path): + await asyncio.to_thread(os.remove, synthesized_path) + + if as_base64: + audio_bytes = await asyncio.to_thread(lambda: open(output_filepath, "rb").read()) + audio_b64 = base64.b64encode(audio_bytes).decode("utf-8") + return JSONResponse({ + "engine": req.engine, "model": req.model, "speaker": req.speaker, "format": req.format, + "audio_base64": audio_b64, "chunking": req.chunking, "message": "Audio from synth, base64 included" + }) + + return JSONResponse({ + "engine": req.engine, "model": req.model, "speaker": req.speaker, "format": req.format, + "audio_url": f"/audio/{output_filename}", "cached": False, "chunking": req.chunking, + "message": "Synthesized new audio. Download from audio_url" + }) + + @app.get("/audio/{filename}") + async def audio_file(filename: str): # Made async + fpath = os.path.join(settings.AUDIO_CACHE_DIR, filename) + if not await asyncio.to_thread(os.path.isfile, fpath) or not await asyncio.to_thread(lambda: fpath.startswith(os.path.abspath(settings.AUDIO_CACHE_DIR))): + raise HTTPException(status_code=404, detail="Audio file not found") + media_type = "audio/wav" if filename.endswith(".wav") else ( + "audio/ogg" if filename.endswith(".ogg") else "audio/mpeg" + ) + return FileResponse(fpath, media_type=media_type, filename=filename) + + @app.get("/engines") + def engines_endpoint(): + engines = {} + for name, engine in app.ENGINE_REGISTRY.items(): + engines[name] = engine.healthcheck() + return engines + + @app.get("/models") + def models_endpoint(): + result = {} + for name, engine in app.ENGINE_REGISTRY.items(): + try: + result[name] = engine.list_models() + except Exception as e: + result[name] = {"error": str(e)} + return result + + @app.get("/speakers") + def speakers_endpoint(engine: str, model: str = None): + e = app.ENGINE_REGISTRY.get(engine.lower()) + if not e: + raise HTTPException(status_code=404, detail=f"Engine '{engine}' not found.") + try: + speakers = e.list_voices(model) + except Exception as err: + speakers = [] + return {"engine": engine, "model": model, "speakers": speakers} + + @app.get("/version") + def version(): + return {"version": app.version} + + @app.get("/health") + def health(): + status = {name: engine.healthcheck()["status"] for name, engine in app.ENGINE_REGISTRY.items()} + return {"status": status, "detail": "API and engines loaded"} + + return app + +# If main.py is executed directly, create the app and run uvicorn +if __name__ == "__main__": + app_instance = create_app() + uvicorn.run( + app_instance, # Pass the app instance + host=settings.HOST, + port=settings.PORT, + reload=True + ) + + diff --git a/config.py b/config.py new file mode 100644 index 0000000..af8fc55 --- /dev/null +++ b/config.py @@ -0,0 +1,25 @@ +""" +NovaAi – TTS-Engine-Hub +config.py +Version: v0.0.1 + +Description: + Centralized configuration management using pydantic-settings. + Loads settings from a .env file and environment variables. +""" +from pydantic_settings import BaseSettings, SettingsConfigDict +from typing import List, Set + +class Settings(BaseSettings): + # Server Configuration + HOST: str = "0.0.0.0" + PORT: int = 8000 + + # Application Configuration + ACTIVE_ENGINES: Set[str] = {"piper"} + ASSET_DIR: str = "asset" + AUDIO_CACHE_DIR: str = "asset/audio" + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding='utf-8') + +settings = Settings() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1d2ef96 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: '3.8' + +services: + app: + build: . + container_name: audio_engine_hub_app + restart: unless-stopped + volumes: + # Mount local app directory for hot-reloading in dev + - ./app:/home/appuser/app + # Mount models directory to provide models to the container + - ./models:/home/appuser/models + # Mount asset directory to persist generated audio files + - ./asset:/home/appuser/asset + command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + env_file: + - .env + labels: + - "traefik.enable=true" + - "traefik.http.routers.app-router.rule=Host(`localhost`)" + - "traefik.http.routers.app-router.entrypoints=web" + - "traefik.http.services.app-service.loadbalancer.server.port=8000" + networks: + - web + + traefik: + image: "traefik:v2.10" + container_name: traefik_proxy + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + ports: + - "80:80" # The HTTP port Traefik listens on + - "8080:8080" # The Traefik Web UI (Dashboard) + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" # Traefik needs access to the Docker daemon + networks: + - web + +networks: + web: + external: false diff --git a/engines/chattts.py b/engines/chattts.py new file mode 100644 index 0000000..d9ddb92 --- /dev/null +++ b/engines/chattts.py @@ -0,0 +1,48 @@ +""" +NovaAi – TTS-Engine-Hub +engines/chattts.py +Version: v0.0.1 + +Description: + ChatTTS engine adapter. + Implements TTSEngineBase interface for ChatTTS integration (dummy implementation). + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: chattts.py +""" + +import asyncio +from engines.engine_base import TTSEngineBase + +class ChatTTSEngine(TTSEngineBase): + async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "mp3"): + # Dummy implementation: returns an empty string as it doesn't produce a file. + print("Warning: ChatTTSEngine.synthesize is a dummy and does not produce audio.") + return "" + + def list_models(self): + # Dummy implementation + return ["chattts-v1", "chattts-v2"] + + def list_voices(self, model: str = None): + # Dummy implementation + return ["default", "custom1", "custom2"] + + def healthcheck(self): + # Dummy implementation + return {"status": "ok", "engine": "chattts"} + + async def selftest(self): + # Dummy implementation + return {"selftest": True, "engine": "chattts"} + +if __name__ == "__main__": + async def main(): + engine = ChatTTSEngine() + print("Selftest:", await engine.selftest()) + print("Models:", engine.list_models()) + print("Voices:", engine.list_voices()) + print("Healthcheck:", engine.healthcheck()) + + asyncio.run(main()) diff --git a/engines/engine_base.py b/engines/engine_base.py new file mode 100644 index 0000000..ebfe33d --- /dev/null +++ b/engines/engine_base.py @@ -0,0 +1,45 @@ +""" +NovaAi – TTS-Engine-Hub +engine_base.py +Version: v0.0.1 + +Description: + Abstract base class for all TTS engine modules. + Defines the required interface for engine adapters. + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: engine_base.py +""" + +from abc import ABC, abstractmethod +import asyncio + +class TTSEngineBase(ABC): + @abstractmethod + async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "ogg"): + """ + Asynchronously generate speech audio from text input. + Returns path to audio file. + """ + raise NotImplementedError + + @abstractmethod + def list_models(self): + """Return a list of available models.""" + raise NotImplementedError + + @abstractmethod + def list_voices(self, model: str = None): + """Return a list of available voices for a model.""" + raise NotImplementedError + + @abstractmethod + def healthcheck(self): + """Return health/status info for this engine.""" + raise NotImplementedError + + @abstractmethod + async def selftest(self): + """Asynchronously run internal self-test (basic functionality check).""" + raise NotImplementedError diff --git a/engines/f5-tts-voices/default.txt b/engines/f5-tts-voices/default.txt new file mode 100644 index 0000000..d9021fa --- /dev/null +++ b/engines/f5-tts-voices/default.txt @@ -0,0 +1 @@ +Some call me nature, others call me mother nature. \ No newline at end of file diff --git a/engines/f5_tts.py b/engines/f5_tts.py new file mode 100644 index 0000000..c265584 --- /dev/null +++ b/engines/f5_tts.py @@ -0,0 +1,143 @@ +""" +NovaAi – TTS-Engine-Hub +f5_tts.py +Version: v0.0.2 + +Description: + F5-TTS engine module. + Implements the TTSEngineBase for F5-TTS text-to-speech synthesis. + Now with robust speaker handling. + +Author: Your Name (or leave as generated) +Date: 2025-12-03 +""" + +import os +import tempfile +import torch +import torchaudio +import numpy as np +import soundfile as sf +import asyncio +from .engine_base import TTSEngineBase +from importlib.resources import files + +try: + from f5_tts.api import F5TTS +except ImportError: + print("Warning: F5TTS could not be imported. F5-TTS engine will not be available.") + F5TTS = None + +class F5TTSEngine(TTSEngineBase): + def __init__(self): + # Initialize F5-TTS specific resources, models, etc. + print("F5-TTS Engine Initializing...") + self.speakers = {} + self.model = None + + if F5TTS: + try: + self.model = F5TTS(model="F5TTS_v1_Base") + print("F5-TTS Engine Initialized.") + self._load_speakers() + except Exception as e: + print(f"Error initializing F5-TTS Engine: {e}") + self.model = None + else: + print("F5-TTS Engine not initialized because F5TTS is not available.") + + def _load_speakers(self): + # Add the default speaker + default_wav = str(files("f5_tts").joinpath("infer/examples/basic/basic_ref_en.wav")) + default_txt = "engines/f5-tts-voices/default.txt" + if os.path.exists(default_txt): + self.speakers["default"] = {"wav": default_wav, "txt": default_txt} + + # Scan for custom speakers + voices_dir = "engines/f5-tts-voices" + if not os.path.isdir(voices_dir): + return + for file in os.listdir(voices_dir): + if file.endswith(".wav"): + speaker_name = file.rsplit('.', 1)[0] + wav_path = os.path.join(voices_dir, file) + txt_path = os.path.join(voices_dir, f"{speaker_name}.txt") + if os.path.exists(txt_path): + self.speakers[speaker_name] = {"wav": wav_path, "txt": txt_path} + print(f"Found custom speaker: {speaker_name}") + + def _blocking_synthesize(self, text: str, speaker: str, fmt: str): + """The actual blocking synthesis logic.""" + speaker_data = self.speakers[speaker] + ref_file = speaker_data["wav"] + + with open(speaker_data["txt"], 'r') as f: + ref_text = f.read() + + print(f"F5-TTS: Synthesizing '{text}' with reference voice from '{ref_file}'.") + + wav, sr, spec = self.model.infer( + ref_file=ref_file, + ref_text=ref_text, + gen_text=text, + ) + + with tempfile.NamedTemporaryFile(delete=False, suffix=f".{fmt}") as temp_file: + if fmt == "wav": + torchaudio.save(temp_file.name, torch.from_numpy(wav).unsqueeze(0), sr, format="wav") + else: + # Convert to float32 for soundfile + wav_float = wav.astype(np.float32) / np.iinfo(wav.dtype).max + sf.write(temp_file.name, wav_float, sr) + return temp_file.name + + async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "ogg"): + """ + Asynchronously generate speech audio from text input using F5-TTS. + """ + if not self.model: + raise RuntimeError("F5-TTS Engine not initialized.") + + speaker_to_use = speaker if speaker in self.speakers else "default" + if speaker and speaker not in self.speakers: + print(f"Warning: Speaker '{speaker}' not found. Falling back to default speaker.") + + if speaker_to_use not in self.speakers: + raise RuntimeError("No default speaker found for F5-TTS. Please add a 'default.wav' and 'default.txt' to the 'engines/f5-tts-voices' directory.") + + try: + # Run the blocking synthesis in a separate thread + return await asyncio.to_thread(self._blocking_synthesize, text, speaker_to_use, fmt) + except Exception as e: + raise RuntimeError(f"F5-TTS synthesis failed: {e}") + + def list_models(self): + """Return a list of available F5-TTS models.""" + if not self.model: + return [] + return ["F5TTS_v1_Base"] + + def list_voices(self, model: str = None): + """Return a list of available F5-TTS voices for a model.""" + return list(self.speakers.keys()) + + def healthcheck(self): + """Return health/status info for F5-TTS engine.""" + if self.model: + return {"status": "ok", "message": "F5-TTS engine is ready"} + else: + return {"status": "error", "message": "F5-TTS engine failed to initialize"} + + async def selftest(self): + """Run internal self-test for F5-TTS.""" + if not self.model: + return {"status": "failed", "message": "F5-TTS Engine not initialized."} + try: + # Await the async synthesize method + audio_file = await self.synthesize("this is a test.") + selftest_passed = os.path.exists(audio_file) and os.path.getsize(audio_file) > 0 + if selftest_passed: + os.remove(audio_file) + return {"status": "passed" if selftest_passed else "failed", "message": "F5-TTS self-test successful"} + except Exception as e: + return {"status": "failed", "message": f"F5-TTS self-test failed: {e}"} \ No newline at end of file diff --git a/engines/piper.py b/engines/piper.py new file mode 100644 index 0000000..489097f --- /dev/null +++ b/engines/piper.py @@ -0,0 +1,163 @@ +""" +NovaAi – TTS-Engine-Hub +engines/piper.py +Version: v0.1.1 + +Description: + Piper TTS engine adapter: real CLI invocation + output as WAV, OGG, or MP3. + Synthesizes WAV via Piper, converts to OGG/MP3 via ffmpeg-python if needed. + Uses dynamic model path: ./models/piper/[model]/model.onnx + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: piper.py +""" + +import asyncio +import subprocess +import tempfile +import os +import shutil +import json +from engines.engine_base import TTSEngineBase +import ffmpeg + +class PiperEngine(TTSEngineBase): + def __init__(self): + self.piper_executable = shutil.which("piper") + self.ffmpeg_executable = shutil.which("ffmpeg") + + def _load_config(self, model: str): + """Load the model config JSON file to get speaker mappings.""" + model_dir = f"./models/piper/{model}" + config_file = os.path.join(model_dir, f"{model}.onnx.json") + if os.path.isfile(config_file): + with open(config_file, 'r') as f: + return json.load(f) + return {} + + def _get_speaker_id(self, speaker: str, model: str): + """Convert speaker name to speaker ID using the model's config.""" + if not speaker or speaker == "default": + return None + if speaker.isdigit(): + return speaker + config = self._load_config(model) + speaker_id_map = config.get('speaker_id_map', {}) + return str(speaker_id_map.get(speaker)) + + def _run_ffmpeg_blocking(self, input_path, output_path): + """Wrapper for the blocking ffmpeg call.""" + ( + ffmpeg + .input(input_path) + .output(output_path) + .run(overwrite_output=True, quiet=True) + ) + + async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "ogg"): + if not self.piper_executable: + raise RuntimeError("Piper executable not found. Please install it and ensure it's in your PATH.") + if not model: + raise ValueError("Model must be specified for Piper.") + model_dir = f"./models/piper/{model}" + model_file = os.path.join(model_dir, f"{model}.onnx") + if not os.path.isfile(model_file): + raise FileNotFoundError(f"Piper model not found: {model_file}") + + with tempfile.NamedTemporaryFile(suffix=".wav", prefix="piper_", delete=False) as wav_file: + output_wav_path = wav_file.name + + cmd = [self.piper_executable, "--model", model_file, "--output_file", output_wav_path, "--stdin_text"] + + if speaker: + speaker_id = self._get_speaker_id(speaker, model) + if speaker_id: + cmd += ["--speaker", speaker_id] + + process = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate(input=text.encode('utf-8')) + + if process.returncode != 0: + os.remove(output_wav_path) + raise RuntimeError(f"Piper synth failed: {stderr.decode()}") + + fmt = (fmt or "ogg").lower() + if fmt == "wav": + return output_wav_path + + if not self.ffmpeg_executable: + os.remove(output_wav_path) + raise RuntimeError("ffmpeg not found, cannot convert audio format.") + + with tempfile.NamedTemporaryFile(suffix=f'.{fmt}', prefix="piper_conv_", delete=False) as converted_file: + output_other_path = converted_file.name + + try: + await asyncio.to_thread(self._run_ffmpeg_blocking, output_wav_path, output_other_path) + except Exception as e: + raise RuntimeError(f"ffmpeg conversion failed: {e}") + finally: + os.remove(output_wav_path) + + return output_other_path + + def list_models(self): + models_dir = "./models/piper/" + if not os.path.isdir(models_dir): + return [] + return [name for name in os.listdir(models_dir) + if os.path.isdir(os.path.join(models_dir, name))] + + def list_voices(self, model: str = None): + if not model: + return ["default"] + config = self._load_config(model) + speaker_id_map = config.get('speaker_id_map', {}) + if speaker_id_map: + return ["default"] + sorted(speaker_id_map.keys()) + return ["default"] + + def healthcheck(self): + status = "ok" + if not self.piper_executable: + status = "missing_piper_executable" + return {"status": status, "engine": "piper"} + + async def selftest(self): + if not self.piper_executable: + return {"selftest": False, "error": "Piper executable not found.", "engine": "piper"} + try: + models = self.list_models() + if not models: + return {"selftest": False, "error": "No Piper models found.", "engine": "piper"} + + test_text = "This is a selftest." + first_model = models[0] + voices = self.list_voices(first_model) + test_voice = voices[0] if voices else None + + audio_file = await self.synthesize(test_text, speaker=test_voice, model=first_model, fmt="wav") + + selftest_passed = os.path.exists(audio_file) and os.path.getsize(audio_file) > 0 + if selftest_passed: + os.remove(audio_file) + + return {"selftest": selftest_passed, "models": models, "engine": "piper"} + except Exception as e: + return {"selftest": False, "error": str(e), "engine": "piper"} + +if __name__ == "__main__": + async def main(): + engine = PiperEngine() + print("Selftest:", await engine.selftest()) + print("Models:", engine.list_models()) + print("Voices:", engine.list_voices(engine.list_models()[0])) + print("Healthcheck:", engine.healthcheck()) + + asyncio.run(main()) diff --git a/engines/styletts.py b/engines/styletts.py new file mode 100644 index 0000000..63a0dbf --- /dev/null +++ b/engines/styletts.py @@ -0,0 +1,48 @@ +""" +NovaAi – TTS-Engine-Hub +engines/styletts.py +Version: v0.0.1 + +Description: + StyleTTS engine adapter. + Implements TTSEngineBase interface for StyleTTS integration (dummy implementation). + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: styletts.py +""" + +import asyncio +from engines.engine_base import TTSEngineBase + +class StyleTTSEngine(TTSEngineBase): + async def synthesize(self, text: str, speaker: str = None, model: str = None, fmt: str = "mp3"): + # Dummy implementation: returns an empty string as it doesn't produce a file. + print("Warning: StyleTTSEngine.synthesize is a dummy and does not produce audio.") + return "" + + def list_models(self): + # Dummy implementation + return ["styletts_v2_de", "styletts_v2_en"] + + def list_voices(self, model: str = None): + # Dummy implementation + return ["neutral", "emotional", "female"] + + def healthcheck(self): + # Dummy implementation + return {"status": "ok", "engine": "styletts"} + + async def selftest(self): + # Dummy implementation + return {"selftest": True, "engine": "styletts"} + +if __name__ == "__main__": + async def main(): + engine = StyleTTSEngine() + print("Selftest:", await engine.selftest()) + print("Models:", engine.list_models()) + print("Voices:", engine.list_voices()) + print("Healthcheck:", engine.healthcheck()) + + asyncio.run(main()) diff --git a/guide.md b/guide.md new file mode 100644 index 0000000..178595c --- /dev/null +++ b/guide.md @@ -0,0 +1,296 @@ +# Leitfaden: Best Practices zur Containerisierung von Python-Backends + +Dieser Leitfaden zeigt einen professionellen Workflow zur Containerisierung einer Python-Backend-Anwendung mit Docker, inklusive Integration eines Reverse Proxys (Traefik) für die lokale Entwicklung. Wir verwenden eine minimale FastAPI-Anwendung als Beispiel. + +Die vorgestellten Methoden umfassen: +- Optimierte, sichere Docker-Images durch Multi-Stage-Builds. +- Einen einfachen Workflow durch die Verwendung eines `Makefile`. +- Trennung von Entwicklungs- und Produktionsumgebungen. +- Korrekte Verwaltung von Code und Images mit Git und einer Container Registry. +- Integration von Traefik für dynamisches Routing in der lokalen Entwicklung. + +## 1. Projektstruktur + +Wir haben die folgende Struktur erstellt: + +``` +/ +├── .dockerignore # Listet Dateien auf, die Docker ignorieren soll. +├── .gitignore # Listet Dateien auf, die Git ignorieren soll. +├── Dockerfile # Die Blaupause für unser produktives Docker-Image. +├── Makefile # Vereinfacht die Ausführung von Docker-Befehlen. +├── app/ +│ ├── __init__.py # Macht 'app' zu einem Python-Paket. +│ └── main.py # Unser FastAPI-Anwendungscode. +├── docker-compose.yml # Definiert die lokale Entwicklungsumgebung mit Traefik. +├── guide.md # Dieser Leitfaden. +└── requirements.txt # Liste der Python-Abhängigkeiten. +``` + +## 2. Die Komponenten im Detail + +### `requirements.txt` +Hier definieren wir die Python-Bibliotheken, die unser Projekt benötigt. +``` +fastapi +uvicorn[standard] +gunicorn +``` +- `fastapi`: Das Web-Framework. +- `uvicorn`: Ein schneller ASGI-Server, ideal für die Entwicklung. +- `gunicorn`: Ein robuster WSGI-Produktionsserver, der Uvicorn-Worker zur Ausführung unserer ASGI-Anwendung nutzt. + +### `.gitignore` & `.dockerignore` +- **`.gitignore`**: Verhindert, dass sensible Daten (`.env`), temporäre Dateien (`__pycache__`) oder lokale Konfigurationen (`.vscode/`) in Git eingecheckt werden. +- **`.dockerignore`**: Verhindert, dass unnötige oder sensible Dateien in das Docker-Image kopiert werden. Dies hält das Image klein und sicher. Wir schließen z.B. das `.git`-Verzeichnis, das `Dockerfile` selbst und lokale Umgebungsdateien aus. + +### `app/main.py` +Eine minimale FastAPI-Anwendung mit zwei Endpunkten. +```python +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +def read_root(): + return {"message": "Hello World"} + +@app.get("/health") +def health_check(): + return {"status": "ok"} +``` + +### `Dockerfile` (Multi-Stage-Build) +Dies ist das Herzstück unserer Containerisierung. Ein Multi-Stage-Build trennt die Build-Umgebung von der Laufzeitumgebung. + +**Stufe 1: `builder`** +```dockerfile +FROM python:3.11 as builder +WORKDIR /opt/venv +RUN python -m venv . +COPY requirements.txt . +RUN . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt +``` +- Wir starten mit einem vollständigen Python-Image. +- Erstellen ein virtuelles Environment (`venv`) und installieren die Abhängigkeiten hinein. +- Das Ergebnis ist ein Ordner `/opt/venv` mit einer sauberen Python-Umgebung. + +**Stufe 2: `runner` (Das finale Image)** +```dockerfile +FROM python:3.11-slim +RUN useradd --create-home --shell /bin/bash appuser +WORKDIR /home/appuser/app +COPY --from=builder /opt/venv /opt/venv +COPY app/ . +ENV PATH="/opt/venv/bin:$PATH" +EXPOSE 8000 +USER appuser +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "2", "-b", "0.0.0.0:8000", "main:app"] +``` +- Wir starten mit einem minimalen `-slim`-Image. +- **Sicherheit**: Wir erstellen einen unprivilegierten Benutzer `appuser`. Die Anwendung wird als dieser Benutzer ausgeführt. +- Wir kopieren das `venv` aus der `builder`-Stufe und unseren App-Code. +- **Produktions-Server**: Wir starten die App mit `gunicorn` und 2 `uvicorn`-Workern. Dies ist ein stabiles Setup für die Produktion. + +### `docker-compose.yml` (Für die lokale Entwicklung mit Traefik) +Diese Datei definiert unsere lokale Entwicklungsumgebung, die nun auch Traefik als Reverse Proxy enthält. + +**Was ist Traefik?** +Traefik ist ein moderner Edge Router und Reverse Proxy, der dynamisch Dienste auf der Grundlage ihrer Konfiguration (z.B. Docker-Labels) entdeckt. Er leitet Anfragen an die richtigen Container weiter, ohne dass man manuelle Konfigurationen in einer separaten Datei vornehmen muss. + +```yaml +version: '3.8' + +services: + app: + build: . + volumes: + - ./app:/home/appuser/app + command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] + labels: + - "traefik.enable=true" + # Definiert einen Router für den App-Dienst + - "traefik.http.routers.app-router.rule=Host(`localhost`)" + - "traefik.http.routers.app-router.entrypoints=web" + # Definiert den internen Port des Dienstes (im Container) + - "traefik.http.services.app-service.loadbalancer.server.port=8000" + networks: + - web + + traefik: + image: "traefik:v2.10" + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" # Nur Dienste mit traefik.enable=true exposen + - "--entrypoints.web.address=:80" + ports: + - "80:80" # Der HTTP-Port, über den Traefik lauscht + - "8080:8080" # Das Web UI (Dashboard) von Traefik + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" # Traefik braucht Zugriff auf den Docker-Daemon + networks: + - web + +networks: + web: + external: false +``` +- **`app` Service**: Die direkte Port-Exponierung wurde entfernt. Stattdessen nutzt Traefik die `labels`, um das Routing zu konfigurieren. + - `traefik.enable=true`: Aktiviert die Erkennung durch Traefik. + - `traefik.http.routers.app-router.rule=Host('localhost')`: Sagt Traefik, dass Anfragen an die Host `localhost` an diesen Dienst geleitet werden sollen. + - `traefik.http.routers.app-router.entrypoints=web`: Verknüpft diesen Router mit dem `web`-Entrypoint von Traefik (Port 80). + - `traefik.http.services.app-service.loadbalancer.server.port=8000`: Informiert Traefik über den internen Port des FastAPI-Dienstes. +- **`traefik` Service**: Dies ist der Traefik-Container selbst. + - Er konfiguriert das Dashboard (`--api.dashboard=true`) und aktiviert den Docker-Provider (`--providers.docker=true`), der Container anhand ihrer Labels entdeckt. + - Er lauscht auf Port 80 (HTTP) und 8080 (Dashboard). + - `/var/run/docker.sock`: Ermöglicht Traefik die Kommunikation mit dem Docker-Daemon, um Container und deren Labels zu erkennen. +- **`networks: web`**: Erstellt ein gemeinsames Netzwerk, in dem Traefik und die `app` kommunizieren können. + +### `Makefile` (Der Kommando-Runner) +Das `Makefile` gibt uns einfache Befehle für komplexe Aktionen. Es wurde angepasst, um die Traefik-Ports zu berücksichtigen. + +```makefile +# --- Configuration --- +# Default internal application port (exposed to Traefik) +PORT := 8000 +# Traefik's external entrypoints +TRAEFIK_WEB_PORT := 80 +TRAEFIK_DASHBOARD_PORT := 8080 + +# --- Port Checking Functions --- + +# Function to check if required Traefik ports are free +# Exits if any are busy, does not suggest alternatives. +define check_traefik_ports_free + @echo "Checking if Traefik ports ($(TRAEFIK_WEB_PORT}, $(TRAEFIK_DASHBOARD_PORT}) are free..." >&2 + @local busy_ports=""; \ + if ss -tulnp | grep ":$(TRAEFIK_WEB_PORT) " > /dev/null; then \ + busy_ports="$$busy_ports $(TRAEFIK_WEB_PORT)"; \ + fi; \ + if ss -tulnp | grep ":$(TRAEFIK_DASHBOARD_PORT) " > /dev/null; then \ + busy_ports="$$busy_ports $(TRAEFIK_DASHBOARD_PORT)"; \ + fi; \ + if [ -n "$$busy_ports" ]; then \ + echo "ERROR: The following Traefik ports are already in use: $$busy_ports. Please free them or stop Traefik if already running." >&2; \ + exit 1; \ + fi; \ + @echo "Traefik ports are free." >&2 +endef + +# Function to find next free host port for the app and ask user +# This function will echo the chosen port if successful, or exit with an error. +define check_app_port_free + @local start_port=$(PORT); \ + local found_port=$$start_port; \ + local is_free=false; \ + \ + if ! ss -tulnp | grep ":$$start_port " > /dev/null; then \ + echo "$$start_port"; \ + exit 0; \ + fi; \ + \ + echo "Port $$start_port is busy. Searching for a free port for the app..." >&2; \ + while ! $$is_free; do \ + if ! ss -tulnp | grep ":$$found_port " > /dev/null; then \ + is_free=true; \ + else \ + ((found_port++)); \ + if [ "$$found_port" -gt 65535 ]; then \ + echo "ERROR: No free ports found up to 65535. Aborting." >&2; \ + exit 1; \ + fi; \ + fi; \ + done; \ + \ + echo "Port $$start_port is busy. I found port $$found_port to be free for the app." >&2; \ + read -p "Do you want to use port $$found_port for the app? (y/N): " choice; \ + case "$$choice" in \ + y|Y ) \ + echo "$$found_port"; \ + ;; \ + * ) \ + echo "Operation cancelled by user." >&2; \ + exit 1; \ + ;; \ + esac; +endef + +# --- Docker Commands --- + +.PHONY: build +build: + @echo "Building Docker image: $(IMAGE_NAME):$(TAG)" + docker build -t $(IMAGE_NAME):$(TAG) . + +.PHONY: run +run: + @export SELECTED_HOST_PORT=$$(bash -c 'func() { $(check_app_port_free) }; func') && \ + echo "Using host port $$SELECTED_HOST_PORT for single app container" && \ + docker run -d -p $$SELECTED_HOST_PORT:$(PORT) --name $(IMAGE_NAME) $(IMAGE_NAME):$(TAG) + +.PHONY: stop +stop: + @echo "Stopping Docker container: $(IMAGE_NAME)" + docker stop $(IMAGE_NAME) || true + docker rm $(IMAGE_NAME) || true + +.PHONY: logs +logs: + @echo "Showing logs for container: $(IMAGE_NAME)" + docker logs -f $(IMAGE_NAME) + +.PHONY: shell +shell: + @echo "Accessing shell in container: $(IMAGE_NAME)" + docker exec -it $(IMAGE_NAME) /bin/bash + +# --- Docker Compose Commands --- + +.PHONY: up +up: + $(call check_traefik_ports_free) # Check Traefik ports before starting + @echo "Starting development environment with Docker Compose (Traefik enabled)..." + docker-compose up --build -d + +.PHONY: down +down: + @echo "Stopping development environment with Docker Compose..." + docker-compose down + +# --- Image Management --- + +.PHONY: tag +tag: + @echo "Tagging image $(IMAGE_NAME):$(TAG) as $(REGISTRY)/$(IMAGE_NAME):$(TAG)" + docker tag $(IMAGE_NAME):$(TAG) $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +.PHONY: push +push: tag + @echo "Pushing image $(REGISTRY)/$(IMAGE_NAME):$(TAG) to registry..." + docker push $(REGISTRY)/$(IMAGE_NAME):$(TAG) + +# --- Cleanup --- + +.PHONY: clean +clean: + @echo "Cleaning up stopped containers and dangling images..." + docker container prune -f + docker image prune -f + +.PHONY: help +help: + @echo "Available commands:" + @echo " build - Build the Docker image" + @echo " run - Run the Docker container (single app, no Traefik)" + @echo " stop - Stop and remove the Docker container" + @echo " logs - Follow the logs of the container" + @echo " shell - Get a shell inside the running container" + @echo " up - Start the dev environment with docker-compose (with Traefik)" + @echo " down - Stop the dev environment with docker-compose" + @echo " tag - Tag the image for a registry" + @echo " push - Push the image to a registry (after tagging)" + @echo " clean - Clean up unused containers and images" + @echo " help - Show this help message" + +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/models/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json b/models/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json new file mode 100644 index 0000000..2329bd2 --- /dev/null +++ b/models/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json @@ -0,0 +1,493 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "high" + }, + "espeak": { + "voice": "de" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "de_DE", + "family": "de", + "region": "DE", + "name_native": "Deutsch", + "name_english": "German", + "country_english": "Germany" + }, + "dataset": "thorsten" +} \ No newline at end of file diff --git a/models/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json b/models/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json new file mode 100644 index 0000000..a84f9e2 --- /dev/null +++ b/models/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json @@ -0,0 +1,502 @@ +{ + "piper_version": "1.1.0", + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "de" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 8, + "speaker_id_map": { + "amused": 0, + "angry": 1, + "disgusted": 2, + "drunk": 3, + "neutral": 4, + "sleepy": 5, + "surprised": 6, + "whisper": 7 + }, + "language": { + "code": "de_DE", + "family": "de", + "region": "DE", + "name_native": "Deutsch", + "name_english": "German", + "country_english": "Germany" + }, + "dataset": "thorsten_emotional" +} \ No newline at end of file diff --git a/models/en_GB-cori-high/en_GB-cori-high.onnx.json b/models/en_GB-cori-high/en_GB-cori-high.onnx.json new file mode 100644 index 0000000..ec08ff8 --- /dev/null +++ b/models/en_GB-cori-high/en_GB-cori-high.onnx.json @@ -0,0 +1,502 @@ +{ + "dataset": "cori", + "audio": { + "sample_rate": 22050, + "quality": "high" + }, + "espeak": { + "voice": "en" + }, + "language": { + "code": "en_GB", + "family": "en", + "region": "GB", + "name_native": "English", + "name_english": "English", + "country_english": "Great Britain" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + " ": [ + 3 + ], + "!": [ + 4 + ], + "\"": [ + 150 + ], + "#": [ + 149 + ], + "$": [ + 2 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "X": [ + 156 + ], + "^": [ + 1 + ], + "_": [ + 0 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "g": [ + 154 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʦ": [ + 155 + ], + "ʰ": [ + 145 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "ˤ": [ + 146 + ], + "̃": [ + 141 + ], + "̧": [ + 140 + ], + "̩": [ + 144 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ], + "β": [ + 125 + ], + "ε": [ + 147 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "↑": [ + 151 + ], + "↓": [ + 148 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0" +} \ No newline at end of file diff --git a/models/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json b/models/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json new file mode 100644 index 0000000..8b9509f --- /dev/null +++ b/models/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json @@ -0,0 +1,603 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en-gb-x-rp" + }, + "inference": { + "noise_scale": 0.333, + "length_scale": 1.4, + "noise_w": 0.333 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 109, + "speaker_id_map": { + "p239": 0, + "p236": 1, + "p264": 2, + "p250": 3, + "p259": 4, + "p247": 5, + "p261": 6, + "p263": 7, + "p283": 8, + "p286": 9, + "p274": 10, + "p276": 11, + "p270": 12, + "p281": 13, + "p277": 14, + "p231": 15, + "p271": 16, + "p238": 17, + "p257": 18, + "p273": 19, + "p284": 20, + "p329": 21, + "p361": 22, + "p287": 23, + "p360": 24, + "p374": 25, + "p376": 26, + "p310": 27, + "p304": 28, + "p334": 29, + "p340": 30, + "p323": 31, + "p347": 32, + "p330": 33, + "p308": 34, + "p314": 35, + "p317": 36, + "p339": 37, + "p311": 38, + "p294": 39, + "p305": 40, + "p266": 41, + "p335": 42, + "p318": 43, + "p351": 44, + "p333": 45, + "p313": 46, + "p316": 47, + "p244": 48, + "p307": 49, + "p363": 50, + "p336": 51, + "p297": 52, + "p312": 53, + "p267": 54, + "p275": 55, + "p295": 56, + "p258": 57, + "p288": 58, + "p301": 59, + "p232": 60, + "p292": 61, + "p272": 62, + "p280": 63, + "p278": 64, + "p341": 65, + "p268": 66, + "p298": 67, + "p299": 68, + "p279": 69, + "p285": 70, + "p326": 71, + "p300": 72, + "s5": 73, + "p230": 74, + "p345": 75, + "p254": 76, + "p269": 77, + "p293": 78, + "p252": 79, + "p262": 80, + "p243": 81, + "p227": 82, + "p343": 83, + "p255": 84, + "p229": 85, + "p240": 86, + "p248": 87, + "p253": 88, + "p233": 89, + "p228": 90, + "p282": 91, + "p251": 92, + "p246": 93, + "p234": 94, + "p226": 95, + "p260": 96, + "p245": 97, + "p241": 98, + "p303": 99, + "p265": 100, + "p306": 101, + "p237": 102, + "p249": 103, + "p256": 104, + "p302": 105, + "p364": 106, + "p225": 107, + "p362": 108 + }, + "piper_version": "1.0.0", + "language": { + "code": "en_GB", + "family": "en", + "region": "GB", + "name_native": "English", + "name_english": "English", + "country_english": "Great Britain" + }, + "dataset": "vctk" +} \ No newline at end of file diff --git a/models/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json b/models/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json new file mode 100644 index 0000000..049160c --- /dev/null +++ b/models/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json @@ -0,0 +1,508 @@ +{ + "dataset": "hfc_female", + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en-us" + }, + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + " ": [ + 3 + ], + "!": [ + 4 + ], + "\"": [ + 150 + ], + "#": [ + 149 + ], + "$": [ + 2 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "X": [ + 156 + ], + "^": [ + 1 + ], + "_": [ + 0 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "g": [ + 154 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʦ": [ + 155 + ], + "ʰ": [ + 145 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "ˤ": [ + 146 + ], + "̃": [ + 141 + ], + "̊": [ + 158 + ], + "̝": [ + 157 + ], + "̧": [ + 140 + ], + "̩": [ + 144 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ], + "β": [ + 125 + ], + "ε": [ + 147 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "↑": [ + 151 + ], + "↓": [ + 148 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0" +} diff --git a/models/en_US-kristin-medium/en_US-kristin-medium.onnx.json b/models/en_US-kristin-medium/en_US-kristin-medium.onnx.json new file mode 100644 index 0000000..e94be84 --- /dev/null +++ b/models/en_US-kristin-medium/en_US-kristin-medium.onnx.json @@ -0,0 +1,502 @@ +{ + "dataset": "kristin", + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en" + }, + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + " ": [ + 3 + ], + "!": [ + 4 + ], + "\"": [ + 150 + ], + "#": [ + 149 + ], + "$": [ + 2 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "X": [ + 156 + ], + "^": [ + 1 + ], + "_": [ + 0 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "g": [ + 154 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʦ": [ + 155 + ], + "ʰ": [ + 145 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "ˤ": [ + 146 + ], + "̃": [ + 141 + ], + "̧": [ + 140 + ], + "̩": [ + 144 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ], + "β": [ + 125 + ], + "ε": [ + 147 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "↑": [ + 151 + ], + "↓": [ + 148 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0" +} \ No newline at end of file diff --git a/models/piper/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json b/models/piper/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json new file mode 100644 index 0000000..2329bd2 --- /dev/null +++ b/models/piper/de_DE-thorsten-high/de_DE-thorsten-high.onnx.json @@ -0,0 +1,493 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "high" + }, + "espeak": { + "voice": "de" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0", + "language": { + "code": "de_DE", + "family": "de", + "region": "DE", + "name_native": "Deutsch", + "name_english": "German", + "country_english": "Germany" + }, + "dataset": "thorsten" +} \ No newline at end of file diff --git a/models/piper/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json b/models/piper/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json new file mode 100644 index 0000000..a84f9e2 --- /dev/null +++ b/models/piper/de_DE-thorsten_emotional-medium/de_DE-thorsten_emotional-medium.onnx.json @@ -0,0 +1,502 @@ +{ + "piper_version": "1.1.0", + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "de" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 8, + "speaker_id_map": { + "amused": 0, + "angry": 1, + "disgusted": 2, + "drunk": 3, + "neutral": 4, + "sleepy": 5, + "surprised": 6, + "whisper": 7 + }, + "language": { + "code": "de_DE", + "family": "de", + "region": "DE", + "name_native": "Deutsch", + "name_english": "German", + "country_english": "Germany" + }, + "dataset": "thorsten_emotional" +} \ No newline at end of file diff --git a/models/piper/en_GB-cori-high/en_GB-cori-high.onnx.json b/models/piper/en_GB-cori-high/en_GB-cori-high.onnx.json new file mode 100644 index 0000000..ec08ff8 --- /dev/null +++ b/models/piper/en_GB-cori-high/en_GB-cori-high.onnx.json @@ -0,0 +1,502 @@ +{ + "dataset": "cori", + "audio": { + "sample_rate": 22050, + "quality": "high" + }, + "espeak": { + "voice": "en" + }, + "language": { + "code": "en_GB", + "family": "en", + "region": "GB", + "name_native": "English", + "name_english": "English", + "country_english": "Great Britain" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + " ": [ + 3 + ], + "!": [ + 4 + ], + "\"": [ + 150 + ], + "#": [ + 149 + ], + "$": [ + 2 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "X": [ + 156 + ], + "^": [ + 1 + ], + "_": [ + 0 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "g": [ + 154 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʦ": [ + 155 + ], + "ʰ": [ + 145 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "ˤ": [ + 146 + ], + "̃": [ + 141 + ], + "̧": [ + 140 + ], + "̩": [ + 144 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ], + "β": [ + 125 + ], + "ε": [ + 147 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "↑": [ + 151 + ], + "↓": [ + 148 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0" +} \ No newline at end of file diff --git a/models/piper/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json b/models/piper/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json new file mode 100644 index 0000000..8b9509f --- /dev/null +++ b/models/piper/en_GB-vctk-medium/en_GB-vctk-medium.onnx.json @@ -0,0 +1,603 @@ +{ + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en-gb-x-rp" + }, + "inference": { + "noise_scale": 0.333, + "length_scale": 1.4, + "noise_w": 0.333 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + "_": [ + 0 + ], + "^": [ + 1 + ], + "$": [ + 2 + ], + " ": [ + 3 + ], + "!": [ + 4 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "β": [ + 125 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "ⱱ": [ + 129 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + "̧": [ + 140 + ], + "̃": [ + 141 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̩": [ + 144 + ], + "ʰ": [ + 145 + ], + "ˤ": [ + 146 + ], + "ε": [ + 147 + ], + "↓": [ + 148 + ], + "#": [ + 149 + ], + "\"": [ + 150 + ], + "↑": [ + 151 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ] + }, + "num_symbols": 256, + "num_speakers": 109, + "speaker_id_map": { + "p239": 0, + "p236": 1, + "p264": 2, + "p250": 3, + "p259": 4, + "p247": 5, + "p261": 6, + "p263": 7, + "p283": 8, + "p286": 9, + "p274": 10, + "p276": 11, + "p270": 12, + "p281": 13, + "p277": 14, + "p231": 15, + "p271": 16, + "p238": 17, + "p257": 18, + "p273": 19, + "p284": 20, + "p329": 21, + "p361": 22, + "p287": 23, + "p360": 24, + "p374": 25, + "p376": 26, + "p310": 27, + "p304": 28, + "p334": 29, + "p340": 30, + "p323": 31, + "p347": 32, + "p330": 33, + "p308": 34, + "p314": 35, + "p317": 36, + "p339": 37, + "p311": 38, + "p294": 39, + "p305": 40, + "p266": 41, + "p335": 42, + "p318": 43, + "p351": 44, + "p333": 45, + "p313": 46, + "p316": 47, + "p244": 48, + "p307": 49, + "p363": 50, + "p336": 51, + "p297": 52, + "p312": 53, + "p267": 54, + "p275": 55, + "p295": 56, + "p258": 57, + "p288": 58, + "p301": 59, + "p232": 60, + "p292": 61, + "p272": 62, + "p280": 63, + "p278": 64, + "p341": 65, + "p268": 66, + "p298": 67, + "p299": 68, + "p279": 69, + "p285": 70, + "p326": 71, + "p300": 72, + "s5": 73, + "p230": 74, + "p345": 75, + "p254": 76, + "p269": 77, + "p293": 78, + "p252": 79, + "p262": 80, + "p243": 81, + "p227": 82, + "p343": 83, + "p255": 84, + "p229": 85, + "p240": 86, + "p248": 87, + "p253": 88, + "p233": 89, + "p228": 90, + "p282": 91, + "p251": 92, + "p246": 93, + "p234": 94, + "p226": 95, + "p260": 96, + "p245": 97, + "p241": 98, + "p303": 99, + "p265": 100, + "p306": 101, + "p237": 102, + "p249": 103, + "p256": 104, + "p302": 105, + "p364": 106, + "p225": 107, + "p362": 108 + }, + "piper_version": "1.0.0", + "language": { + "code": "en_GB", + "family": "en", + "region": "GB", + "name_native": "English", + "name_english": "English", + "country_english": "Great Britain" + }, + "dataset": "vctk" +} \ No newline at end of file diff --git a/models/piper/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json b/models/piper/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json new file mode 100644 index 0000000..049160c --- /dev/null +++ b/models/piper/en_US-hfc_female-medium/en_US-hfc_female-medium.onnx.json @@ -0,0 +1,508 @@ +{ + "dataset": "hfc_female", + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en-us" + }, + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + " ": [ + 3 + ], + "!": [ + 4 + ], + "\"": [ + 150 + ], + "#": [ + 149 + ], + "$": [ + 2 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "X": [ + 156 + ], + "^": [ + 1 + ], + "_": [ + 0 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "g": [ + 154 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʦ": [ + 155 + ], + "ʰ": [ + 145 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "ˤ": [ + 146 + ], + "̃": [ + 141 + ], + "̊": [ + 158 + ], + "̝": [ + 157 + ], + "̧": [ + 140 + ], + "̩": [ + 144 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ], + "β": [ + 125 + ], + "ε": [ + 147 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "↑": [ + 151 + ], + "↓": [ + 148 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0" +} diff --git a/models/piper/en_US-kristin-medium/en_US-kristin-medium.onnx.json b/models/piper/en_US-kristin-medium/en_US-kristin-medium.onnx.json new file mode 100644 index 0000000..e94be84 --- /dev/null +++ b/models/piper/en_US-kristin-medium/en_US-kristin-medium.onnx.json @@ -0,0 +1,502 @@ +{ + "dataset": "kristin", + "audio": { + "sample_rate": 22050, + "quality": "medium" + }, + "espeak": { + "voice": "en" + }, + "language": { + "code": "en_US", + "family": "en", + "region": "US", + "name_native": "English", + "name_english": "English", + "country_english": "United States" + }, + "inference": { + "noise_scale": 0.667, + "length_scale": 1, + "noise_w": 0.8 + }, + "phoneme_type": "espeak", + "phoneme_map": {}, + "phoneme_id_map": { + " ": [ + 3 + ], + "!": [ + 4 + ], + "\"": [ + 150 + ], + "#": [ + 149 + ], + "$": [ + 2 + ], + "'": [ + 5 + ], + "(": [ + 6 + ], + ")": [ + 7 + ], + ",": [ + 8 + ], + "-": [ + 9 + ], + ".": [ + 10 + ], + "0": [ + 130 + ], + "1": [ + 131 + ], + "2": [ + 132 + ], + "3": [ + 133 + ], + "4": [ + 134 + ], + "5": [ + 135 + ], + "6": [ + 136 + ], + "7": [ + 137 + ], + "8": [ + 138 + ], + "9": [ + 139 + ], + ":": [ + 11 + ], + ";": [ + 12 + ], + "?": [ + 13 + ], + "X": [ + 156 + ], + "^": [ + 1 + ], + "_": [ + 0 + ], + "a": [ + 14 + ], + "b": [ + 15 + ], + "c": [ + 16 + ], + "d": [ + 17 + ], + "e": [ + 18 + ], + "f": [ + 19 + ], + "g": [ + 154 + ], + "h": [ + 20 + ], + "i": [ + 21 + ], + "j": [ + 22 + ], + "k": [ + 23 + ], + "l": [ + 24 + ], + "m": [ + 25 + ], + "n": [ + 26 + ], + "o": [ + 27 + ], + "p": [ + 28 + ], + "q": [ + 29 + ], + "r": [ + 30 + ], + "s": [ + 31 + ], + "t": [ + 32 + ], + "u": [ + 33 + ], + "v": [ + 34 + ], + "w": [ + 35 + ], + "x": [ + 36 + ], + "y": [ + 37 + ], + "z": [ + 38 + ], + "æ": [ + 39 + ], + "ç": [ + 40 + ], + "ð": [ + 41 + ], + "ø": [ + 42 + ], + "ħ": [ + 43 + ], + "ŋ": [ + 44 + ], + "œ": [ + 45 + ], + "ǀ": [ + 46 + ], + "ǁ": [ + 47 + ], + "ǂ": [ + 48 + ], + "ǃ": [ + 49 + ], + "ɐ": [ + 50 + ], + "ɑ": [ + 51 + ], + "ɒ": [ + 52 + ], + "ɓ": [ + 53 + ], + "ɔ": [ + 54 + ], + "ɕ": [ + 55 + ], + "ɖ": [ + 56 + ], + "ɗ": [ + 57 + ], + "ɘ": [ + 58 + ], + "ə": [ + 59 + ], + "ɚ": [ + 60 + ], + "ɛ": [ + 61 + ], + "ɜ": [ + 62 + ], + "ɞ": [ + 63 + ], + "ɟ": [ + 64 + ], + "ɠ": [ + 65 + ], + "ɡ": [ + 66 + ], + "ɢ": [ + 67 + ], + "ɣ": [ + 68 + ], + "ɤ": [ + 69 + ], + "ɥ": [ + 70 + ], + "ɦ": [ + 71 + ], + "ɧ": [ + 72 + ], + "ɨ": [ + 73 + ], + "ɪ": [ + 74 + ], + "ɫ": [ + 75 + ], + "ɬ": [ + 76 + ], + "ɭ": [ + 77 + ], + "ɮ": [ + 78 + ], + "ɯ": [ + 79 + ], + "ɰ": [ + 80 + ], + "ɱ": [ + 81 + ], + "ɲ": [ + 82 + ], + "ɳ": [ + 83 + ], + "ɴ": [ + 84 + ], + "ɵ": [ + 85 + ], + "ɶ": [ + 86 + ], + "ɸ": [ + 87 + ], + "ɹ": [ + 88 + ], + "ɺ": [ + 89 + ], + "ɻ": [ + 90 + ], + "ɽ": [ + 91 + ], + "ɾ": [ + 92 + ], + "ʀ": [ + 93 + ], + "ʁ": [ + 94 + ], + "ʂ": [ + 95 + ], + "ʃ": [ + 96 + ], + "ʄ": [ + 97 + ], + "ʈ": [ + 98 + ], + "ʉ": [ + 99 + ], + "ʊ": [ + 100 + ], + "ʋ": [ + 101 + ], + "ʌ": [ + 102 + ], + "ʍ": [ + 103 + ], + "ʎ": [ + 104 + ], + "ʏ": [ + 105 + ], + "ʐ": [ + 106 + ], + "ʑ": [ + 107 + ], + "ʒ": [ + 108 + ], + "ʔ": [ + 109 + ], + "ʕ": [ + 110 + ], + "ʘ": [ + 111 + ], + "ʙ": [ + 112 + ], + "ʛ": [ + 113 + ], + "ʜ": [ + 114 + ], + "ʝ": [ + 115 + ], + "ʟ": [ + 116 + ], + "ʡ": [ + 117 + ], + "ʢ": [ + 118 + ], + "ʦ": [ + 155 + ], + "ʰ": [ + 145 + ], + "ʲ": [ + 119 + ], + "ˈ": [ + 120 + ], + "ˌ": [ + 121 + ], + "ː": [ + 122 + ], + "ˑ": [ + 123 + ], + "˞": [ + 124 + ], + "ˤ": [ + 146 + ], + "̃": [ + 141 + ], + "̧": [ + 140 + ], + "̩": [ + 144 + ], + "̪": [ + 142 + ], + "̯": [ + 143 + ], + "̺": [ + 152 + ], + "̻": [ + 153 + ], + "β": [ + 125 + ], + "ε": [ + 147 + ], + "θ": [ + 126 + ], + "χ": [ + 127 + ], + "ᵻ": [ + 128 + ], + "↑": [ + 151 + ], + "↓": [ + 148 + ], + "ⱱ": [ + 129 + ] + }, + "num_symbols": 256, + "num_speakers": 1, + "speaker_id_map": {}, + "piper_version": "1.0.0" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..724af38 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +# NovaAi – TTS-Engine-Hub +# requirements.txt +# Version: v0.0.1 + +fastapi +uvicorn +gunicorn +pydantic +pydantic-settings +ffmpeg-python +piper-tts + + +f5-tts +torch +torchaudio + +# Development & Testing +pytest-cov +pytest-asyncio + + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..c65620d --- /dev/null +++ b/run.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# NovaAi – TTS-Engine-Hub +# run.sh +# Version: v0.0.1 +# +# Activates venv and runs main.py via uvicorn (with reload for dev convenience). +# Author: Abby (ChatGPT) +# Date: 2025-07-23 +# Canvas: run.sh + +if [ ! -d ".venv" ]; then + echo "Virtual environment not found! Please run setup_env.sh first." + exit 1 +fi + +source .venv/bin/activate + +export PYTHONPATH=$(pwd) + +python main.py + diff --git a/setup_env.sh b/setup_env.sh new file mode 100644 index 0000000..c8c30dd --- /dev/null +++ b/setup_env.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# NovaAi – TTS-Engine-Hub +# setup_env.sh +# Version: v0.0.1 +# +# Deletes old venv (if exists), creates new one, and installs dependencies from requirements.txt. +# Author: Abby (ChatGPT) +# Date: 2025-07-23 +# Canvas: setup_env.sh + +set -e + +if [ -d ".venv" ]; then + echo "Removing existing venv..." + rm -rf .venv +fi + +echo "Creating new virtual environment..." +python3 -m venv .venv + +echo "Activating virtual environment..." +source .venv/bin/activate + +if [ ! -f requirements.txt ]; then + echo "requirements.txt not found!" + exit 1 +fi + +echo "Installing requirements..." +pip install --upgrade pip +pip install -r requirements.txt + +echo "Setup complete!" + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f1ee13b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,34 @@ +import pytest +from fastapi.testclient import TestClient +from main import create_app # Import the app factory function +from config import settings # Import settings to monkeypatch +import os + +@pytest.fixture +def tmp_audio_dir(tmp_path): + """Provides a temporary directory for audio caching for each test.""" + audio_dir = tmp_path / "test_audio_cache" + audio_dir.mkdir() + return audio_dir + +@pytest.fixture +def app_instance(tmp_audio_dir, monkeypatch): + """ + Provides a fresh FastAPI application instance for each test, + with its AUDIO_CACHE_DIR redirected to a temporary location. + """ + monkeypatch.setattr(settings, "AUDIO_CACHE_DIR", str(tmp_audio_dir)) + app = create_app() + return app + +@pytest.fixture +def app_client(app_instance): + """Provides a TestClient instance for the FastAPI application.""" + with TestClient(app_instance) as client: + yield client + +@pytest.fixture +def piper_engine(app_instance): + """Provides the PiperEngine instance from the registry of the fresh app instance.""" + # Access the engine registry from the app_instance + return app_instance.ENGINE_REGISTRY.get("piper") \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..925b775 --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,123 @@ +from fastapi.testclient import TestClient +import pytest +# from main import app, ENGINE_REGISTRY # No longer needed, using fixtures +import shutil +import asyncio + +TEST_TEXT = "Dies ist ein NovaAi Test." +# Ensure the engine being tested is in the default .env config +ENGINE = "piper" +MODEL = "de_DE-thorsten-high" +FORMAT = "ogg" + +# === Happy Path Tests === + +@pytest.mark.asyncio +async def test_tts_link_success(app_client): + """Tests successful synthesis returning a URL.""" + resp = app_client.post("/tts", json={ + "text": TEST_TEXT, + "engine": ENGINE, + "model": MODEL, + "format": FORMAT + }) + assert resp.status_code == 200 + data = resp.json() + assert "audio_url" in data + + # Also test the audio download endpoint + audio_url = data['audio_url'] + audio_resp = app_client.get(audio_url) + assert audio_resp.status_code == 200 + assert len(audio_resp.content) > 1000 + +@pytest.mark.asyncio +async def test_tts_base64_success(app_client): + """Tests successful synthesis returning base64 data.""" + resp = app_client.post("/tts?as=true", json={ + "text": TEST_TEXT, + "engine": ENGINE, + "model": MODEL, + "format": FORMAT + }) + assert resp.status_code == 200 + data = resp.json() + assert "audio_base64" in data + assert len(data["audio_base64"]) > 1000 + +@pytest.mark.asyncio +async def test_health_and_engines_endpoints(app_client): + """Tests the /health and /engines endpoints for correct responses.""" + resp_engines = app_client.get("/engines") + assert resp_engines.status_code == 200 + engines = resp_engines.json() + assert ENGINE in engines + + resp_health = app_client.get("/health") + assert resp_health.status_code == 200 + health = resp_health.json() + assert health["status"][ENGINE] == "ok" + +# === Failure Path Tests === + +@pytest.mark.asyncio +async def test_tts_fails_with_invalid_engine(app_client): + """Tests that a request with a non-existent engine fails with HTTP 404.""" + response = app_client.post("/tts", json={ + "text": TEST_TEXT, + "engine": "non_existent_engine", + "model": MODEL, + }) + assert response.status_code == 404 + assert "Engine 'non_existent_engine' not found" in response.json()["detail"] + +@pytest.mark.asyncio +async def test_tts_fails_with_invalid_model(app_client): + """Tests that a request with a non-existent model fails with HTTP 400.""" + response = app_client.post("/tts", json={ + "text": TEST_TEXT, + "engine": ENGINE, + "model": "non_existent_model", + }) + assert response.status_code == 400 + assert "Model 'non_existent_model' not found" in response.json()["detail"] + +@pytest.mark.asyncio +async def test_tts_fails_with_invalid_speaker(app_client, piper_engine): + """Tests that a request with a non-existent speaker fails with HTTP 400.""" + # This model has speakers, so requesting a non-existent one should fail. + # We need to pick a model that is known to have multiple speakers + # The default piper engine models should have this. + model_with_speakers = "en_US-kristin-medium" # Example model with speakers + + # First, check if the model is actually available to test against + if model_with_speakers not in piper_engine.list_models(): + pytest.skip(f"Model '{model_with_speakers}' not available for '{ENGINE}' to test against.") + + response = app_client.post("/tts", json={ + "text": "This is a test.", + "engine": ENGINE, + "model": model_with_speakers, + "speaker": "non_existent_speaker" + }) + assert response.status_code == 400 + assert "Speaker 'non_existent_speaker' not found" in response.json()["detail"] + +@pytest.mark.asyncio +async def test_tts_fails_with_unavailable_engine(app_client, piper_engine, monkeypatch): + """Tests that a request fails with HTTP 503 if an engine is unhealthy.""" + # Simulate the piper executable being not found by monkeypatching the specific instance + monkeypatch.setattr(piper_engine, "piper_executable", None) + + response = app_client.post("/tts", json={ + "text": TEST_TEXT, + "engine": ENGINE, + "model": MODEL, + }) + + assert response.status_code == 503 + assert "is not available" in response.json()["detail"] + + + + diff --git a/tests/test_f5_tts.py b/tests/test_f5_tts.py new file mode 100644 index 0000000..e823c32 --- /dev/null +++ b/tests/test_f5_tts.py @@ -0,0 +1,70 @@ +import unittest +import os +import shutil +import asyncio +from importlib.resources import files +from engines.f5_tts import F5TTSEngine + +class TestF5TTSEngine(unittest.TestCase): + async def asyncSetUp(self): + self.engine = F5TTSEngine() + self.voices_dir = "engines/f5-tts-voices" + self.test_speaker_name = "test_speaker" + self.test_speaker_wav = os.path.join(self.voices_dir, f"{self.test_speaker_name}.wav") + self.test_speaker_txt = os.path.join(self.voices_dir, f"{self.test_speaker_name}.txt") + + # Create a dummy speaker for testing + if not await asyncio.to_thread(os.path.exists, self.test_speaker_wav): + default_wav_path = str(files("f5_tts").joinpath("infer/examples/basic/basic_ref_en.wav")) + await asyncio.to_thread(shutil.copy, default_wav_path, self.test_speaker_wav) + if not await asyncio.to_thread(os.path.exists, self.test_speaker_txt): + await asyncio.to_thread(lambda: open(self.test_speaker_txt, "w").write("Some call me nature, others call me mother nature.")) + + # Reload speakers to include the new test speaker + self.engine._load_speakers() + + async def asyncTearDown(self): + # Clean up the dummy speaker files + if await asyncio.to_thread(os.path.exists, self.test_speaker_wav): + await asyncio.to_thread(os.remove, self.test_speaker_wav) + if await asyncio.to_thread(os.path.exists, self.test_speaker_txt): + await asyncio.to_thread(os.remove, self.test_speaker_txt) + + async def test_synthesize_default_wav(self): + if not self.engine.model: + self.skipTest("F5-TTS model not initialized.") + + text = "Hello, this is a test with the default voice." + audio_file = await self.engine.synthesize(text, fmt="wav") + + self.assertTrue(await asyncio.to_thread(os.path.exists, audio_file)) + self.assertTrue(await asyncio.to_thread(os.path.getsize, audio_file) > 0) + + await asyncio.to_thread(os.remove, audio_file) + + async def test_synthesize_custom_speaker_wav(self): + if not self.engine.model: + self.skipTest("F5-TTS model not initialized.") + + text = "Hello, this is a test with a custom voice." + audio_file = await self.engine.synthesize(text, speaker=self.test_speaker_name, fmt="wav") + + self.assertTrue(await asyncio.to_thread(os.path.exists, audio_file)) + self.assertTrue(await asyncio.to_thread(os.path.getsize, audio_file) > 0) + + await asyncio.to_thread(os.remove, audio_file) + + async def test_synthesize_ogg(self): + if not self.engine.model: + self.skipTest("F5-TTS model not initialized.") + + text = "Hello, this is a test in ogg format." + audio_file = await self.engine.synthesize(text, fmt="ogg") + + self.assertTrue(await asyncio.to_thread(os.path.exists, audio_file)) + self.assertTrue(await asyncio.to_thread(os.path.getsize, audio_file) > 0) + + await asyncio.to_thread(os.remove, audio_file) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/utils/audio.py b/utils/audio.py new file mode 100644 index 0000000..60437f1 --- /dev/null +++ b/utils/audio.py @@ -0,0 +1,76 @@ +""" +NovaAi – TTS-Engine-Hub +utils/audio.py +Version: v0.0.1 + +Description: + Audio processing utilities: concat, merging chunks, etc. + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: utils/audio.py +""" + +import tempfile +import os +import ffmpeg +import shutil + +def check_ffmpeg(): + """Check if ffmpeg is installed and available in the system's PATH.""" + return shutil.which("ffmpeg") is not None + +def concat_audio(files, fmt): + """ + Concatenate a list of audio files into a single file of the given format. + Supports: wav, ogg, mp3 + """ + if len(files) == 1: + # If there's only one file, just return it, no cleanup needed here. + return files[0] + + # Securely create a temporary file for the output + with tempfile.NamedTemporaryFile(suffix=f'.{fmt}', prefix="chunked_", delete=False) as temp_output_file: + output_file_path = temp_output_file.name + + try: + if fmt == "wav": + import wave + data = [] + params = None + for f in files: + with wave.open(f, 'rb') as wf: + if params is None: + params = wf.getparams() + data.append(wf.readframes(wf.getnframes())) + with wave.open(output_file_path, 'wb') as wf: + wf.setparams(params) + for d in data: + wf.writeframes(d) + else: + if not check_ffmpeg(): + raise RuntimeError("ffmpeg not found. Please install ffmpeg and ensure it is in your PATH.") + + with tempfile.NamedTemporaryFile("w", delete=False) as tf: + list_file_path = tf.name + for f in files: + tf.write(f"file '{os.path.abspath(f)}'\\n") + tf.flush() + + try: + ( + ffmpeg + .input(list_file_path, format='concat', safe=0) + .output(output_file_path, acodec='copy') + .run(overwrite_output=True, quiet=True) + ) + finally: + os.unlink(list_file_path) + + finally: + # Clean up the input chunk files + for f in files: + if os.path.exists(f): + os.remove(f) + + return output_file_path diff --git a/utils/cache.py b/utils/cache.py new file mode 100644 index 0000000..6b437ee --- /dev/null +++ b/utils/cache.py @@ -0,0 +1,22 @@ +""" +NovaAi – TTS-Engine-Hub +utils/cache.py +Version: v0.0.1 + +Description: + Caching utilities for TTS requests (cache key generation, etc). + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: utils/cache.py +""" + +import hashlib + +# Für die API: Wichtig! req muss mindestens die Felder .text, .engine, .model, .speaker, .format, .chunking haben. +def build_cache_key(req) -> str: + """ + Build a cache key from all relevant TTS request parameters. + """ + data = f"{req.text}|{req.engine}|{req.model}|{req.speaker}|{req.format}|{getattr(req, 'chunking', False)}" + return hashlib.sha256(data.encode()).hexdigest() diff --git a/utils/text.py b/utils/text.py new file mode 100644 index 0000000..36e5bad --- /dev/null +++ b/utils/text.py @@ -0,0 +1,30 @@ +""" +NovaAi – TTS-Engine-Hub +utils/text.py +Version: v0.0.1 + +Description: + Text processing utilities: chunking, splitting, etc. + +Author: Abby (ChatGPT) +Date: 2025-07-23 +Canvas: utils/text.py +""" + +def chunk_text(text, maxlen=250): + """ + Split text into chunks of roughly maxlen (split at sentence boundaries if possible). + """ + import re + sentences = re.split(r'([.!?]\s)', text) + chunks = [] + buf = "" + for s in sentences: + if len(buf) + len(s) > maxlen: + if buf: + chunks.append(buf.strip()) + buf = "" + buf += s + if buf.strip(): + chunks.append(buf.strip()) + return [c for c in chunks if c.strip()]