FYP_ASR_Service / main.py
HK0712's picture
ms to s
9418793
# 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 管理器
@asynccontextmanager
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
@app.middleware("http")
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
@app.get("/health", status_code=200)
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 端點定義 (不變)
@api_router.post("/recognize")
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)