import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.responses import HTMLResponse, FileResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from app.config import settings from app.api.v1.audio import router as audio_router from app.api.v1.tasks import router as tasks_router from app.api.v1.multitrack import router as multitrack_router from app.api.v1.auth import router as auth_router from app.api.v1.admin import router as admin_router from app.api.v1.projects import router as projects_router from app.api.v1.user_config import router as user_config_router from app.api.v1.ai_proxy import router as ai_proxy_router from app.api.v1.ai_presets import router as ai_presets_router from app.api.v1.plugins import router as plugins_router from app.api.v1.media import router as media_router from app.core.auth import seed_admin from app.core.soundfont_scanner import SoundFontAutoScanner # Ensure storage directories exist os.makedirs(settings.UPLOADS_DIR, exist_ok=True) os.makedirs(settings.PROCESSED_DIR, exist_ok=True) _SF_SCANNER_STOP = None @asynccontextmanager async def lifespan(app: FastAPI): # Startup seed_admin() scanner = SoundFontAutoScanner() global _SF_SCANNER_STOP _SF_SCANNER_STOP = scanner.start_background(interval=30) yield # Shutdown if _SF_SCANNER_STOP is not None: _SF_SCANNER_STOP.set() app = FastAPI(title="SonicForge API Engine", lifespan=lifespan) app.add_middleware(GZipMiddleware, minimum_size=500) # Auth is token/cookie based (no cookies required for CORS), so credentials are # disabled — "*" + allow_credentials=True is rejected by browsers anyway. app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=False, allow_methods=["*"], allow_headers=["*"], ) # Mount storage directory (must come before general /static mount) app.mount("/static/audio", StaticFiles(directory=settings.STORAGE_DIR), name="audio") # Mount app static files (js, css) # Dùng settings.APP_DIR (freeze-aware) thay vì os.path.dirname(__file__): # khi PyInstaller onefile, __file__ trỏ vào thư mục giải nén tạm _MEI... # nhưng static/templates nằm trong sys._MEIPASS/app (config.py đã xử lý). STATIC_DIR = os.path.join(settings.APP_DIR, "static") # ⚠️ Fallback an toàn: nếu vì lý do nào đó static không nằm đúng chỗ (vd # bundle thiếu file, chạy từ nơi khác), thử các vị trí khác; nếu vẫn không # có → TỰ TẠO thư mục rỗng để app KHÔNG crash khi khởi động (lỗi "Directory # does not exist" từ StaticFiles làm engine chết ngay lúc import — đã gặp). if not os.path.isdir(STATIC_DIR): for cand in [ os.path.join(getattr(sys, "_MEIPASS", ""), "app", "static"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "static"), ]: if cand and os.path.isdir(cand): STATIC_DIR = cand break else: try: os.makedirs(STATIC_DIR, exist_ok=True) except OSError: pass app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") # Include routers app.include_router(audio_router, prefix="/api/v1/audio", tags=["audio"]) app.include_router(tasks_router, prefix="/api/v1/audio", tags=["tasks"]) app.include_router(multitrack_router, prefix="/api/v1/multitrack", tags=["multitrack"]) app.include_router(auth_router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(admin_router, prefix="/api/v1/admin", tags=["admin"]) app.include_router(projects_router, prefix="/api/v1/projects", tags=["projects"]) app.include_router(user_config_router, prefix="/api/v1/user", tags=["user_config"]) app.include_router(ai_proxy_router, prefix="/api/v1/ai", tags=["ai"]) app.include_router(ai_presets_router, prefix="/api/v1/ai", tags=["ai"]) app.include_router(plugins_router, prefix="/api/v1/plugins", tags=["plugins"]) app.include_router(media_router, prefix="/api/v1/media", tags=["media"]) @app.get("/health") async def health(): return {"status": "ok"} @app.get("/", response_class=HTMLResponse) async def get_index(): index_path = os.path.join(settings.TEMPLATES_DIR, "index.html") if not os.path.exists(index_path): return HTMLResponse(content=f"

SonicForge Studio: index.html not found at {index_path}

", status_code=404) with open(index_path, "r", encoding="utf-8") as file: resp = HTMLResponse(content=file.read(), status_code=200) # no-cache: index.html PHẢI luôn mới (các bundle JS dùng ?v= để bust) — # nếu browser cache HTML cũ → stamp cũ → tải bundle cũ (bug "không load # được bundle mới" ở incognito — cache heuristic không có Cache-Control). resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return resp @app.get("/favicon.svg") async def get_favicon(): favicon_path = os.path.join(settings.TEMPLATES_DIR, "favicon.svg") if os.path.exists(favicon_path): return FileResponse(favicon_path, media_type="image/svg+xml") return HTMLResponse(content="", status_code=404) @app.get("/ai-prompt-generator", response_class=HTMLResponse) async def get_ai_prompt_generator(): md_path = os.path.join(settings.BASE_DIR, "md", "49_AI_PROMPT_GENERATOR.md") if not os.path.exists(md_path): return HTMLResponse(content="

File not found

", status_code=404) with open(md_path, "r", encoding="utf-8") as file: return HTMLResponse(content=file.read(), status_code=200)