Spaces:
Running
Running
| # main.py | |
| # ======================================================================= | |
| # 1. 匯入區 (Imports) | |
| # 所有需要的模組都放在檔案的最頂部。 | |
| # ======================================================================= | |
| import os | |
| import logging | |
| import importlib | |
| import uvicorn | |
| import shutil | |
| import re | |
| import time # <--- 【新增】時間計算 | |
| from datetime import datetime | |
| from contextlib import asynccontextmanager | |
| from fastapi import FastAPI, File, UploadFile, Form, HTTPException, APIRouter, Request | |
| from fastapi.middleware.cors import CORSMiddleware | |
| class HealthCheckFilter(logging.Filter): | |
| def filter(self, record: logging.LogRecord) -> bool: | |
| # record.getMessage() 會包含像 "GET /health HTTP/1.1" 這樣的訊息 | |
| return record.getMessage().find("/health") == -1 | |
| # 將過濾器應用到 uvicorn 的存取日誌 | |
| logging.getLogger("uvicorn.access").addFilter(HealthCheckFilter()) | |
| # ======================================================================= | |
| # 2. 全域變數與配置區 (Global Variables & Config) | |
| # 在應用程式啟動前,定義好所有全域會用到的變數。 | |
| # ======================================================================= | |
| # 【【修改】】不再硬編碼,讓它們在啟動時被動態填充 | |
| SUPPORTED_LANGUAGES = [] | |
| ANALYZERS = {} | |
| # 【【新增】】讀取環境變數「信號旗」 | |
| APP_ENV = os.getenv('APP_ENV', 'development') | |
| # 【【新增】】這個臨時目錄的定義也放在這裡,保持整潔 | |
| TEMP_DIR = "/tmp/temp_audio" | |
| if not os.path.exists(TEMP_DIR): | |
| os.makedirs(TEMP_DIR) | |
| # ======================================================================= | |
| # 3. 應用程式生命週期管理區 (Application Lifespan Management) | |
| # 這是最核心的新增部分,它控制著應用程式啟動和關閉時的行為。 | |
| # ======================================================================= | |
| # 【【新增】】動態掃描 analyzer/ 資料夾的輔助函數 | |
| def discover_supported_languages(): | |
| global SUPPORTED_LANGUAGES | |
| analyzer_dir = 'analyzer' | |
| pattern = re.compile(r"ASR_(.+)\.py$") | |
| try: | |
| for filename in os.listdir(analyzer_dir): | |
| match = pattern.match(filename) | |
| if match: | |
| language_code = match.group(1) | |
| SUPPORTED_LANGUAGES.append(language_code) | |
| if not SUPPORTED_LANGUAGES: | |
| print("WARNING: No language analyzer modules found in 'analyzer' directory.") | |
| else: | |
| print(f"Discovered supported languages: {', '.join(SUPPORTED_LANGUAGES)}") | |
| except FileNotFoundError: | |
| print(f"ERROR: The '{analyzer_dir}' directory was not found.") | |
| SUPPORTED_LANGUAGES = [] | |
| # 【【新增】】FastAPI 的 lifespan 管理器 | |
| async def lifespan(app: FastAPI): | |
| # --- 應用程式啟動時執行的程式碼 --- | |
| print("="*60) | |
| print(f"🚀 Application starting up in '{APP_ENV}' mode.") | |
| discover_supported_languages() # 首先,動態發現支援的語言 | |
| if APP_ENV == "production": | |
| print("🏭 Production mode detected. Eager loading all models...") | |
| for lang in SUPPORTED_LANGUAGES: | |
| try: | |
| print(f"⏳ Loading model for language: {lang} ...") | |
| analyzer_module = importlib.import_module(f"analyzer.ASR_{lang}") | |
| ANALYZERS[lang] = analyzer_module | |
| print(f"✅ Model for {lang} loaded successfully.") | |
| except Exception as e: | |
| print(f"❌ FATAL: Failed to load model for {lang}. Error: {e}") | |
| print("✨ All models pre-loaded. Application is ready for requests.") | |
| else: | |
| print("🛠️ Development mode detected. Models will be loaded on-demand (lazily).") | |
| print("="*60) | |
| yield # <--- 應用程式在這裡運行 | |
| # --- 應用程式關閉時執行的程式碼 --- | |
| print("🛑 Application shutting down.") | |
| # ======================================================================= | |
| # 4. FastAPI 應用程式實例化與中介軟體設定區 | |
| # 創建 FastAPI app 物件,並掛載所有需要的設定。 | |
| # ======================================================================= | |
| # 【【修改】】告訴 FastAPI 使用我們上面定義的 lifespan | |
| app = FastAPI(title="Pronunciation Analysis API", lifespan=lifespan) | |
| # 【【新增】】Emoji 日誌 Middleware | |
| async def log_requests(request: Request, call_next): | |
| # 排除 health check 避免刷屏 | |
| if "/health" in request.url.path: | |
| return await call_next(request) | |
| start_time = time.time() | |
| response = await call_next(request) | |
| process_time = time.time() - start_time | |
| # 根據狀態碼選擇 Emoji | |
| if 200 <= response.status_code < 300: | |
| status_emoji = "✅" | |
| elif 400 <= response.status_code < 500: | |
| status_emoji = "⚠️ " | |
| else: | |
| status_emoji = "💥" | |
| # 格式: [Emoji] [Method] [Path] [Status] - [Time] | |
| print(f"{status_emoji} {request.method} {request.url.path} {response.status_code} - {process_time:.2f}s") | |
| return response | |
| def health_check(): | |
| return {"status": "ok"} | |
| # CORS 中介軟體設定 (不變) | |
| origins = ["*"] | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ======================================================================= | |
| # 5. 核心業務邏輯與 API 端點區 | |
| # 定義所有 API 的路由和處理函數。 | |
| # ======================================================================= | |
| api_router = APIRouter(prefix="/api/v1") | |
| # 【【修改】】這是我們新的、更簡潔的 get_analyzer_module 函數 | |
| def get_analyzer_module(language: str): | |
| if language in ANALYZERS: | |
| return ANALYZERS[language] | |
| # 這段程式碼只會在開發模式下被執行 | |
| print(f"🐢 '{language}' not in cache. Loading on-demand (development mode)...") | |
| try: | |
| analyzer_module = importlib.import_module(f"analyzer.ASR_{language}") | |
| ANALYZERS[language] = analyzer_module | |
| print(f"⚡ '{language}' analyzer loaded and cached successfully.") | |
| return analyzer_module | |
| except ImportError: | |
| print(f"❌ Error: Analyzer module for '{language}' not found (analyzer.ASR_{language}.py).") | |
| raise HTTPException(status_code=400, detail=f"Unsupported language: {language}") | |
| except Exception as e: | |
| print(f"💥 Error: A critical error occurred while loading the model for '{language}': {e}") | |
| raise HTTPException(status_code=500, detail=f"Failed to load model for language '{language}'.") | |
| # API 端點定義 (不變) | |
| async def recognize_speech_api( | |
| file: UploadFile = File(...), | |
| target_sentence: str = Form(...), | |
| language: str = Form(...) | |
| ): | |
| analyzer_module = get_analyzer_module(language) | |
| base_filename = os.path.basename(file.filename) | |
| temp_file_path = os.path.join(TEMP_DIR, f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{base_filename}") | |
| print(f"🎤 Processing file: {temp_file_path} with '{language}' analyzer.") | |
| try: | |
| with open(temp_file_path, "wb") as buffer: | |
| shutil.copyfileobj(file.file, buffer) | |
| result = analyzer_module.analyze(temp_file_path, target_sentence) | |
| return result | |
| except Exception as e: | |
| print(f"💥 An unexpected error occurred during request processing: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| finally: | |
| if os.path.exists(temp_file_path): | |
| os.remove(temp_file_path) | |
| # 將路由掛載到主應用上 | |
| app.include_router(api_router) | |
| # ======================================================================= | |
| # 6. 主程式入口 (Main Entry Point) | |
| # 當您直接運行 `python main.py` 時,這部分程式碼會被執行。 | |
| # ======================================================================= | |
| if __name__ == "__main__": | |
| print("="*60) | |
| print("Starting FastAPI server in development mode (http://localhost:8000 )...") | |
| print("Run ngrok manually if needed: ngrok http 8000" ) | |
| print("="*60) | |
| # uvicorn 會自動找到 app 物件並運行它 | |
| uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) | |