feat: Extract hardcoded configs to environment variables
- Add environment variable configuration for backend and frontend - Backend: DB_POOL_SIZE, JWT_EXPIRE_HOURS, timeout configs, directory paths - Frontend: VITE_API_BASE_URL, VITE_UPLOAD_TIMEOUT, Whisper configs - Create deployment script (scripts/deploy-backend.sh) - Create 1Panel deployment guide (docs/1panel-deployment.md) - Update DEPLOYMENT.md with env var documentation - Create README.md with project overview - Remove obsolete PRD.md, SDD.md, TDD.md (replaced by OpenSpec) - Keep CORS allow_origins=["*"] for Electron EXE distribution 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,69 @@
|
||||
# =============================================================================
|
||||
# Meeting Assistant Backend Configuration
|
||||
# Copy this file to .env and fill in your values
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Server Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# Host address to bind (0.0.0.0 for all interfaces)
|
||||
BACKEND_HOST=0.0.0.0
|
||||
# Port number to listen on
|
||||
BACKEND_PORT=8000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
DB_HOST=mysql.theaken.com
|
||||
DB_PORT=33306
|
||||
DB_USER=A060
|
||||
DB_USER=your_username
|
||||
DB_PASS=your_password_here
|
||||
DB_NAME=db_A060
|
||||
DB_NAME=your_database
|
||||
# Connection pool size (default: 5)
|
||||
DB_POOL_SIZE=5
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# External APIs
|
||||
# -----------------------------------------------------------------------------
|
||||
# Company authentication API endpoint
|
||||
AUTH_API_URL=https://pj-auth-api.vercel.app/api/auth/login
|
||||
# Dify API base URL
|
||||
DIFY_API_URL=https://dify.theaken.com/v1
|
||||
# Dify LLM API key (for summarization)
|
||||
DIFY_API_KEY=app-xxxxxxxxxxx
|
||||
# Dify STT API key (for audio transcription)
|
||||
DIFY_STT_API_KEY=app-xxxxxxxxxxx
|
||||
|
||||
# Application Settings
|
||||
ADMIN_EMAIL=ymirliu@panjit.com.tw
|
||||
# -----------------------------------------------------------------------------
|
||||
# Authentication Settings
|
||||
# -----------------------------------------------------------------------------
|
||||
# Email address with admin privileges
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
# JWT signing secret (use a strong random string in production)
|
||||
JWT_SECRET=your_jwt_secret_here
|
||||
# JWT token expiration time in hours (default: 24)
|
||||
JWT_EXPIRE_HOURS=24
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Timeout Configuration (in milliseconds)
|
||||
# -----------------------------------------------------------------------------
|
||||
# File upload timeout (default: 600000 = 10 minutes)
|
||||
UPLOAD_TIMEOUT=600000
|
||||
# Dify STT transcription timeout per chunk (default: 300000 = 5 minutes)
|
||||
DIFY_STT_TIMEOUT=300000
|
||||
# Dify LLM processing timeout (default: 120000 = 2 minutes)
|
||||
LLM_TIMEOUT=120000
|
||||
# Authentication API timeout (default: 30000 = 30 seconds)
|
||||
AUTH_TIMEOUT=30000
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# File Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
# Template directory path (leave empty for default: ./template)
|
||||
# TEMPLATE_DIR=/path/to/templates
|
||||
# Record output directory path (leave empty for default: ./record)
|
||||
# RECORD_DIR=/path/to/records
|
||||
# Maximum upload file size in bytes (default: 524288000 = 500MB)
|
||||
MAX_FILE_SIZE=524288000
|
||||
# Supported audio formats (comma-separated)
|
||||
SUPPORTED_AUDIO_FORMATS=.mp3,.wav,.m4a,.webm,.ogg,.flac,.aac
|
||||
|
||||
@@ -5,12 +5,19 @@ load_dotenv()
|
||||
|
||||
|
||||
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_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"
|
||||
)
|
||||
@@ -18,8 +25,62 @@ class Settings:
|
||||
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) -> str:
|
||||
"""Get template directory path, resolving relative paths."""
|
||||
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) -> str:
|
||||
"""Get record directory path, resolving relative paths."""
|
||||
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")
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -10,7 +10,7 @@ def init_db_pool():
|
||||
global connection_pool
|
||||
connection_pool = pooling.MySQLConnectionPool(
|
||||
pool_name="meeting_pool",
|
||||
pool_size=5,
|
||||
pool_size=settings.DB_POOL_SIZE,
|
||||
host=settings.DB_HOST,
|
||||
port=settings.DB_PORT,
|
||||
user=settings.DB_USER,
|
||||
|
||||
@@ -13,10 +13,6 @@ from ..config import settings
|
||||
from ..models import SummarizeRequest, SummarizeResponse, ActionItemCreate, TokenPayload
|
||||
from .auth import get_current_user
|
||||
|
||||
# Supported audio formats
|
||||
SUPPORTED_AUDIO_FORMATS = {".mp3", ".wav", ".m4a", ".webm", ".ogg", ".flac", ".aac"}
|
||||
MAX_FILE_SIZE = 500 * 1024 * 1024 # 500MB
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -45,7 +41,7 @@ async def summarize_transcript(
|
||||
"response_mode": "blocking",
|
||||
"user": current_user.email,
|
||||
},
|
||||
timeout=120.0, # Long timeout for LLM processing
|
||||
timeout=settings.llm_timeout_seconds,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
@@ -135,10 +131,10 @@ async def transcribe_audio(
|
||||
|
||||
# Validate file extension
|
||||
file_ext = os.path.splitext(file.filename or "")[1].lower()
|
||||
if file_ext not in SUPPORTED_AUDIO_FORMATS:
|
||||
if file_ext not in settings.supported_audio_formats_set:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported audio format. Supported: {', '.join(SUPPORTED_AUDIO_FORMATS)}"
|
||||
detail=f"Unsupported audio format. Supported: {settings.SUPPORTED_AUDIO_FORMATS}"
|
||||
)
|
||||
|
||||
# Create temp directory for processing
|
||||
@@ -151,10 +147,10 @@ async def transcribe_audio(
|
||||
with open(temp_file_path, "wb") as f:
|
||||
while chunk := await file.read(1024 * 1024): # 1MB chunks
|
||||
file_size += len(chunk)
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
if file_size > settings.MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB"
|
||||
detail=f"File too large. Maximum size: {settings.MAX_FILE_SIZE // (1024*1024)}MB"
|
||||
)
|
||||
f.write(chunk)
|
||||
|
||||
@@ -245,18 +241,18 @@ async def transcribe_audio_stream(
|
||||
|
||||
# Validate file extension
|
||||
file_ext = os.path.splitext(file.filename or "")[1].lower()
|
||||
if file_ext not in SUPPORTED_AUDIO_FORMATS:
|
||||
if file_ext not in settings.supported_audio_formats_set:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported audio format. Supported: {', '.join(SUPPORTED_AUDIO_FORMATS)}"
|
||||
detail=f"Unsupported audio format. Supported: {settings.SUPPORTED_AUDIO_FORMATS}"
|
||||
)
|
||||
|
||||
# Read file into memory for streaming
|
||||
file_content = await file.read()
|
||||
if len(file_content) > MAX_FILE_SIZE:
|
||||
if len(file_content) > settings.MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=413,
|
||||
detail=f"File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB"
|
||||
detail=f"File too large. Maximum size: {settings.MAX_FILE_SIZE // (1024*1024)}MB"
|
||||
)
|
||||
|
||||
async def generate_progress() -> AsyncGenerator[str, None]:
|
||||
@@ -366,7 +362,7 @@ async def segment_audio_with_sidecar(audio_path: str, output_dir: str) -> dict:
|
||||
# Send command and wait for response
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(input=f"{cmd_input}\n{{\"action\": \"quit\"}}\n".encode()),
|
||||
timeout=600 # 10 minutes timeout for large files
|
||||
timeout=settings.upload_timeout_seconds
|
||||
)
|
||||
|
||||
# Parse response (skip status messages, find the segment result)
|
||||
@@ -490,7 +486,7 @@ async def transcribe_chunk_with_dify(
|
||||
}
|
||||
]
|
||||
},
|
||||
timeout=300.0, # 5 minutes per chunk (increased for longer segments)
|
||||
timeout=settings.dify_stt_timeout_seconds,
|
||||
)
|
||||
|
||||
print(f"[Dify] Chat response: {response.status_code}")
|
||||
|
||||
@@ -17,7 +17,7 @@ def create_token(email: str, role: str) -> str:
|
||||
payload = {
|
||||
"email": email,
|
||||
"role": role,
|
||||
"exp": datetime.utcnow() + timedelta(hours=24),
|
||||
"exp": datetime.utcnow() + timedelta(hours=settings.JWT_EXPIRE_HOURS),
|
||||
}
|
||||
return jwt.encode(payload, settings.JWT_SECRET, algorithm="HS256")
|
||||
|
||||
@@ -67,7 +67,7 @@ async def login(request: LoginRequest):
|
||||
response = await client.post(
|
||||
settings.AUTH_API_URL,
|
||||
json={"username": request.email, "password": request.password},
|
||||
timeout=30.0,
|
||||
timeout=settings.auth_timeout_seconds,
|
||||
)
|
||||
|
||||
if response.status_code == 401:
|
||||
|
||||
@@ -6,14 +6,14 @@ import io
|
||||
import os
|
||||
|
||||
from ..database import get_db_cursor
|
||||
from ..config import settings
|
||||
from ..models import TokenPayload
|
||||
from .auth import get_current_user, is_admin
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Directory paths
|
||||
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "template")
|
||||
RECORD_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "record")
|
||||
# Base directory for resolving relative paths
|
||||
BASE_DIR = os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
|
||||
|
||||
def fill_template_workbook(
|
||||
@@ -186,8 +186,12 @@ async def export_meeting(
|
||||
)
|
||||
actions = cursor.fetchall()
|
||||
|
||||
# Get directory paths from config
|
||||
template_dir = settings.get_template_dir(BASE_DIR)
|
||||
record_dir = settings.get_record_dir(BASE_DIR)
|
||||
|
||||
# Check for template file
|
||||
template_path = os.path.join(TEMPLATE_DIR, "meeting_template.xlsx")
|
||||
template_path = os.path.join(template_dir, "meeting_template.xlsx")
|
||||
if os.path.exists(template_path):
|
||||
# Load and fill template
|
||||
wb = load_workbook(template_path)
|
||||
@@ -204,10 +208,10 @@ async def export_meeting(
|
||||
filename = f"meeting_{meeting.get('uuid', meeting_id)}.xlsx"
|
||||
|
||||
# Ensure record directory exists
|
||||
os.makedirs(RECORD_DIR, exist_ok=True)
|
||||
os.makedirs(record_dir, exist_ok=True)
|
||||
|
||||
# Save to record directory
|
||||
record_path = os.path.join(RECORD_DIR, filename)
|
||||
record_path = os.path.join(record_dir, filename)
|
||||
wb.save(record_path)
|
||||
|
||||
# Save to bytes buffer for download
|
||||
|
||||
Reference in New Issue
Block a user