diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0ad4ec9 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Orpheus-FastAPI Configuration +# Copy this file to .env and customize as needed + +# Server connection settings +ORPHEUS_API_URL=http://127.0.0.1:1234/v1/completions +ORPHEUS_API_TIMEOUT=120 # You should scale this value based on max tokens and your inference speed + +# Generation parameters +ORPHEUS_MAX_TOKENS=8192 # If you want longer completions, increase this value +ORPHEUS_TEMPERATURE=0.6 +ORPHEUS_TOP_P=0.9 +# Repetition penalty is now hardcoded to 1.1 for stability (this is a model constraint) - this setting is no longer used +# ORPHEUS_REPETITION_PENALTY=1.1 +ORPHEUS_SAMPLE_RATE=24000 + +# Web UI settings (keep in mind that the web UI is not secure and should not be exposed to the internet) +ORPHEUS_PORT=5005 +ORPHEUS_HOST=0.0.0.0 diff --git a/README.md b/README.md index 973f0c6..0d9c376 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,15 @@ High-performance Text-to-Speech server with OpenAI-compatible API, 8 voices, emotion tags, and modern web UI. Optimized for RTX GPUs. +## Changelog + +**v1.1.0** (2025-03-23) +- ✨ Added long-form audio support with sentence-based batching and crossfade stitching +- 🔊 Improved short audio quality with optimized token buffer handling +- 🔄 Enhanced environment variable support with .env file loading (configurable via UI) +- 🖥️ Added automatic hardware detection and optimization for different GPUs +- 📊 Implemented detailed performance reporting for audio generation + [GitHub Repository](https://github.com/Lex-au/Orpheus-FastAPI) ## Voice Demos @@ -27,7 +36,11 @@ Listen to sample outputs with different voices and emotions: - **High Performance**: Optimized for RTX GPUs with parallel processing - **Multiple Voices**: 8 different voice options with different characteristics - **Emotion Tags**: Support for laughter, sighs, and other emotional expressions -- **Long-form Audio**: Efficient generation of extended audio content in a single request +- **Unlimited Audio Length**: Generate audio of any length through intelligent batching +- **Smooth Transitions**: Crossfaded audio segments for seamless listening experience +- **Web UI Configuration**: Configure all server settings directly from the interface +- **Dynamic Environment Variables**: Update API endpoint, timeouts, and model parameters without editing files +- **Server Restart**: Apply configuration changes with one-click server restart ## Project Structure @@ -187,6 +200,55 @@ This server works as a frontend that connects to an external LLM inference serve For best performance, adjust the API_URL in `tts_engine/inference.py` to point to your LLM inference server endpoint. +### Hardware Detection and Optimization + +The system features intelligent hardware detection that automatically optimizes performance based on your hardware capabilities: + +- **High-End GPU Mode** (dynamically detected based on capabilities): + - Triggered by either: 16GB+ VRAM, compute capability 8.0+, or 12GB+ VRAM with 7.0+ compute capability + - Advanced parallel processing with 4 workers + - Optimized batch sizes (32 tokens) + - High-throughput parallel file I/O + - Full hardware details displayed (name, VRAM, compute capability) + - GPU-specific optimizations automatically applied + +- **Standard GPU Mode** (other CUDA-capable GPUs): + - Efficient parallel processing + - GPU-optimized parameters + - CUDA acceleration where beneficial + - Detailed GPU specifications + +- **CPU Mode** (when no GPU is available): + - Conservative processing with 2 workers + - Optimized memory usage + - Smaller batch sizes (16 tokens) + - Sequential file I/O + - Detailed CPU cores, threads, and RAM information + +No manual configuration is needed - the system automatically detects hardware capabilities and adapts for optimal performance across different generations of GPUs and CPUs. + +### Token Processing Optimization + +The token processing system has been optimized with mathematically aligned parameters: +- Uses a context window of 49 tokens (7²) +- Processes in batches of 7 tokens (Orpheus model standard) +- This square relationship ensures complete token processing with no missed tokens +- Results in cleaner audio generation with proper token alignment +- Repetition penalty fixed at 1.1 for optimal quality generation (cannot be changed) + +### Long Text Processing + +The system features efficient batch processing for texts of any length: +- Automatically detects longer inputs (>1000 characters) +- Splits text at logical points to create manageable chunks +- Processes each chunk independently for reliability +- Combines audio segments with smooth 50ms crossfades +- Intelligently stitches segments in-memory for consistent output +- Handles texts of unlimited length with no truncation +- Provides detailed progress reporting for each batch + +**Note about long-form audio**: While the system now supports texts of unlimited length, there may be slight audio discontinuities between segments due to architectural constraints of the underlying model. The Orpheus model was designed for short to medium text segments, and our batching system works around this limitation by intelligently splitting and stitching content with minimal audible impact. + ### Integration with OpenWebUI You can easily integrate this TTS solution with [OpenWebUI](https://github.com/open-webui/open-webui) to add high-quality voice capabilities to your chatbot: @@ -194,7 +256,7 @@ You can easily integrate this TTS solution with [OpenWebUI](https://github.com/o 1. Start your Orpheus-FASTAPI server 2. In OpenWebUI, go to Admin Panel > Settings > Audio 3. Change TTS from Web API to OpenAI -4. Set APIBASE URL to your server address (e.g., `http://localhost:5005`) +4. Set APIBASE URL to your server address (e.g., `http://localhost:5005/v1`) 5. API Key can be set to "not-needed" 6. Set TTS Voice to one of the available voices: `tara`, `leah`, `jess`, `leo`, `dan`, `mia`, `zac`, or `zoe` 7. Set TTS Model to `tts-1` @@ -214,10 +276,22 @@ The inference server should be configured to expose an API endpoint that this Fa ### Environment Variables -You can configure the system by setting environment variables: +You can configure the system using environment variables or a `.env` file: - `ORPHEUS_API_URL`: URL of the LLM inference API (tts_engine/inference.py) - `ORPHEUS_API_TIMEOUT`: Timeout in seconds for API requests (default: 120) +- `ORPHEUS_MAX_TOKENS`: Maximum tokens to generate (default: 8192) +- `ORPHEUS_TEMPERATURE`: Temperature for generation (default: 0.6) +- `ORPHEUS_TOP_P`: Top-p sampling parameter (default: 0.9) +- `ORPHEUS_SAMPLE_RATE`: Audio sample rate in Hz (default: 24000) +- `ORPHEUS_PORT`: Web server port (default: 5005) +- `ORPHEUS_HOST`: Web server host (default: 0.0.0.0) + +The system now supports loading environment variables from a `.env` file in the project root, making it easier to configure without modifying system-wide environment settings. See `.env.example` for a template. + + + +Note: Repetition penalty is hardcoded to 1.1 and cannot be changed through environment variables as this is the only value that produces stable, high-quality output. Make sure the `ORPHEUS_API_URL` points to your running inference server. @@ -233,6 +307,27 @@ Make sure the `ORPHEUS_API_URL` points to your running inference server. To add new voices, update the `AVAILABLE_VOICES` list in `tts_engine/inference.py` and add corresponding descriptions in the HTML template. +## Using with llama.cpp + +When running the Orpheus model with llama.cpp, use these parameters to ensure optimal performance: + +```bash +./llama-server -m models/Orpheus-3b-FT-Q8_0.gguf \ + --ctx-size={{your ORPHEUS_MAX_TOKENS from .env}} \ + --n-predict={{your ORPHEUS_MAX_TOKENS from .env}} \ + --rope-scaling=linear +``` + +Important parameters: +- `--ctx-size`: Sets the context window size, should match your ORPHEUS_MAX_TOKENS setting +- `--n-predict`: Maximum tokens to generate, should match your ORPHEUS_MAX_TOKENS setting +- `--rope-scaling=linear`: Required for optimal positional encoding with the Orpheus model + +For extended audio generation (books, long narrations), you may want to increase your token limits: +1. Set ORPHEUS_MAX_TOKENS to 32768 or higher in your .env file (or via the Web UI) +2. Increase ORPHEUS_API_TIMEOUT to 1800 for longer processing times +3. Use the same values in your llama.cpp parameters (if you're using llama.cpp) + ## License This project is licensed under the Apache License 2.0 - see the LICENSE.txt file for details. diff --git a/app.py b/app.py index cd47368..549c651 100644 --- a/app.py +++ b/app.py @@ -4,14 +4,36 @@ import os import time +import asyncio from datetime import datetime from typing import List, Optional +from dotenv import load_dotenv + +# Function to ensure .env file exists +def ensure_env_file_exists(): + """Create a default .env file if one doesn't exist""" + if not os.path.exists(".env") and os.path.exists(".env.example"): + try: + # Copy .env.example to .env + with open(".env.example", "r") as example_file: + with open(".env", "w") as env_file: + env_file.write(example_file.read()) + print("✅ Created default configuration file at .env") + except Exception as e: + print(f"⚠️ Error creating default .env file: {e}") + +# Ensure .env file exists before loading environment variables +ensure_env_file_exists() + +# Load environment variables from .env file +load_dotenv(override=True) from fastapi import FastAPI, Request, Form, HTTPException, Depends from fastapi.responses import HTMLResponse, FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel +import json from tts_engine import generate_speech_from_api, AVAILABLE_VOICES, DEFAULT_VOICE @@ -22,6 +44,10 @@ app = FastAPI( version="1.0.0" ) +# We'll use FastAPI's built-in startup complete mechanism +# The log message "INFO: Application startup complete." indicates +# that the application is ready + # Ensure directories exist os.makedirs("outputs", exist_ok=True) os.makedirs("static", exist_ok=True) @@ -53,6 +79,9 @@ async def create_speech_api(request: SpeechRequest): """ Generate speech from text using the Orpheus TTS model. Compatible with OpenAI's /v1/audio/speech endpoint. + + For longer texts (>1000 characters), batched generation is used + to improve reliability and avoid truncation issues. """ if not request.input: raise HTTPException(status_code=400, detail="Missing input text") @@ -61,12 +90,19 @@ async def create_speech_api(request: SpeechRequest): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"outputs/{request.voice}_{timestamp}.wav" - # Generate speech + # Check if we should use batched generation + use_batching = len(request.input) > 1000 + if use_batching: + print(f"Using batched generation for long text ({len(request.input)} characters)") + + # Generate speech with automatic batching for long texts start = time.time() generate_speech_from_api( prompt=request.input, voice=request.voice, - output_file=output_path + output_file=output_path, + use_batching=use_batching, + max_batch_chars=1000 # Process in ~1000 character chunks (roughly 1 paragraph) ) end = time.time() generation_time = round(end - start, 2) @@ -95,9 +131,20 @@ async def speak(request: Request): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"outputs/{voice}_{timestamp}.wav" - # Generate speech + # Check if we should use batched generation for longer texts + use_batching = len(text) > 1000 + if use_batching: + print(f"Using batched generation for long text ({len(text)} characters)") + + # Generate speech with batching for longer texts start = time.time() - generate_speech_from_api(prompt=text, voice=voice, output_file=output_path) + generate_speech_from_api( + prompt=text, + voice=voice, + output_file=output_path, + use_batching=use_batching, + max_batch_chars=1000 + ) end = time.time() generation_time = round(end - start, 2) @@ -120,11 +167,99 @@ async def root(request: Request): @app.get("/web/", response_class=HTMLResponse) async def web_ui(request: Request): """Main web UI for TTS generation""" + # Get current config for the Web UI + config = get_current_config() return templates.TemplateResponse( "tts.html", - {"request": request, "voices": AVAILABLE_VOICES} + {"request": request, "voices": AVAILABLE_VOICES, "config": config} ) +@app.get("/get_config") +async def get_config(): + """Get current configuration from .env file or defaults""" + config = get_current_config() + return JSONResponse(content=config) + +@app.post("/save_config") +async def save_config(request: Request): + """Save configuration to .env file""" + data = await request.json() + + # Convert values to proper types + for key, value in data.items(): + if key in ["ORPHEUS_MAX_TOKENS", "ORPHEUS_API_TIMEOUT", "ORPHEUS_PORT", "ORPHEUS_SAMPLE_RATE"]: + try: + data[key] = str(int(value)) + except (ValueError, TypeError): + pass + elif key in ["ORPHEUS_TEMPERATURE", "ORPHEUS_TOP_P"]: # Removed ORPHEUS_REPETITION_PENALTY since it's hardcoded now + try: + data[key] = str(float(value)) + except (ValueError, TypeError): + pass + + # Write configuration to .env file + with open(".env", "w") as f: + for key, value in data.items(): + f.write(f"{key}={value}\n") + + return JSONResponse(content={"status": "ok", "message": "Configuration saved successfully. Restart server to apply changes."}) + +@app.post("/restart_server") +async def restart_server(): + """Restart the server by touching a file that triggers Uvicorn's reload""" + import threading + + def touch_restart_file(): + # Wait a moment to let the response get back to the client + time.sleep(0.5) + + # Create or update restart.flag file to trigger reload + restart_file = "restart.flag" + with open(restart_file, "w") as f: + f.write(str(time.time())) + + print("🔄 Restart flag created, server will reload momentarily...") + + # Start the touch operation in a separate thread + threading.Thread(target=touch_restart_file, daemon=True).start() + + # Return success response + return JSONResponse(content={"status": "ok", "message": "Server is restarting. Please wait a moment..."}) + +def get_current_config(): + """Read current configuration from .env.example and .env files""" + # Default config from .env.example + default_config = {} + if os.path.exists(".env.example"): + with open(".env.example", "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + default_config[key] = value + + # Current config from .env + current_config = {} + if os.path.exists(".env"): + with open(".env", "r") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#") and "=" in line: + key, value = line.split("=", 1) + current_config[key] = value + + # Merge configs, with current taking precedence + config = {**default_config, **current_config} + + # Add current environment variables + for key in config: + env_value = os.environ.get(key) + if env_value is not None: + config[key] = env_value + + return config + @app.post("/web/", response_class=HTMLResponse) async def generate_from_web( request: Request, @@ -145,9 +280,20 @@ async def generate_from_web( timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = f"outputs/{voice}_{timestamp}.wav" - # Generate speech + # Check if we should use batched generation for longer texts + use_batching = len(text) > 1000 + if use_batching: + print(f"Using batched generation for long text from web form ({len(text)} characters)") + + # Generate speech with batching for longer texts start = time.time() - generate_speech_from_api(prompt=text, voice=voice, output_file=output_path) + generate_speech_from_api( + prompt=text, + voice=voice, + output_file=output_path, + use_batching=use_batching, + max_batch_chars=1000 + ) end = time.time() generation_time = round(end - start, 2) @@ -166,5 +312,43 @@ async def generate_from_web( if __name__ == "__main__": import uvicorn - print("🔥 Starting Orpheus-FASTAPI Server (CUDA)") - uvicorn.run("app:app", host="0.0.0.0", port=5005, reload=True) + + # Check for required settings + required_settings = ["ORPHEUS_HOST", "ORPHEUS_PORT"] + missing_settings = [s for s in required_settings if s not in os.environ] + if missing_settings: + print(f"⚠️ Missing environment variable(s): {', '.join(missing_settings)}") + print(" Using fallback values for server startup.") + + # Get host and port from environment variables with better error handling + try: + host = os.environ.get("ORPHEUS_HOST") + if not host: + print("⚠️ ORPHEUS_HOST not set, using 0.0.0.0 as fallback") + host = "0.0.0.0" + except Exception: + print("⚠️ Error reading ORPHEUS_HOST, using 0.0.0.0 as fallback") + host = "0.0.0.0" + + try: + port = int(os.environ.get("ORPHEUS_PORT", "5005")) + except (ValueError, TypeError): + print("⚠️ Invalid ORPHEUS_PORT value, using 5005 as fallback") + port = 5005 + + print(f"🔥 Starting Orpheus-FASTAPI Server on {host}:{port}") + print(f"💬 Web UI available at http://{host if host != '0.0.0.0' else 'localhost'}:{port}") + print(f"📖 API docs available at http://{host if host != '0.0.0.0' else 'localhost'}:{port}/docs") + + # Read current API_URL for user information + api_url = os.environ.get("ORPHEUS_API_URL") + if not api_url: + print("⚠️ ORPHEUS_API_URL not set. Please configure in .env file before generating speech.") + else: + print(f"🔗 Using LLM inference server at: {api_url}") + + # Include restart.flag in the reload_dirs to monitor it for changes + extra_files = ["restart.flag"] if os.path.exists("restart.flag") else [] + + # Start with reload enabled to allow automatic restart when restart.flag changes + uvicorn.run("app:app", host=host, port=port, reload=True, reload_dirs=["."], reload_includes=["*.py", "*.html", "restart.flag"]) diff --git a/docs/ServerConfig.png b/docs/ServerConfig.png new file mode 100644 index 0000000..eff22ba Binary files /dev/null and b/docs/ServerConfig.png differ diff --git a/docs/terminal.png b/docs/terminal.png index d9c6f82..ddbdc85 100644 Binary files a/docs/terminal.png and b/docs/terminal.png differ diff --git a/requirements.txt b/requirements.txt index ed4ebef..3c32737 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,30 @@ -# Core dependencies -fastapi>=0.103.1 -uvicorn>=0.23.2 -jinja2>=3.1.2 -pydantic>=2.3.0 -numpy>=1.24.0 -requests>=2.31.0 -sounddevice>=0.4.6 -python-multipart>=0.0.6 +# Web Server Dependencies +fastapi==0.103.1 +uvicorn==0.23.2 +jinja2==3.1.2 +pydantic==2.3.0 +python-multipart==0.0.6 -# SNAC is required for audio generation from tokens -snac>=0.3.0 +# API and Communication +requests==2.31.0 +python-dotenv==1.0.0 -# PyTorch - Note: Install PyTorch with CUDA 12.4 support separately: -# pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 +# Audio Processing +numpy==1.24.0 +sounddevice==0.4.6 +snac==1.2.1 # Required for audio generation from tokens + +# System Utilities +psutil==5.9.0 + +# PyTorch - Install separately with CUDA support: +# On Windows/Linux: +# pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124 +# On macOS: +# pip3 install torch torchvision torchaudio + +# Optional Dependencies +# For MP3 conversion (not currently implemented) +# pydub==0.25.1 +# For better sentence splitting (potential future improvement) +# nltk==3.8.1 diff --git a/templates/tts.html b/templates/tts.html index c9ca439..3a5c6bf 100644 --- a/templates/tts.html +++ b/templates/tts.html @@ -144,6 +144,7 @@ name="text" id="text" rows="4" + maxlength="8192" class="block w-full rounded-md border-dark-600 bg-dark-700 text-white shadow-sm focus:border-primary-500 focus:ring-primary-500 focus:ring-offset-dark-800 sm:text-sm px-3 py-2" placeholder="Enter text to convert to speech..." required @@ -191,6 +192,8 @@ + +
These settings will be saved to a .env file. Restart the server to apply changes.
+ +
+