Files
voicebox/providers/pytorch-cpu/main.py
T
Jamie Pine 80689ad8ce Implement TTS provider management system and update release workflow
- Added support for TTS providers in the backend, including endpoints for listing, starting, stopping, and downloading providers.
- Enhanced the release workflow to build and upload TTS provider binaries for both Windows and Linux platforms.
- Updated the architecture documentation to reflect the new provider system and its benefits for modularity and user experience.
- Introduced a new `ProviderSettings` component in the frontend for managing provider configurations.
2026-01-31 03:05:50 -08:00

239 lines
5.8 KiB
Python

"""
Standalone TTS provider server for PyTorch CPU.
"""
import argparse
import asyncio
import base64
import io
import sys
from pathlib import Path
from typing import Optional
import numpy as np
import soundfile as sf
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
# Add parent directory to path to import backend modules
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "backend"))
from backend.backends.pytorch_backend import PyTorchTTSBackend
app = FastAPI(title="Voicebox TTS Provider - PyTorch CPU")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Global backend instance
_backend: Optional[PyTorchTTSBackend] = None
def get_backend() -> PyTorchTTSBackend:
"""Get or create backend instance."""
global _backend
if _backend is None:
_backend = PyTorchTTSBackend()
return _backend
@app.get("/tts/health")
async def health():
"""Health check endpoint."""
backend = get_backend()
backend_type = "pytorch-cpu"
model_size = None
if backend.is_loaded():
if hasattr(backend, '_current_model_size') and backend._current_model_size:
model_size = backend._current_model_size
device = backend.device if hasattr(backend, 'device') else "cpu"
return {
"status": "healthy",
"provider": backend_type,
"version": "1.0.0", # TODO: Get from version file
"model": model_size,
"device": device,
}
@app.get("/tts/status")
async def status():
"""Model status endpoint."""
backend = get_backend()
model_size = None
if backend.is_loaded():
if hasattr(backend, '_current_model_size') and backend._current_model_size:
model_size = backend._current_model_size
available_sizes = ["1.7B", "0.6B"]
gpu_available = False
vram_used_mb = None
try:
import torch
gpu_available = torch.cuda.is_available()
if gpu_available:
vram_used_mb = int(torch.cuda.memory_allocated() / 1024 / 1024)
except ImportError:
pass
return {
"model_loaded": backend.is_loaded(),
"model_size": model_size,
"available_sizes": available_sizes,
"gpu_available": gpu_available,
"vram_used_mb": vram_used_mb,
}
@app.post("/tts/generate")
async def generate(
text: str,
voice_prompt: dict,
language: str = "en",
seed: Optional[int] = None,
model_size: str = "1.7B",
):
"""
Generate speech from text.
Request body (JSON):
{
"text": "Hello world!",
"voice_prompt": {...},
"language": "en",
"seed": 12345,
"model_size": "1.7B"
}
"""
backend = get_backend()
# Load model if not loaded or different size
if not backend.is_loaded() or (
hasattr(backend, '_current_model_size') and
backend._current_model_size != model_size
):
await backend.load_model_async(model_size)
# Generate audio
audio, sample_rate = await backend.generate(
text=text,
voice_prompt=voice_prompt,
language=language,
seed=seed,
instruct=None, # TODO: Add instruct support
)
# Convert to base64
buffer = io.BytesIO()
sf.write(buffer, audio, sample_rate, format="WAV")
buffer.seek(0)
audio_bytes = buffer.read()
audio_b64 = base64.b64encode(audio_bytes).decode('utf-8')
# Calculate duration
duration = len(audio) / sample_rate
return {
"audio": audio_b64,
"sample_rate": sample_rate,
"duration": duration,
}
@app.post("/tts/create_voice_prompt")
async def create_voice_prompt(
audio: UploadFile = File(...),
reference_text: str = Form(...),
use_cache: bool = Form(True),
):
"""
Create voice prompt from reference audio.
Request (multipart/form-data):
- audio: Audio file
- reference_text: Transcript
- use_cache: Whether to use cached prompts (default: true)
"""
backend = get_backend()
# Save uploaded file temporarily
import tempfile
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
tmp_path = tmp_file.name
content = await audio.read()
tmp_file.write(content)
try:
# Create voice prompt
voice_prompt, was_cached = await backend.create_voice_prompt(
audio_path=tmp_path,
reference_text=reference_text,
use_cache=use_cache,
)
return {
"voice_prompt": voice_prompt,
"was_cached": was_cached,
}
finally:
# Clean up temp file
Path(tmp_path).unlink(missing_ok=True)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(description="Voicebox TTS Provider - PyTorch CPU")
parser.add_argument(
"--port",
type=int,
default=0, # 0 means random port
help="Port to bind to",
)
parser.add_argument(
"--data-dir",
type=str,
default=None,
help="Data directory for models and cache",
)
args = parser.parse_args()
# Set data directory if provided
if args.data_dir:
from backend import config
config.set_data_dir(args.data_dir)
# Determine port
port = args.port
if port == 0:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('', 0))
port = s.getsockname()[1]
print(f"Starting TTS Provider (PyTorch CPU) on port {port}")
uvicorn.run(
app,
host="127.0.0.1",
port=port,
log_level="info",
)
if __name__ == "__main__":
main()