import os import sys from dotenv import load_dotenv load_dotenv() def get_base_dir() -> str: """Get base directory, supporting PyInstaller frozen executables.""" if getattr(sys, "frozen", False): # Running as PyInstaller bundle return os.path.dirname(sys.executable) else: # Running as script - go up two levels from app/config.py to backend/ return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def get_app_data_dir() -> str: """Get persistent app data directory for storing user data. This directory persists across application restarts, unlike temp folders used by portable executables. Returns: Windows: %APPDATA%/Meeting-Assistant macOS: ~/Library/Application Support/Meeting-Assistant Linux: ~/.config/meeting-assistant """ if sys.platform == "win32": # Windows: Use APPDATA base = os.environ.get("APPDATA", os.path.expanduser("~")) return os.path.join(base, "Meeting-Assistant") elif sys.platform == "darwin": # macOS: Use Application Support return os.path.expanduser("~/Library/Application Support/Meeting-Assistant") else: # Linux: Use XDG config or fallback to ~/.config xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) return os.path.join(xdg_config, "meeting-assistant") class Settings: # Server Configuration BACKEND_HOST: str = os.getenv("BACKEND_HOST", "0.0.0.0") BACKEND_PORT: int = int(os.getenv("BACKEND_PORT", "8000")) # Database Configuration DB_TYPE: str = os.getenv("DB_TYPE", "mysql") # "mysql" or "sqlite" SQLITE_PATH: str = os.getenv("SQLITE_PATH", "data/meeting.db") DB_HOST: str = os.getenv("DB_HOST", "mysql.theaken.com") DB_PORT: int = int(os.getenv("DB_PORT", "33306")) DB_USER: str = os.getenv("DB_USER", "A060") DB_PASS: str = os.getenv("DB_PASS", "") DB_NAME: str = os.getenv("DB_NAME", "db_A060") DB_POOL_SIZE: int = int(os.getenv("DB_POOL_SIZE", "5")) # External API Configuration AUTH_API_URL: str = os.getenv( "AUTH_API_URL", "https://pj-auth-api.vercel.app/api/auth/login" ) DIFY_API_URL: str = os.getenv("DIFY_API_URL", "https://dify.theaken.com/v1") DIFY_API_KEY: str = os.getenv("DIFY_API_KEY", "") DIFY_STT_API_KEY: str = os.getenv("DIFY_STT_API_KEY", "") # Authentication Configuration ADMIN_EMAIL: str = os.getenv("ADMIN_EMAIL", "ymirliu@panjit.com.tw") JWT_SECRET: str = os.getenv("JWT_SECRET", "meeting-assistant-secret") JWT_EXPIRE_HOURS: int = int(os.getenv("JWT_EXPIRE_HOURS", "24")) # Timeout Configuration (in milliseconds) UPLOAD_TIMEOUT: int = int(os.getenv("UPLOAD_TIMEOUT", "600000")) # 10 minutes DIFY_STT_TIMEOUT: int = int(os.getenv("DIFY_STT_TIMEOUT", "300000")) # 5 minutes LLM_TIMEOUT: int = int(os.getenv("LLM_TIMEOUT", "120000")) # 2 minutes AUTH_TIMEOUT: int = int(os.getenv("AUTH_TIMEOUT", "30000")) # 30 seconds # File Configuration TEMPLATE_DIR: str = os.getenv("TEMPLATE_DIR", "") RECORD_DIR: str = os.getenv("RECORD_DIR", "") MAX_FILE_SIZE: int = int(os.getenv("MAX_FILE_SIZE", str(500 * 1024 * 1024))) # 500MB SUPPORTED_AUDIO_FORMATS: str = os.getenv( "SUPPORTED_AUDIO_FORMATS", ".mp3,.wav,.m4a,.webm,.ogg,.flac,.aac" ) @property def supported_audio_formats_set(self) -> set: """Return supported audio formats as a set.""" return set(self.SUPPORTED_AUDIO_FORMATS.split(",")) def get_template_dir(self, base_dir: str | None = None) -> str: """Get template directory path, resolving relative paths. Args: base_dir: Base directory for relative paths. If None, uses get_base_dir() which supports frozen executables. """ if base_dir is None: base_dir = get_base_dir() if self.TEMPLATE_DIR: if os.path.isabs(self.TEMPLATE_DIR): return self.TEMPLATE_DIR return os.path.join(base_dir, self.TEMPLATE_DIR) return os.path.join(base_dir, "template") def get_record_dir(self, base_dir: str | None = None) -> str: """Get record directory path, resolving relative paths. Args: base_dir: Base directory for relative paths. If None, uses get_base_dir() which supports frozen executables. """ if base_dir is None: base_dir = get_base_dir() if self.RECORD_DIR: if os.path.isabs(self.RECORD_DIR): return self.RECORD_DIR return os.path.join(base_dir, self.RECORD_DIR) return os.path.join(base_dir, "record") def get_sqlite_path(self, base_dir: str | None = None) -> str: """Get SQLite database file path, resolving relative paths. For packaged executables (frozen), uses persistent app data directory to survive portable exe cleanup. For development, uses relative path. Args: base_dir: Base directory for relative paths. If None, auto-detects. """ # If absolute path specified, use it directly if self.SQLITE_PATH and os.path.isabs(self.SQLITE_PATH): return self.SQLITE_PATH # For frozen executables, use persistent app data directory # This ensures SQLite data survives portable exe temp cleanup if getattr(sys, "frozen", False): app_data = get_app_data_dir() db_name = os.path.basename(self.SQLITE_PATH) if self.SQLITE_PATH else "meeting.db" return os.path.join(app_data, "data", db_name) # For development, use relative path from base_dir if base_dir is None: base_dir = get_base_dir() if self.SQLITE_PATH: return os.path.join(base_dir, self.SQLITE_PATH) return os.path.join(base_dir, "data", "meeting.db") # Timeout helpers (convert ms to seconds for httpx) @property def upload_timeout_seconds(self) -> float: return self.UPLOAD_TIMEOUT / 1000.0 @property def dify_stt_timeout_seconds(self) -> float: return self.DIFY_STT_TIMEOUT / 1000.0 @property def llm_timeout_seconds(self) -> float: return self.LLM_TIMEOUT / 1000.0 @property def auth_timeout_seconds(self) -> float: return self.AUTH_TIMEOUT / 1000.0 settings = Settings()