From 948712bb3fd295b8e72a83ab4fc98ff537904351 Mon Sep 17 00:00:00 2001 From: Steve White Date: Tue, 12 Aug 2025 11:31:00 -0500 Subject: [PATCH] current workign version using chatterbox. --- API_REFERENCE.md | 2 +- ENVIRONMENT_SETUP.md | 4 +- README.md | 2 +- backend/app/config.py | 48 ++++++++++++++++++++---- frontend/js/config.js | 11 +++++- speaker_data/speakers.yaml | 3 ++ start_servers.py | 77 +++++++++++++++++++++----------------- 7 files changed, 99 insertions(+), 48 deletions(-) diff --git a/API_REFERENCE.md b/API_REFERENCE.md index ef5d022..09014a9 100644 --- a/API_REFERENCE.md +++ b/API_REFERENCE.md @@ -359,7 +359,7 @@ The API uses the following directory structure (configurable in `app/config.py`) - **Temporary Files**: `{PROJECT_ROOT}/tts_temp_outputs/` ### CORS Settings -- Allowed Origins: `http://localhost:8001`, `http://127.0.0.1:8001` +- Allowed Origins: `http://localhost:8001`, `http://127.0.0.1:8001` (plus any `FRONTEND_HOST:FRONTEND_PORT` when using `start_servers.py`) - Allowed Methods: All - Allowed Headers: All - Credentials: Enabled diff --git a/ENVIRONMENT_SETUP.md b/ENVIRONMENT_SETUP.md index 6081211..3c7bc2f 100644 --- a/ENVIRONMENT_SETUP.md +++ b/ENVIRONMENT_SETUP.md @@ -58,7 +58,7 @@ The application uses environment variables for configuration. Three `.env` files - `VITE_DEV_SERVER_HOST`: Frontend development server host #### CORS Configuration -- `CORS_ORIGINS`: Comma-separated list of allowed origins +- `CORS_ORIGINS`: Comma-separated list of allowed origins. When using `start_servers.py` with the default `FRONTEND_HOST=0.0.0.0` and no explicit `CORS_ORIGINS`, CORS will allow all origins (wildcard) to simplify development. #### Device Configuration - `DEVICE`: Device for TTS model (auto, cpu, cuda, mps) @@ -101,7 +101,7 @@ CORS_ORIGINS=http://localhost:3000 ### Common Issues 1. **Permission Errors**: Ensure the `PROJECT_ROOT` directory is writable -2. **CORS Errors**: Check that your frontend URL is in `CORS_ORIGINS` +2. **CORS Errors**: Check that your frontend URL is in `CORS_ORIGINS`. (When using `start_servers.py`, your specified `FRONTEND_HOST:FRONTEND_PORT` will be auto‑included.) 3. **Model Loading Errors**: Verify `DEVICE` setting matches your hardware 4. **Path Errors**: Ensure all path variables point to existing, accessible directories diff --git a/README.md b/README.md index 3f863d5..f516147 100644 --- a/README.md +++ b/README.md @@ -149,5 +149,5 @@ The application automatically: - **"Skipping unknown speaker"**: Configure speaker in `speaker_data/speakers.yaml` - **"Sample file not found"**: Verify audio files exist in `speaker_data/speaker_samples/` - **Memory issues**: Use model reinitialization options for long content -- **CORS errors**: Check frontend/backend port configuration +- **CORS errors**: Check frontend/backend port configuration (frontend origin is auto-included when using `start_servers.py`) - **Import errors**: Run `python import_helper.py` to check dependencies diff --git a/backend/app/config.py b/backend/app/config.py index 2812436..be3b33e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -6,20 +6,34 @@ from dotenv import load_dotenv load_dotenv() # Project root - can be overridden by environment variable -PROJECT_ROOT = Path(os.getenv("PROJECT_ROOT", Path(__file__).parent.parent.parent)).resolve() +PROJECT_ROOT = Path( + os.getenv("PROJECT_ROOT", Path(__file__).parent.parent.parent) +).resolve() # 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"))) +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_TEMP_OUTPUT_DIR = Path(os.getenv("TTS_TEMP_OUTPUT_DIR", str(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) # These are stored within the 'backend' directory to be easily servable. DIALOG_OUTPUT_PARENT_DIR = PROJECT_ROOT / "backend" -DIALOG_GENERATED_DIR = Path(os.getenv("DIALOG_GENERATED_DIR", str(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 DIALOG_OUTPUT_DIR = DIALOG_GENERATED_DIR @@ -29,8 +43,26 @@ 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(",")] +# CORS configuration: determine allowed origins based on env & frontend binding +_cors_env = os.getenv("CORS_ORIGINS", "") +_frontend_host = os.getenv("FRONTEND_HOST") +_frontend_port = os.getenv("FRONTEND_PORT") + +# If the dev server is bound to 0.0.0.0 (all interfaces), allow all origins +if _frontend_host == "0.0.0.0": # dev convenience when binding wildcard + CORS_ORIGINS = ["*"] +elif _cors_env: + # parse comma-separated origins, strip whitespace + CORS_ORIGINS = [origin.strip() for origin in _cors_env.split(",") if origin.strip()] +else: + # default to allow all origins in development + CORS_ORIGINS = ["*"] + +# Auto-include specific frontend origin when not using wildcard CORS +if CORS_ORIGINS != ["*"] and _frontend_host and _frontend_port: + _frontend_origin = f"http://{_frontend_host.strip()}:{_frontend_port.strip()}" + if _frontend_origin not in CORS_ORIGINS: + CORS_ORIGINS.append(_frontend_origin) # Device configuration DEVICE = os.getenv("DEVICE", "auto") diff --git a/frontend/js/config.js b/frontend/js/config.js index e8dbc5e..9cbf4f1 100644 --- a/frontend/js/config.js +++ b/frontend/js/config.js @@ -13,8 +13,15 @@ const getEnvVar = (name, 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'); +// Default to the same hostname as the frontend, on port 8000 (override via VITE_API_BASE_URL*) +const _defaultHost = (typeof window !== 'undefined' && window.location?.hostname) || 'localhost'; +const _defaultPort = getEnvVar('VITE_API_BASE_URL_PORT', '8000'); +const _defaultBase = `http://${_defaultHost}:${_defaultPort}`; +export const API_BASE_URL = getEnvVar('VITE_API_BASE_URL', _defaultBase); +export const API_BASE_URL_WITH_PREFIX = getEnvVar( + 'VITE_API_BASE_URL_WITH_PREFIX', + `${_defaultBase}/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; diff --git a/speaker_data/speakers.yaml b/speaker_data/speakers.yaml index 2c608f1..285f093 100644 --- a/speaker_data/speakers.yaml +++ b/speaker_data/speakers.yaml @@ -28,3 +28,6 @@ dd3552d9-f4e8-49ed-9892-f9e67afcf23c: 2cdd6d3d-c533-44bf-a5f6-cc83bd089d32: name: Grace sample_path: speaker_samples/2cdd6d3d-c533-44bf-a5f6-cc83bd089d32.wav +3d3e85db-3d67-4488-94b2-ffc189fbb287: + name: RCB + sample_path: speaker_samples/3d3e85db-3d67-4488-94b2-ffc189fbb287.wav diff --git a/start_servers.py b/start_servers.py index cf908c7..6e34dae 100755 --- a/start_servers.py +++ b/start_servers.py @@ -14,101 +14,109 @@ 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') +BACKEND_PORT = int(os.getenv("BACKEND_PORT", "8000")) +BACKEND_HOST = os.getenv("BACKEND_HOST", "0.0.0.0") +# Frontend host/port (for dev server binding) +FRONTEND_PORT = int(os.getenv("FRONTEND_PORT", "8001")) +FRONTEND_HOST = os.getenv("FRONTEND_HOST", "0.0.0.0") + +# Export frontend host/port so backend CORS config can pick them up automatically +os.environ["FRONTEND_HOST"] = FRONTEND_HOST +os.environ["FRONTEND_PORT"] = str(FRONTEND_PORT) # 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}" + 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, + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, - bufsize=1 + 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 + bufsize=1, ) + def print_process_output(process, prefix): """Print process output with a prefix""" - for line in iter(process.stdout.readline, ''): + for line in iter(process.stdout.readline, ""): if not line: break - print(f"{prefix} | {line}", end='') + 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 + target=print_process_output, args=(backend_process, "BACKEND"), daemon=True ) frontend_monitor = threading.Thread( - target=print_process_output, - args=(frontend_process, "FRONTEND"), - daemon=True + 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...") @@ -117,16 +125,16 @@ def main(): # 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: @@ -134,5 +142,6 @@ def main(): except KeyboardInterrupt: signal_handler(None, None) + if __name__ == "__main__": main()