diff --git a/src/.env.example b/src/.env.example index 64320bc..f7c789d 100644 --- a/src/.env.example +++ b/src/.env.example @@ -8,3 +8,6 @@ APP_ID=683ea1bc86197e19f71fc1ae DELETE_SESSION_URL=http://127.0.0.1:3030/api/core/chat/delHistory?chatId={chatId}&appId={appId} DELETE_CHAT_URL=http://127.0.0.1:3030/api/core/chat/item/delete?contentId={contentId}&chatId={chatId}&appId={appId} GET_CHAT_RECORDS_URL=http://127.0.0.1:3030/api/core/chat/getPaginationRecords + +# Voice demo (Pipecat /ws-product). Relative to project root, or an absolute path. +VOICE_CONFIG=config/voice.json diff --git a/src/voice/config.py b/src/voice/config.py index 327e47b..0211f65 100644 --- a/src/voice/config.py +++ b/src/voice/config.py @@ -1,11 +1,30 @@ from __future__ import annotations import json +import os from dataclasses import dataclass, field from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent -DEFAULT_VOICE_CONFIG = PROJECT_ROOT / "config" / "voice.json" +DEFAULT_VOICE_CONFIG_REL = "config/voice.json" + + +def resolve_voice_config_path() -> Path: + """Return the voice config path from VOICE_CONFIG or the default.""" + configured = os.getenv("VOICE_CONFIG", DEFAULT_VOICE_CONFIG_REL).strip() + if not configured: + configured = DEFAULT_VOICE_CONFIG_REL + path = Path(configured) + if not path.is_absolute(): + path = PROJECT_ROOT / path + return path + + +DEFAULT_VOICE_CONFIG = resolve_voice_config_path() @dataclass(frozen=True) @@ -143,7 +162,7 @@ class EngineConfig: def load_config(path: str | Path | None = None) -> EngineConfig: - config_path = Path(path) if path is not None else DEFAULT_VOICE_CONFIG + config_path = Path(path) if path is not None else resolve_voice_config_path() if not config_path.is_absolute(): config_path = PROJECT_ROOT / config_path data = json.loads(config_path.read_text(encoding="utf-8")) diff --git a/src/voice/routes.py b/src/voice/routes.py index 7b21621..e60eb50 100644 --- a/src/voice/routes.py +++ b/src/voice/routes.py @@ -8,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from loguru import logger -from .config import DEFAULT_VOICE_CONFIG, EngineConfig, load_config +from .config import EngineConfig, load_config, resolve_voice_config_path from .pipeline import run_product_voice_pipeline PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent @@ -19,7 +19,12 @@ router = APIRouter(tags=["voice"]) @lru_cache(maxsize=1) def get_voice_config() -> EngineConfig: - return load_config(DEFAULT_VOICE_CONFIG) + return load_config() + + +@lru_cache(maxsize=1) +def get_voice_config_path() -> Path: + return resolve_voice_config_path() def _normalize_mount_path(path: str) -> str: @@ -39,6 +44,7 @@ async def voice_health() -> dict[str, object]: ) return { "status": "healthy", + "config": str(get_voice_config_path()), "protocols": { "/ws-product": "va.ws.v1.json_base64", }, @@ -62,12 +68,14 @@ async def product_websocket_endpoint(websocket: WebSocket) -> None: def register_voice(app: FastAPI) -> None: """Mount voice websocket routes and optional browser demo static files.""" - if not DEFAULT_VOICE_CONFIG.exists(): - logger.warning(f"Voice config not found at {DEFAULT_VOICE_CONFIG}; voice demo disabled") + voice_config_path = get_voice_config_path() + if not voice_config_path.exists(): + logger.warning(f"Voice config not found at {voice_config_path}; voice demo disabled") return config = get_voice_config() app.include_router(router) + logger.info(f"Voice config loaded from {voice_config_path}") if config.server.cors_origins: app.add_middleware(