Compare commits
2 Commits
c91a9598b1
...
2af705ca43
Author | SHA1 | Date |
---|---|---|
|
2af705ca43 | |
|
758aa02053 |
|
@ -0,0 +1,27 @@
|
||||||
|
# Chatterbox TTS Application Configuration
|
||||||
|
# Copy this file to .env and adjust values for your environment
|
||||||
|
|
||||||
|
# Project paths (adjust these for your system)
|
||||||
|
PROJECT_ROOT=/path/to/your/chatterbox-ui
|
||||||
|
SPEAKER_SAMPLES_DIR=${PROJECT_ROOT}/speaker_data/speaker_samples
|
||||||
|
TTS_TEMP_OUTPUT_DIR=${PROJECT_ROOT}/tts_temp_outputs
|
||||||
|
DIALOG_GENERATED_DIR=${PROJECT_ROOT}/backend/tts_generated_dialogs
|
||||||
|
|
||||||
|
# Backend server configuration
|
||||||
|
BACKEND_HOST=0.0.0.0
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
BACKEND_RELOAD=true
|
||||||
|
|
||||||
|
# Frontend development server configuration
|
||||||
|
FRONTEND_HOST=127.0.0.1
|
||||||
|
FRONTEND_PORT=8001
|
||||||
|
|
||||||
|
# API URLs (usually derived from backend configuration)
|
||||||
|
API_BASE_URL=http://localhost:8000
|
||||||
|
API_BASE_URL_WITH_PREFIX=http://localhost:8000/api
|
||||||
|
|
||||||
|
# CORS configuration (comma-separated list)
|
||||||
|
CORS_ORIGINS=http://localhost:8001,http://127.0.0.1:8001,http://localhost:3000,http://127.0.0.1:3000
|
||||||
|
|
||||||
|
# Device configuration for TTS model (auto, cpu, cuda, mps)
|
||||||
|
DEVICE=auto
|
|
@ -9,5 +9,16 @@ dialog_output/
|
||||||
__pycache__
|
__pycache__
|
||||||
projects/
|
projects/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
backend/.env
|
||||||
|
frontend/.env
|
||||||
|
|
||||||
|
# Generated directories
|
||||||
|
tts_temp_outputs/
|
||||||
|
backend/tts_generated_dialogs/
|
||||||
|
|
||||||
# Node.js dependencies
|
# Node.js dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Agent Guidelines for Chatterbox-UI
|
||||||
|
|
||||||
|
## Build/Test Commands
|
||||||
|
```bash
|
||||||
|
# Backend (FastAPI)
|
||||||
|
pip install -r backend/requirements.txt
|
||||||
|
uvicorn backend.app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
python backend/run_api_test.py # Run all backend tests
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
npm test # Run all frontend tests
|
||||||
|
npx jest frontend/tests/api.test.js # Run single test file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### Python
|
||||||
|
- Use type hints (from typing import Optional, List, etc.)
|
||||||
|
- Exception handling: Use try/except with specific exceptions
|
||||||
|
- Async/await for FastAPI endpoints and services
|
||||||
|
- Docstrings for functions and classes
|
||||||
|
- Use pathlib.Path for file operations
|
||||||
|
- Organize code into routers, models, and services
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
- ES6 modules with import/export
|
||||||
|
- JSDoc comments for functions
|
||||||
|
- Async/await for API calls
|
||||||
|
- Proper error handling with detailed messages
|
||||||
|
- Descriptive variable and function names
|
||||||
|
- Consistent error handling pattern in API calls
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Backend: Raise specific exceptions, use try/except/finally
|
||||||
|
- Frontend: Use try/catch with detailed error messages
|
||||||
|
- Always include error details in API responses
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- Python: snake_case for variables/functions, PascalCase for classes
|
||||||
|
- JavaScript: camelCase for variables/functions
|
||||||
|
- Descriptive, intention-revealing names
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Environment Configuration Guide
|
||||||
|
|
||||||
|
This guide explains how to configure the Chatterbox TTS application for different environments using environment variables.
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
1. **Run the setup script:**
|
||||||
|
```bash
|
||||||
|
python setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Install backend dependencies:**
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the servers:**
|
||||||
|
```bash
|
||||||
|
# Terminal 1 - Backend
|
||||||
|
cd backend
|
||||||
|
python start_server.py
|
||||||
|
|
||||||
|
# Terminal 2 - Frontend
|
||||||
|
cd frontend
|
||||||
|
python start_dev_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Open the application:**
|
||||||
|
Open http://127.0.0.1:8001 in your browser
|
||||||
|
|
||||||
|
## Manual Configuration
|
||||||
|
|
||||||
|
### Environment Files
|
||||||
|
|
||||||
|
The application uses environment variables for configuration. Three `.env` files control different aspects:
|
||||||
|
|
||||||
|
- **Root `.env`**: Global configuration
|
||||||
|
- **`backend/.env`**: Backend-specific settings
|
||||||
|
- **`frontend/.env`**: Frontend-specific settings
|
||||||
|
|
||||||
|
### Key Configuration Options
|
||||||
|
|
||||||
|
#### Paths
|
||||||
|
- `PROJECT_ROOT`: Base directory for the project
|
||||||
|
- `SPEAKER_SAMPLES_DIR`: Directory containing speaker audio samples
|
||||||
|
- `TTS_TEMP_OUTPUT_DIR`: Temporary directory for TTS processing
|
||||||
|
- `DIALOG_GENERATED_DIR`: Directory for generated dialog audio files
|
||||||
|
|
||||||
|
#### Server Configuration
|
||||||
|
- `HOST`: Backend server host (default: 0.0.0.0)
|
||||||
|
- `PORT`: Backend server port (default: 8000)
|
||||||
|
- `RELOAD`: Enable auto-reload for development (default: true)
|
||||||
|
|
||||||
|
#### Frontend Configuration
|
||||||
|
- `VITE_API_BASE_URL`: Backend API base URL
|
||||||
|
- `VITE_DEV_SERVER_PORT`: Frontend development server port
|
||||||
|
- `VITE_DEV_SERVER_HOST`: Frontend development server host
|
||||||
|
|
||||||
|
#### CORS Configuration
|
||||||
|
- `CORS_ORIGINS`: Comma-separated list of allowed origins
|
||||||
|
|
||||||
|
#### Device Configuration
|
||||||
|
- `DEVICE`: Device for TTS model (auto, cpu, cuda, mps)
|
||||||
|
|
||||||
|
## Example Configurations
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
PROJECT_ROOT=/Users/yourname/chatterbox-ui
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
FRONTEND_PORT=8001
|
||||||
|
DEVICE=auto
|
||||||
|
CORS_ORIGINS=http://localhost:8001,http://127.0.0.1:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Environment
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
PROJECT_ROOT=/opt/chatterbox-ui
|
||||||
|
BACKEND_HOST=0.0.0.0
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
FRONTEND_PORT=3000
|
||||||
|
DEVICE=cuda
|
||||||
|
CORS_ORIGINS=https://yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Environment
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
PROJECT_ROOT=/app
|
||||||
|
BACKEND_HOST=0.0.0.0
|
||||||
|
BACKEND_PORT=8000
|
||||||
|
DEVICE=cpu
|
||||||
|
CORS_ORIGINS=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Permission Errors**: Ensure the `PROJECT_ROOT` directory is writable
|
||||||
|
2. **CORS Errors**: Check that your frontend URL is in `CORS_ORIGINS`
|
||||||
|
3. **Model Loading Errors**: Verify `DEVICE` setting matches your hardware
|
||||||
|
4. **Path Errors**: Ensure all path variables point to existing, accessible directories
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
Enable debug logging by setting:
|
||||||
|
```bash
|
||||||
|
export PYTHONPATH="${PYTHONPATH}:$(pwd)"
|
||||||
|
export DEBUG=1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resetting Configuration
|
||||||
|
|
||||||
|
To reset to defaults:
|
||||||
|
```bash
|
||||||
|
rm .env backend/.env frontend/.env
|
||||||
|
python setup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
chatterbox-ui/
|
||||||
|
├── .env # Global configuration
|
||||||
|
├── .env.example # Template for global config
|
||||||
|
├── setup.py # Automated setup script
|
||||||
|
├── backend/
|
||||||
|
│ ├── .env # Backend configuration
|
||||||
|
│ ├── .env.example # Template for backend config
|
||||||
|
│ ├── start_server.py # Backend startup script
|
||||||
|
│ └── app/
|
||||||
|
│ └── config.py # Configuration loader
|
||||||
|
├── frontend/
|
||||||
|
│ ├── .env # Frontend configuration
|
||||||
|
│ ├── .env.example # Template for frontend config
|
||||||
|
│ ├── start_dev_server.py # Frontend dev server
|
||||||
|
│ └── js/
|
||||||
|
│ └── config.js # Frontend configuration loader
|
||||||
|
└── speaker_data/
|
||||||
|
└── speaker_samples/ # Speaker audio files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
- Never commit `.env` files to version control
|
||||||
|
- Use strong, unique values for production
|
||||||
|
- Restrict CORS origins in production
|
||||||
|
- Use HTTPS in production environments
|
||||||
|
- Regularly update dependencies
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Backend Configuration
|
||||||
|
# Copy this file to .env and adjust values as needed
|
||||||
|
|
||||||
|
# Project paths
|
||||||
|
PROJECT_ROOT=/Users/stwhite/CODE/chatterbox-ui
|
||||||
|
SPEAKER_SAMPLES_DIR=${PROJECT_ROOT}/speaker_data/speaker_samples
|
||||||
|
TTS_TEMP_OUTPUT_DIR=${PROJECT_ROOT}/tts_temp_outputs
|
||||||
|
DIALOG_GENERATED_DIR=${PROJECT_ROOT}/backend/tts_generated_dialogs
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=8000
|
||||||
|
RELOAD=true
|
||||||
|
|
||||||
|
# CORS configuration
|
||||||
|
CORS_ORIGINS=http://localhost:8001,http://127.0.0.1:8001,http://localhost:3000,http://127.0.0.1:3000
|
||||||
|
|
||||||
|
# Device configuration (auto, cpu, cuda, mps)
|
||||||
|
DEVICE=auto
|
|
@ -15,20 +15,45 @@ This directory contains the FastAPI backend for the Chatterbox TTS application.
|
||||||
|
|
||||||
## Setup & Running
|
## Setup & Running
|
||||||
|
|
||||||
It is assumed you have a Python virtual environment at the project root (e.g., `.venv`).
|
### Prerequisites
|
||||||
|
- Python 3.8 or higher
|
||||||
|
- A Python virtual environment (recommended)
|
||||||
|
|
||||||
1. Navigate to the **project root** directory (e.g., `/Volumes/SAM2/CODE/chatterbox-test`).
|
### Installation
|
||||||
2. Activate the existing Python virtual environment:
|
|
||||||
```bash
|
1. **Navigate to the backend directory**:
|
||||||
source .venv/bin/activate # On macOS/Linux
|
```bash
|
||||||
# .\.venv\Scripts\activate # On Windows
|
cd /path/to/chatterbox-ui/backend
|
||||||
```
|
```
|
||||||
3. Install dependencies (ensure your terminal is in the **project root**):
|
|
||||||
```bash
|
2. **Set up a virtual environment** (if not already created):
|
||||||
pip install -r backend/requirements.txt
|
```bash
|
||||||
```
|
python -m venv .venv
|
||||||
4. Run the development server (ensure your terminal is in the **project root**):
|
source .venv/bin/activate # On macOS/Linux
|
||||||
```bash
|
# .\.venv\Scripts\activate # On Windows
|
||||||
uvicorn backend.app.main:app --reload --host 0.0.0.0 --port 8000
|
```
|
||||||
```
|
|
||||||
The API should then be accessible at `http://127.0.0.1:8000`.
|
3. **Install dependencies**:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running the Development Server
|
||||||
|
|
||||||
|
From the `backend` directory, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing the API
|
||||||
|
|
||||||
|
Once running, you can access:
|
||||||
|
- API documentation (Swagger UI): `http://127.0.0.1:8000/docs`
|
||||||
|
- Alternative API docs (ReDoc): `http://127.0.0.1:8000/redoc`
|
||||||
|
- API root: `http://127.0.0.1:8000/`
|
||||||
|
|
||||||
|
### Development Notes
|
||||||
|
- The `--reload` flag enables auto-reload on code changes
|
||||||
|
- The server will be accessible on all network interfaces with `--host 0.0.0.0`
|
||||||
|
- Default port is 8000, but you can change it with `--port <port_number>`
|
||||||
|
|
|
@ -1,21 +1,41 @@
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Determine PROJECT_ROOT dynamically.
|
# Load environment variables from .env file
|
||||||
# If config.py is at /Volumes/SAM2/CODE/chatterbox-test/backend/app/config.py
|
load_dotenv()
|
||||||
# then PROJECT_ROOT (/Volumes/SAM2/CODE/chatterbox-test) is 2 levels up.
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
|
|
||||||
# Speaker data paths
|
# Project root - can be overridden by environment variable
|
||||||
SPEAKER_DATA_BASE_DIR = PROJECT_ROOT / "speaker_data"
|
PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT", Path(__file__).parent.parent.parent)).resolve()
|
||||||
SPEAKER_SAMPLES_DIR = SPEAKER_DATA_BASE_DIR / "speaker_samples"
|
|
||||||
SPEAKERS_YAML_FILE = SPEAKER_DATA_BASE_DIR / "speakers.yaml"
|
# Directory paths
|
||||||
|
SPEAKER_DATA_BASE_DIR = Path(os.getenv("SPEAKER_DATA_BASE_DIR", str(PROJECT_ROOT / "speaker_data")))
|
||||||
|
SPEAKER_SAMPLES_DIR = Path(os.getenv("SPEAKER_SAMPLES_DIR", str(SPEAKER_DATA_BASE_DIR / "speaker_samples")))
|
||||||
|
SPEAKERS_YAML_FILE = Path(os.getenv("SPEAKERS_YAML_FILE", str(SPEAKER_DATA_BASE_DIR / "speakers.yaml")))
|
||||||
|
|
||||||
# TTS temporary output path (used by DialogProcessorService)
|
# TTS temporary output path (used by DialogProcessorService)
|
||||||
TTS_TEMP_OUTPUT_DIR = PROJECT_ROOT / "tts_temp_outputs"
|
TTS_TEMP_OUTPUT_DIR = Path(os.getenv("TTS_TEMP_OUTPUT_DIR", str(PROJECT_ROOT / "tts_temp_outputs")))
|
||||||
|
|
||||||
# Final dialog output path (used by Dialog router and served by main app)
|
# Final dialog output path (used by Dialog router and served by main app)
|
||||||
# These are stored within the 'backend' directory to be easily servable.
|
# These are stored within the 'backend' directory to be easily servable.
|
||||||
DIALOG_OUTPUT_PARENT_DIR = PROJECT_ROOT / "backend"
|
DIALOG_OUTPUT_PARENT_DIR = PROJECT_ROOT / "backend"
|
||||||
DIALOG_GENERATED_DIR = DIALOG_OUTPUT_PARENT_DIR / "tts_generated_dialogs"
|
DIALOG_GENERATED_DIR = Path(os.getenv("DIALOG_GENERATED_DIR", str(DIALOG_OUTPUT_PARENT_DIR / "tts_generated_dialogs")))
|
||||||
|
|
||||||
# Alias for clarity and backward compatibility
|
# Alias for clarity and backward compatibility
|
||||||
DIALOG_OUTPUT_DIR = DIALOG_GENERATED_DIR
|
DIALOG_OUTPUT_DIR = DIALOG_GENERATED_DIR
|
||||||
|
|
||||||
|
# Server configuration
|
||||||
|
HOST = os.getenv("HOST", "0.0.0.0")
|
||||||
|
PORT = int(os.getenv("PORT", "8000"))
|
||||||
|
RELOAD = os.getenv("RELOAD", "true").lower() == "true"
|
||||||
|
|
||||||
|
# CORS configuration
|
||||||
|
CORS_ORIGINS = [origin.strip() for origin in os.getenv("CORS_ORIGINS", "http://localhost:8001,http://127.0.0.1:8001").split(",")]
|
||||||
|
|
||||||
|
# Device configuration
|
||||||
|
DEVICE = os.getenv("DEVICE", "auto")
|
||||||
|
|
||||||
|
# Ensure directories exist
|
||||||
|
SPEAKER_SAMPLES_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
TTS_TEMP_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
DIALOG_GENERATED_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
|
@ -12,18 +12,15 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
|
|
||||||
# CORS Middleware configuration
|
# CORS Middleware configuration
|
||||||
origins = [
|
# For development, we'll allow all origins
|
||||||
"http://localhost:8001",
|
# In production, you should restrict this to specific origins
|
||||||
"http://127.0.0.1:8001",
|
|
||||||
# Add other origins if needed, e.g., your deployed frontend URL
|
|
||||||
]
|
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=config.CORS_ORIGINS,
|
||||||
allow_credentials=True,
|
allow_credentials=False,
|
||||||
allow_methods=["*"], # Allows all methods
|
allow_methods=["*"],
|
||||||
allow_headers=["*"], # Allows all headers
|
allow_headers=["*"],
|
||||||
|
expose_headers=["*"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
|
|
|
@ -78,22 +78,55 @@ async def generate_line(
|
||||||
temperature=speech.temperature
|
temperature=speech.temperature
|
||||||
)
|
)
|
||||||
audio_url = f"/generated_audio/{out_path.name}"
|
audio_url = f"/generated_audio/{out_path.name}"
|
||||||
|
return {"audio_url": audio_url}
|
||||||
elif item.get("type") == "silence":
|
elif item.get("type") == "silence":
|
||||||
silence = SilenceItem(**item)
|
silence = SilenceItem(**item)
|
||||||
filename = f"silence_{uuid.uuid4().hex}.wav"
|
filename = f"silence_{uuid.uuid4().hex}.wav"
|
||||||
out_path = Path(config.DIALOG_GENERATED_DIR) / filename
|
out_dir = Path(config.DIALOG_GENERATED_DIR)
|
||||||
# Generate silence tensor and save as WAV
|
out_dir.mkdir(parents=True, exist_ok=True) # Ensure output directory exists
|
||||||
silence_tensor = audio_manipulator._create_silence(silence.duration)
|
out_path = out_dir / filename
|
||||||
import torchaudio
|
|
||||||
torchaudio.save(str(out_path), silence_tensor, audio_manipulator.sample_rate)
|
try:
|
||||||
audio_url = f"/generated_audio/{filename}"
|
# Generate silence
|
||||||
|
silence_tensor = audio_manipulator.generate_silence(silence.duration)
|
||||||
|
import torchaudio
|
||||||
|
torchaudio.save(str(out_path), silence_tensor, audio_manipulator.sample_rate)
|
||||||
|
|
||||||
|
if not out_path.exists() or out_path.stat().st_size == 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Failed to generate silence. Output file not created: {out_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
audio_url = f"/generated_audio/{filename}"
|
||||||
|
return {"audio_url": audio_url}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
raise e
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"Error generating silence: {str(e)}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=400, detail="Unknown dialog item type.")
|
raise HTTPException(
|
||||||
return {"audio_url": audio_url}
|
status_code=400,
|
||||||
|
detail=f"Unknown dialog item type: {item.get('type')}. Expected 'speech' or 'silence'."
|
||||||
|
)
|
||||||
|
|
||||||
|
except HTTPException as he:
|
||||||
|
# Re-raise HTTP exceptions as-is
|
||||||
|
raise he
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
tb = traceback.format_exc()
|
tb = traceback.format_exc()
|
||||||
raise HTTPException(status_code=500, detail=f"Exception: {str(e)}\nTraceback:\n{tb}")
|
error_detail = f"Unexpected error: {str(e)}\n\nTraceback:\n{tb}"
|
||||||
|
print(error_detail) # Log to console for debugging
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=error_detail
|
||||||
|
)
|
||||||
|
|
||||||
async def manage_tts_model_lifecycle(tts_service: TTSService, task_function, *args, **kwargs):
|
async def manage_tts_model_lifecycle(tts_service: TTSService, task_function, *args, **kwargs):
|
||||||
"""Loads TTS model, executes task, then unloads model."""
|
"""Loads TTS model, executes task, then unloads model."""
|
||||||
|
|
|
@ -4,9 +4,41 @@ from typing import Optional
|
||||||
from chatterbox.tts import ChatterboxTTS
|
from chatterbox.tts import ChatterboxTTS
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import gc # Garbage collector for memory management
|
import gc # Garbage collector for memory management
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
# Define a directory for TTS model outputs, could be temporary or configurable
|
# Import configuration
|
||||||
TTS_OUTPUT_DIR = Path("/Volumes/SAM2/CODE/chatterbox-test/tts_outputs") # Example path
|
from app.config import TTS_TEMP_OUTPUT_DIR, SPEAKER_SAMPLES_DIR
|
||||||
|
|
||||||
|
# Use configuration for TTS output directory
|
||||||
|
TTS_OUTPUT_DIR = TTS_TEMP_OUTPUT_DIR
|
||||||
|
|
||||||
|
def safe_load_chatterbox_tts(device):
|
||||||
|
"""
|
||||||
|
Safely load ChatterboxTTS model with device mapping to handle CUDA->MPS/CPU conversion.
|
||||||
|
This patches torch.load temporarily to map CUDA tensors to the appropriate device.
|
||||||
|
"""
|
||||||
|
@contextmanager
|
||||||
|
def patch_torch_load(target_device):
|
||||||
|
original_load = torch.load
|
||||||
|
|
||||||
|
def patched_load(*args, **kwargs):
|
||||||
|
# Add map_location to handle device mapping
|
||||||
|
if 'map_location' not in kwargs:
|
||||||
|
if target_device == "mps" and torch.backends.mps.is_available():
|
||||||
|
kwargs['map_location'] = torch.device('mps')
|
||||||
|
else:
|
||||||
|
kwargs['map_location'] = torch.device('cpu')
|
||||||
|
return original_load(*args, **kwargs)
|
||||||
|
|
||||||
|
torch.load = patched_load
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
torch.load = original_load
|
||||||
|
|
||||||
|
with patch_torch_load(device):
|
||||||
|
return ChatterboxTTS.from_pretrained(device=device)
|
||||||
|
|
||||||
class TTSService:
|
class TTSService:
|
||||||
def __init__(self, device: str = "mps"): # Default to MPS for Macs, can be "cpu" or "cuda"
|
def __init__(self, device: str = "mps"): # Default to MPS for Macs, can be "cpu" or "cuda"
|
||||||
|
@ -23,7 +55,7 @@ class TTSService:
|
||||||
if self.model is None:
|
if self.model is None:
|
||||||
print(f"Loading ChatterboxTTS model to device: {self.device}...")
|
print(f"Loading ChatterboxTTS model to device: {self.device}...")
|
||||||
try:
|
try:
|
||||||
self.model = ChatterboxTTS.from_pretrained(device=self.device)
|
self.model = safe_load_chatterbox_tts(self.device)
|
||||||
print("ChatterboxTTS model loaded successfully.")
|
print("ChatterboxTTS model loaded successfully.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading ChatterboxTTS model: {e}")
|
print(f"Error loading ChatterboxTTS model: {e}")
|
||||||
|
@ -105,7 +137,7 @@ if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
tts_service.load_model()
|
tts_service.load_model()
|
||||||
|
|
||||||
dummy_speaker_root = Path("/Volumes/SAM2/CODE/chatterbox-test/speaker_data/speaker_samples")
|
dummy_speaker_root = SPEAKER_SAMPLES_DIR
|
||||||
dummy_speaker_root.mkdir(parents=True, exist_ok=True)
|
dummy_speaker_root.mkdir(parents=True, exist_ok=True)
|
||||||
dummy_sample_file = dummy_speaker_root / "dummy_speaker_test.wav"
|
dummy_sample_file = dummy_speaker_root / "dummy_speaker_test.wav"
|
||||||
import os # Added for os.remove
|
import os # Added for os.remove
|
||||||
|
|
|
@ -5,3 +5,4 @@ PyYAML
|
||||||
torch
|
torch
|
||||||
torchaudio
|
torchaudio
|
||||||
chatterbox-tts
|
chatterbox-tts
|
||||||
|
python-dotenv
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Backend server startup script that uses environment variables from config.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from app import config
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Starting Chatterbox TTS Backend Server...")
|
||||||
|
print(f"Host: {config.HOST}")
|
||||||
|
print(f"Port: {config.PORT}")
|
||||||
|
print(f"Reload: {config.RELOAD}")
|
||||||
|
print(f"CORS Origins: {config.CORS_ORIGINS}")
|
||||||
|
print(f"Project Root: {config.PROJECT_ROOT}")
|
||||||
|
print(f"Device: {config.DEVICE}")
|
||||||
|
|
||||||
|
uvicorn.run(
|
||||||
|
"app.main:app",
|
||||||
|
host=config.HOST,
|
||||||
|
port=config.PORT,
|
||||||
|
reload=config.RELOAD
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Frontend Configuration
|
||||||
|
# Copy this file to .env and adjust values as needed
|
||||||
|
|
||||||
|
# Backend API configuration
|
||||||
|
VITE_API_BASE_URL=http://localhost:8000
|
||||||
|
VITE_API_BASE_URL_WITH_PREFIX=http://localhost:8000/api
|
||||||
|
|
||||||
|
# Development server configuration
|
||||||
|
VITE_DEV_SERVER_PORT=8001
|
||||||
|
VITE_DEV_SERVER_HOST=127.0.0.1
|
|
@ -1,6 +1,8 @@
|
||||||
// frontend/js/api.js
|
// frontend/js/api.js
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:8000/api'; // Assuming backend runs on port 8000
|
import { API_BASE_URL_WITH_PREFIX } from './config.js';
|
||||||
|
|
||||||
|
const API_BASE_URL = API_BASE_URL_WITH_PREFIX;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the list of available speakers.
|
* Fetches the list of available speakers.
|
||||||
|
@ -107,18 +109,33 @@ export async function deleteSpeaker(speakerId) {
|
||||||
* @throws {Error} If the network response is not ok.
|
* @throws {Error} If the network response is not ok.
|
||||||
*/
|
*/
|
||||||
export async function generateLine(line) {
|
export async function generateLine(line) {
|
||||||
const response = await fetch(`${API_BASE_URL}/dialog/generate_line/`, {
|
console.log('generateLine called with:', line);
|
||||||
|
const response = await fetch(`${API_BASE_URL}/dialog/generate_line`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(line),
|
body: JSON.stringify(line),
|
||||||
});
|
});
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response headers:', [...response.headers.entries()]);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
const errorData = await response.json().catch(() => ({ message: response.statusText }));
|
||||||
throw new Error(`Failed to generate line audio: ${errorData.detail || errorData.message || response.statusText}`);
|
throw new Error(`Failed to generate line audio: ${errorData.detail || errorData.message || response.statusText}`);
|
||||||
}
|
}
|
||||||
return response.json();
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
console.log('Raw response text:', responseText);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(responseText);
|
||||||
|
console.log('Parsed JSON:', jsonData);
|
||||||
|
return jsonData;
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('JSON parse error:', parseError);
|
||||||
|
throw new Error(`Invalid JSON response: ${responseText}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,7 +154,7 @@ export async function generateLine(line) {
|
||||||
* @throws {Error} If the network response is not ok.
|
* @throws {Error} If the network response is not ok.
|
||||||
*/
|
*/
|
||||||
export async function generateDialog(dialogPayload) {
|
export async function generateDialog(dialogPayload) {
|
||||||
const response = await fetch(`${API_BASE_URL}/dialog/generate/`, {
|
const response = await fetch(`${API_BASE_URL}/dialog/generate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js';
|
import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js';
|
||||||
|
import { API_BASE_URL, API_BASE_URL_FOR_FILES } from './config.js';
|
||||||
const API_BASE_URL = 'http://localhost:8000'; // Assuming backend runs here
|
|
||||||
|
|
||||||
// This should match the base URL from which FastAPI serves static files
|
|
||||||
// If your main app is at http://localhost:8000, and static files are served from /generated_audio relative to that,
|
|
||||||
// then this should be http://localhost:8000. The backend will return paths like /generated_audio/...
|
|
||||||
const API_BASE_URL_FOR_FILES = 'http://localhost:8000';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
console.log('DOM fully loaded and parsed');
|
console.log('DOM fully loaded and parsed');
|
||||||
|
@ -314,9 +308,18 @@ async function initializeDialogEditor() {
|
||||||
const payload = { ...item };
|
const payload = { ...item };
|
||||||
// Remove fields not needed by backend
|
// Remove fields not needed by backend
|
||||||
delete payload.audioUrl; delete payload.isGenerating; delete payload.error;
|
delete payload.audioUrl; delete payload.isGenerating; delete payload.error;
|
||||||
|
console.log('Sending payload:', payload);
|
||||||
const result = await generateLine(payload);
|
const result = await generateLine(payload);
|
||||||
dialogItems[index].audioUrl = result.audio_url;
|
console.log('Received result:', result);
|
||||||
|
if (result && result.audio_url) {
|
||||||
|
dialogItems[index].audioUrl = result.audio_url;
|
||||||
|
console.log('Set audioUrl to:', result.audio_url);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid result structure:', result);
|
||||||
|
throw new Error('Invalid response: missing audio_url');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Error in generateLine:', err);
|
||||||
dialogItems[index].error = err.message || 'Failed to generate audio.';
|
dialogItems[index].error = err.message || 'Failed to generate audio.';
|
||||||
alert(dialogItems[index].error);
|
alert(dialogItems[index].error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
// Frontend Configuration
|
||||||
|
// This file handles environment variable configuration for the frontend
|
||||||
|
|
||||||
|
// Get environment variables (these would be injected by a build tool like Vite)
|
||||||
|
// For now, we'll use defaults that can be overridden
|
||||||
|
const getEnvVar = (name, defaultValue) => {
|
||||||
|
// In a real Vite setup, this would be import.meta.env[name]
|
||||||
|
// For now, we'll check if there's a global config object or use defaults
|
||||||
|
if (typeof window !== 'undefined' && window.APP_CONFIG && window.APP_CONFIG[name]) {
|
||||||
|
return window.APP_CONFIG[name];
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// API Configuration
|
||||||
|
export const API_BASE_URL = getEnvVar('VITE_API_BASE_URL', 'http://localhost:8000');
|
||||||
|
export const API_BASE_URL_WITH_PREFIX = getEnvVar('VITE_API_BASE_URL_WITH_PREFIX', 'http://localhost:8000/api');
|
||||||
|
|
||||||
|
// For file serving (same as API_BASE_URL since files are served from the same server)
|
||||||
|
export const API_BASE_URL_FOR_FILES = API_BASE_URL;
|
||||||
|
|
||||||
|
// Development server configuration
|
||||||
|
export const DEV_SERVER_PORT = getEnvVar('VITE_DEV_SERVER_PORT', '8001');
|
||||||
|
export const DEV_SERVER_HOST = getEnvVar('VITE_DEV_SERVER_HOST', '127.0.0.1');
|
||||||
|
|
||||||
|
// Export all config as a single object for convenience
|
||||||
|
export const CONFIG = {
|
||||||
|
API_BASE_URL,
|
||||||
|
API_BASE_URL_WITH_PREFIX,
|
||||||
|
API_BASE_URL_FOR_FILES,
|
||||||
|
DEV_SERVER_PORT,
|
||||||
|
DEV_SERVER_HOST
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CONFIG;
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple development server for the frontend that reads configuration from .env
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Try to load environment variables, but don't fail if dotenv is not available
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
except ImportError:
|
||||||
|
print("python-dotenv not installed, using system environment variables only")
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PORT = int(os.getenv('VITE_DEV_SERVER_PORT', '8001'))
|
||||||
|
HOST = os.getenv('VITE_DEV_SERVER_HOST', '127.0.0.1')
|
||||||
|
|
||||||
|
# Change to frontend directory
|
||||||
|
frontend_dir = Path(__file__).parent
|
||||||
|
os.chdir(frontend_dir)
|
||||||
|
|
||||||
|
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def end_headers(self):
|
||||||
|
# Add CORS headers for development
|
||||||
|
self.send_header('Access-Control-Allow-Origin', '*')
|
||||||
|
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||||
|
self.send_header('Access-Control-Allow-Headers', '*')
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"Starting Frontend Development Server...")
|
||||||
|
print(f"Host: {HOST}")
|
||||||
|
print(f"Port: {PORT}")
|
||||||
|
print(f"Serving from: {frontend_dir}")
|
||||||
|
print(f"Open: http://{HOST}:{PORT}")
|
||||||
|
|
||||||
|
with socketserver.TCPServer((HOST, PORT), MyHTTPRequestHandler) as httpd:
|
||||||
|
print(f"Server running at http://{HOST}:{PORT}/")
|
||||||
|
try:
|
||||||
|
httpd.serve_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down server...")
|
|
@ -0,0 +1,5 @@
|
||||||
|
gradio>=3.50.0
|
||||||
|
PyYAML>=6.0
|
||||||
|
torch>=2.0.0
|
||||||
|
torchaudio>=2.0.0
|
||||||
|
numpy>=1.21.0
|
|
@ -0,0 +1,86 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Setup script for Chatterbox TTS Application
|
||||||
|
This script helps configure the application for different environments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def setup_environment():
|
||||||
|
"""Setup environment configuration files"""
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
|
||||||
|
print("🔧 Setting up Chatterbox TTS Application...")
|
||||||
|
|
||||||
|
# Create .env file if it doesn't exist
|
||||||
|
env_file = project_root / ".env"
|
||||||
|
env_example = project_root / ".env.example"
|
||||||
|
|
||||||
|
if not env_file.exists() and env_example.exists():
|
||||||
|
print("📝 Creating .env file from .env.example...")
|
||||||
|
shutil.copy(env_example, env_file)
|
||||||
|
|
||||||
|
# Update PROJECT_ROOT in .env to current directory
|
||||||
|
with open(env_file, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
content = content.replace('/path/to/your/chatterbox-ui', str(project_root))
|
||||||
|
|
||||||
|
with open(env_file, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"✅ Created .env file with PROJECT_ROOT set to: {project_root}")
|
||||||
|
else:
|
||||||
|
print("ℹ️ .env file already exists")
|
||||||
|
|
||||||
|
# Setup backend .env
|
||||||
|
backend_env = project_root / "backend" / ".env"
|
||||||
|
backend_env_example = project_root / "backend" / ".env.example"
|
||||||
|
|
||||||
|
if not backend_env.exists() and backend_env_example.exists():
|
||||||
|
print("📝 Creating backend/.env file...")
|
||||||
|
shutil.copy(backend_env_example, backend_env)
|
||||||
|
|
||||||
|
# Update PROJECT_ROOT in backend .env
|
||||||
|
with open(backend_env, 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
content = content.replace('/Users/stwhite/CODE/chatterbox-ui', str(project_root))
|
||||||
|
|
||||||
|
with open(backend_env, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"✅ Created backend/.env file")
|
||||||
|
|
||||||
|
# Setup frontend .env
|
||||||
|
frontend_env = project_root / "frontend" / ".env"
|
||||||
|
frontend_env_example = project_root / "frontend" / ".env.example"
|
||||||
|
|
||||||
|
if not frontend_env.exists() and frontend_env_example.exists():
|
||||||
|
print("📝 Creating frontend/.env file...")
|
||||||
|
shutil.copy(frontend_env_example, frontend_env)
|
||||||
|
print(f"✅ Created frontend/.env file")
|
||||||
|
|
||||||
|
# Create necessary directories
|
||||||
|
directories = [
|
||||||
|
project_root / "speaker_data" / "speaker_samples",
|
||||||
|
project_root / "tts_temp_outputs",
|
||||||
|
project_root / "backend" / "tts_generated_dialogs"
|
||||||
|
]
|
||||||
|
|
||||||
|
for directory in directories:
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
print(f"📁 Created directory: {directory}")
|
||||||
|
|
||||||
|
print("\n🎉 Setup complete!")
|
||||||
|
print("\n📋 Next steps:")
|
||||||
|
print("1. Review and adjust the .env files as needed")
|
||||||
|
print("2. Install backend dependencies: cd backend && pip install -r requirements.txt")
|
||||||
|
print("3. Start backend server: cd backend && python start_server.py")
|
||||||
|
print("4. Start frontend server: cd frontend && python start_dev_server.py")
|
||||||
|
print("5. Open http://127.0.0.1:8001 in your browser")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup_environment()
|
|
@ -0,0 +1,21 @@
|
||||||
|
831c1dbe-c379-4d9f-868b-9798adc3c05d:
|
||||||
|
name: Adam
|
||||||
|
sample_path: speaker_samples/831c1dbe-c379-4d9f-868b-9798adc3c05d.wav
|
||||||
|
608903c4-b157-46c5-a0ea-4b25eb4b83b6:
|
||||||
|
name: Denise
|
||||||
|
sample_path: speaker_samples/608903c4-b157-46c5-a0ea-4b25eb4b83b6.wav
|
||||||
|
3c93c9df-86dc-4d67-ab55-8104b9301190:
|
||||||
|
name: Maria
|
||||||
|
sample_path: speaker_samples/3c93c9df-86dc-4d67-ab55-8104b9301190.wav
|
||||||
|
fb84ce1c-f32d-4df9-9673-2c64e9603133:
|
||||||
|
name: Debbie
|
||||||
|
sample_path: speaker_samples/fb84ce1c-f32d-4df9-9673-2c64e9603133.wav
|
||||||
|
90fcd672-ba84-441a-ac6c-0449a59653bd:
|
||||||
|
name: dummy_speaker
|
||||||
|
sample_path: speaker_samples/90fcd672-ba84-441a-ac6c-0449a59653bd.wav
|
||||||
|
a6387c23-4ca4-42b5-8aaf-5699dbabbdf0:
|
||||||
|
name: Mike
|
||||||
|
sample_path: speaker_samples/a6387c23-4ca4-42b5-8aaf-5699dbabbdf0.wav
|
||||||
|
6cf4d171-667d-4bc8-adbb-6d9b7c620cb8:
|
||||||
|
name: Minnie
|
||||||
|
sample_path: speaker_samples/6cf4d171-667d-4bc8-adbb-6d9b7c620cb8.wav
|
|
@ -19,3 +19,12 @@ a6387c23-4ca4-42b5-8aaf-5699dbabbdf0:
|
||||||
6cf4d171-667d-4bc8-adbb-6d9b7c620cb8:
|
6cf4d171-667d-4bc8-adbb-6d9b7c620cb8:
|
||||||
name: Minnie
|
name: Minnie
|
||||||
sample_path: speaker_samples/6cf4d171-667d-4bc8-adbb-6d9b7c620cb8.wav
|
sample_path: speaker_samples/6cf4d171-667d-4bc8-adbb-6d9b7c620cb8.wav
|
||||||
|
f1377dc6-aec5-42fc-bea7-98c0be49c48e:
|
||||||
|
name: Glinda
|
||||||
|
sample_path: speaker_samples/f1377dc6-aec5-42fc-bea7-98c0be49c48e.wav
|
||||||
|
dd3552d9-f4e8-49ed-9892-f9e67afcf23c:
|
||||||
|
name: emily
|
||||||
|
sample_path: speaker_samples/dd3552d9-f4e8-49ed-9892-f9e67afcf23c.wav
|
||||||
|
2cdd6d3d-c533-44bf-a5f6-cc83bd089d32:
|
||||||
|
name: Grace
|
||||||
|
sample_path: speaker_samples/2cdd6d3d-c533-44bf-a5f6-cc83bd089d32.wav
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
#!/Users/stwhite/CODE/chatterbox-ui/.venv/bin/python
|
||||||
|
"""
|
||||||
|
Startup script that launches both the backend and frontend servers concurrently.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Try to load environment variables, but don't fail if dotenv is not available
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv()
|
||||||
|
except ImportError:
|
||||||
|
print("python-dotenv not installed, using system environment variables only")
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BACKEND_PORT = int(os.getenv('BACKEND_PORT', '8000'))
|
||||||
|
BACKEND_HOST = os.getenv('BACKEND_HOST', '0.0.0.0')
|
||||||
|
FRONTEND_PORT = int(os.getenv('FRONTEND_PORT', '8001'))
|
||||||
|
FRONTEND_HOST = os.getenv('FRONTEND_HOST', '127.0.0.1')
|
||||||
|
|
||||||
|
# Get project root directory
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.absolute()
|
||||||
|
|
||||||
|
def run_backend():
|
||||||
|
"""Run the backend FastAPI server"""
|
||||||
|
os.chdir(PROJECT_ROOT / "backend")
|
||||||
|
cmd = [
|
||||||
|
sys.executable, "-m", "uvicorn",
|
||||||
|
"app.main:app",
|
||||||
|
"--reload",
|
||||||
|
f"--host={BACKEND_HOST}",
|
||||||
|
f"--port={BACKEND_PORT}"
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Starting Backend Server at http://{BACKEND_HOST}:{BACKEND_PORT}")
|
||||||
|
print(f"API docs available at http://{BACKEND_HOST}:{BACKEND_PORT}/docs")
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
return subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_frontend():
|
||||||
|
"""Run the frontend development server"""
|
||||||
|
frontend_dir = PROJECT_ROOT / "frontend"
|
||||||
|
os.chdir(frontend_dir)
|
||||||
|
|
||||||
|
cmd = [sys.executable, "start_dev_server.py"]
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["VITE_DEV_SERVER_HOST"] = FRONTEND_HOST
|
||||||
|
env["VITE_DEV_SERVER_PORT"] = str(FRONTEND_PORT)
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Starting Frontend Server at http://{FRONTEND_HOST}:{FRONTEND_PORT}")
|
||||||
|
print(f"{'='*50}\n")
|
||||||
|
|
||||||
|
return subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
universal_newlines=True,
|
||||||
|
bufsize=1
|
||||||
|
)
|
||||||
|
|
||||||
|
def print_process_output(process, prefix):
|
||||||
|
"""Print process output with a prefix"""
|
||||||
|
for line in iter(process.stdout.readline, ''):
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
print(f"{prefix} | {line}", end='')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to start both servers"""
|
||||||
|
print("\n🚀 Starting Chatterbox UI Development Environment")
|
||||||
|
|
||||||
|
# Start the backend server
|
||||||
|
backend_process = run_backend()
|
||||||
|
|
||||||
|
# Give the backend a moment to start
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Start the frontend server
|
||||||
|
frontend_process = run_frontend()
|
||||||
|
|
||||||
|
# Create threads to monitor and print output
|
||||||
|
backend_monitor = threading.Thread(
|
||||||
|
target=print_process_output,
|
||||||
|
args=(backend_process, "BACKEND"),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
frontend_monitor = threading.Thread(
|
||||||
|
target=print_process_output,
|
||||||
|
args=(frontend_process, "FRONTEND"),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
|
||||||
|
backend_monitor.start()
|
||||||
|
frontend_monitor.start()
|
||||||
|
|
||||||
|
# Setup signal handling for graceful shutdown
|
||||||
|
def signal_handler(sig, frame):
|
||||||
|
print("\n\n🛑 Shutting down servers...")
|
||||||
|
backend_process.terminate()
|
||||||
|
frontend_process.terminate()
|
||||||
|
# Threads are daemon, so they'll exit when the main thread exits
|
||||||
|
print("✅ Servers stopped successfully")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
|
||||||
|
# Print access information
|
||||||
|
print("\n📋 Access Information:")
|
||||||
|
print(f" • Frontend: http://{FRONTEND_HOST}:{FRONTEND_PORT}")
|
||||||
|
print(f" • Backend API: http://{BACKEND_HOST}:{BACKEND_PORT}/api")
|
||||||
|
print(f" • API Documentation: http://{BACKEND_HOST}:{BACKEND_PORT}/docs")
|
||||||
|
print("\n⚠️ Press Ctrl+C to stop both servers\n")
|
||||||
|
|
||||||
|
# Keep the main process running
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
signal_handler(None, None)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -0,0 +1,51 @@
|
||||||
|
import torch
|
||||||
|
import torchaudio as ta
|
||||||
|
from chatterbox.tts import ChatterboxTTS
|
||||||
|
|
||||||
|
# Detect device (Mac with M1/M2/M3/M4)
|
||||||
|
device = "mps" if torch.backends.mps.is_available() else "cpu"
|
||||||
|
|
||||||
|
def safe_load_chatterbox_tts(device="mps"):
|
||||||
|
"""
|
||||||
|
Safely load ChatterboxTTS model with proper device mapping.
|
||||||
|
Handles cases where model was saved on CUDA but needs to be loaded on MPS/CPU.
|
||||||
|
"""
|
||||||
|
# Store original torch.load function
|
||||||
|
original_torch_load = torch.load
|
||||||
|
|
||||||
|
def patched_torch_load(f, map_location=None, **kwargs):
|
||||||
|
# If no map_location is specified and we're loading on non-CUDA device,
|
||||||
|
# map CUDA tensors to the target device
|
||||||
|
if map_location is None:
|
||||||
|
if device == "mps" and torch.backends.mps.is_available():
|
||||||
|
map_location = torch.device("mps")
|
||||||
|
elif device == "cpu" or not torch.cuda.is_available():
|
||||||
|
map_location = torch.device("cpu")
|
||||||
|
else:
|
||||||
|
map_location = torch.device(device)
|
||||||
|
|
||||||
|
return original_torch_load(f, map_location=map_location, **kwargs)
|
||||||
|
|
||||||
|
# Temporarily patch torch.load
|
||||||
|
torch.load = patched_torch_load
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Load the model with the patched torch.load
|
||||||
|
model = ChatterboxTTS.from_pretrained(device=device)
|
||||||
|
return model
|
||||||
|
finally:
|
||||||
|
# Restore original torch.load
|
||||||
|
torch.load = original_torch_load
|
||||||
|
|
||||||
|
model = safe_load_chatterbox_tts(device=device)
|
||||||
|
text = "Today is the day. I want to move like a titan at dawn, sweat like a god forging lightning. No more excuses. From now on, my mornings will be temples of discipline. I am going to work out like the gods… every damn day."
|
||||||
|
|
||||||
|
# If you want to synthesize with a different voice, specify the audio prompt
|
||||||
|
AUDIO_PROMPT_PATH = "YOUR_FILE.wav"
|
||||||
|
wav = model.generate(
|
||||||
|
text,
|
||||||
|
audio_prompt_path=AUDIO_PROMPT_PATH,
|
||||||
|
exaggeration=2.0,
|
||||||
|
cfg_weight=0.5
|
||||||
|
)
|
||||||
|
ta.save("test-2.wav", wav, model.sr)
|
Loading…
Reference in New Issue