feat: consolidate env config and add deployment files
- Add debug_font_path, demo_docs_dir, e2e_api_base_url to config.py - Fix hardcoded paths in pp_structure_debug.py, create_demo_images.py - Fix hardcoded paths in test files - Update .env.example with new configuration options - Update .gitignore to exclude AI development files (.claude/, openspec/, AGENTS.md, CLAUDE.md) - Add production startup script (start-prod.sh) - Add README.md with project documentation - Add 1panel Docker deployment files (docker-compose.yml, Dockerfiles, nginx.conf) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,21 +7,43 @@ from typing import List, Optional
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field, model_validator
|
||||
from pathlib import Path
|
||||
import platform
|
||||
from shutil import which
|
||||
|
||||
# Anchor all default paths to the backend directory to avoid scattering runtime folders
|
||||
BACKEND_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
PROJECT_ROOT = BACKEND_ROOT.parent
|
||||
|
||||
|
||||
def _default_pandoc_path() -> str:
|
||||
return which("pandoc") or "/usr/bin/pandoc"
|
||||
|
||||
|
||||
def _default_font_dir() -> str:
|
||||
candidates = []
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
candidates.extend(["/System/Library/Fonts", "/Library/Fonts"])
|
||||
elif system == "Windows":
|
||||
candidates.append(r"C:\Windows\Fonts")
|
||||
else:
|
||||
candidates.extend(["/usr/share/fonts", "/usr/local/share/fonts"])
|
||||
|
||||
for path in candidates:
|
||||
if Path(path).exists():
|
||||
return path
|
||||
return candidates[0] if candidates else ""
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings loaded from environment variables"""
|
||||
|
||||
# ===== Database Configuration =====
|
||||
mysql_host: str = Field(default="mysql.theaken.com")
|
||||
mysql_port: int = Field(default=33306)
|
||||
mysql_user: str = Field(default="A060")
|
||||
mysql_host: str = Field(default="localhost")
|
||||
mysql_port: int = Field(default=3306)
|
||||
mysql_user: str = Field(default="")
|
||||
mysql_password: str = Field(default="")
|
||||
mysql_database: str = Field(default="db_A060")
|
||||
mysql_database: str = Field(default="")
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
@@ -32,14 +54,16 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
# ===== Application Configuration =====
|
||||
backend_host: str = Field(default="0.0.0.0")
|
||||
backend_port: int = Field(default=8000)
|
||||
frontend_host: str = Field(default="0.0.0.0")
|
||||
frontend_port: int = Field(default=5173)
|
||||
secret_key: str = Field(default="your-secret-key-change-this")
|
||||
algorithm: str = Field(default="HS256")
|
||||
access_token_expire_minutes: int = Field(default=1440) # 24 hours
|
||||
|
||||
# ===== External Authentication Configuration =====
|
||||
external_auth_api_url: str = Field(default="https://pj-auth-api.vercel.app")
|
||||
external_auth_api_url: str = Field(default="https://your-auth-api.example.com")
|
||||
external_auth_endpoint: str = Field(default="/api/auth/login")
|
||||
external_auth_timeout: int = Field(default=30)
|
||||
token_refresh_buffer: int = Field(default=300) # Refresh tokens 5 minutes before expiry
|
||||
@@ -441,8 +465,8 @@ class Settings(BaseSettings):
|
||||
result_dir: str = Field(default=str(BACKEND_ROOT / "storage" / "results"))
|
||||
|
||||
# ===== PDF Generation Configuration =====
|
||||
pandoc_path: str = Field(default="/opt/homebrew/bin/pandoc")
|
||||
font_dir: str = Field(default="/System/Library/Fonts")
|
||||
pandoc_path: str = Field(default_factory=_default_pandoc_path)
|
||||
font_dir: str = Field(default_factory=_default_font_dir)
|
||||
pdf_page_size: str = Field(default="A4")
|
||||
pdf_margin_top: int = Field(default=20)
|
||||
pdf_margin_bottom: int = Field(default=20)
|
||||
@@ -456,7 +480,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# ===== Translation Configuration (DIFY API) =====
|
||||
enable_translation: bool = Field(default=True)
|
||||
dify_base_url: str = Field(default="https://dify.theaken.com/v1")
|
||||
dify_base_url: str = Field(default="https://your-dify-instance.example.com/v1")
|
||||
dify_api_key: str = Field(default="") # Required: set in .env.local
|
||||
dify_timeout: float = Field(default=120.0) # seconds
|
||||
dify_max_retries: int = Field(default=3)
|
||||
@@ -487,6 +511,23 @@ class Settings(BaseSettings):
|
||||
log_level: str = Field(default="INFO")
|
||||
log_file: str = Field(default=str(BACKEND_ROOT / "logs" / "app.log"))
|
||||
|
||||
# ===== Development & Testing Configuration =====
|
||||
# Debug font path for visualization scripts (pp_structure_debug, create_demo_images)
|
||||
debug_font_path: str = Field(
|
||||
default="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
description="Font path for debug visualization scripts"
|
||||
)
|
||||
# Demo documents directory for testing
|
||||
demo_docs_dir: str = Field(
|
||||
default=str(PROJECT_ROOT / "demo_docs"),
|
||||
description="Directory containing demo documents for testing"
|
||||
)
|
||||
# E2E test API base URL
|
||||
e2e_api_base_url: str = Field(
|
||||
default="http://localhost:8000/api/v2",
|
||||
description="Base URL for E2E tests"
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _normalize_paths(self):
|
||||
"""Resolve all runtime paths to backend-rooted absolutes"""
|
||||
|
||||
@@ -530,7 +530,7 @@ if __name__ == "__main__":
|
||||
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
host=settings.backend_host,
|
||||
port=settings.backend_port,
|
||||
reload=True,
|
||||
log_level=settings.log_level.lower(),
|
||||
|
||||
@@ -1336,31 +1336,31 @@ class PriorityOperationQueue:
|
||||
# Wait for an item
|
||||
if not self._queue:
|
||||
if timeout is not None:
|
||||
result = self._condition.wait_for(
|
||||
lambda: len(self._queue) > 0,
|
||||
timeout=timeout
|
||||
)
|
||||
result = self._condition.wait_for(lambda: len(self._queue) > 0, timeout=timeout)
|
||||
if not result:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
# Get highest priority item
|
||||
neg_priority, _, item_id, data = heapq.heappop(self._queue)
|
||||
priority = BatchPriority(-neg_priority)
|
||||
# Keep popping until we find a non-cancelled item (or queue is exhausted)
|
||||
while self._queue:
|
||||
neg_priority, _, item_id, data = heapq.heappop(self._queue)
|
||||
priority = BatchPriority(-neg_priority)
|
||||
|
||||
# Skip if cancelled
|
||||
if item_id in self._cancelled:
|
||||
self._cancelled.discard(item_id)
|
||||
self._total_cancelled += 1
|
||||
if item_id in self._cancelled:
|
||||
self._cancelled.discard(item_id)
|
||||
self._total_cancelled += 1
|
||||
self._condition.notify()
|
||||
continue
|
||||
|
||||
self._total_dequeued += 1
|
||||
self._condition.notify()
|
||||
return self.dequeue(timeout=0) # Try next item
|
||||
logger.debug(f"Dequeued operation {item_id} with priority {priority.name}")
|
||||
return item_id, data, priority
|
||||
|
||||
self._total_dequeued += 1
|
||||
self._condition.notify()
|
||||
return None
|
||||
|
||||
logger.debug(f"Dequeued operation {item_id} with priority {priority.name}")
|
||||
return item_id, data, priority
|
||||
return None
|
||||
|
||||
def cancel(self, item_id: str) -> bool:
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,7 @@ from datetime import datetime
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from app.utils.bbox_utils import normalize_bbox
|
||||
from app.core.config import BACKEND_ROOT, settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -186,12 +187,13 @@ class PPStructureDebug:
|
||||
|
||||
# Try to load a font, fall back to default
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 14)
|
||||
small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
|
||||
font = ImageFont.truetype(settings.debug_font_path, 14)
|
||||
small_font = ImageFont.truetype(settings.debug_font_path, 10)
|
||||
except (IOError, OSError):
|
||||
try:
|
||||
font = ImageFont.truetype("/home/egg/project/Tool_OCR/backend/fonts/NotoSansSC-Regular.ttf", 14)
|
||||
small_font = ImageFont.truetype("/home/egg/project/Tool_OCR/backend/fonts/NotoSansSC-Regular.ttf", 10)
|
||||
noto_font = BACKEND_ROOT / "fonts" / "NotoSansSC-Regular.ttf"
|
||||
font = ImageFont.truetype(str(noto_font), 14)
|
||||
small_font = ImageFont.truetype(str(noto_font), 10)
|
||||
except (IOError, OSError):
|
||||
font = ImageFont.load_default()
|
||||
small_font = font
|
||||
|
||||
Reference in New Issue
Block a user