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:
egg
2025-12-14 14:31:55 +08:00
parent 43c413c5ce
commit 01aee1fd0d
19 changed files with 1460 additions and 311 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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,

View File

@@ -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}")

View File

@@ -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:

View File

@@ -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