- Add NSIS installer as default target (--target nsis) - Keep portable option available (--target portable) - Store SQLite database in %APPDATA%\Meeting-Assistant for persistence - Portable temp folder cleanup no longer affects SQLite data - Update build-client.bat with --target parameter support - Update DEPLOYMENT.md with new options and comparisons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
165 lines
6.3 KiB
Python
165 lines
6.3 KiB
Python
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()
|