File size: 8,338 Bytes
a965b51
211b028
a965b51
 
 
 
211b028
a309cba
414b4aa
 
211b028
914c60c
 
414b4aa
a965b51
914c60c
414b4aa
211b028
a309cba
 
 
 
 
 
 
 
a965b51
 
 
 
 
 
 
211b028
a965b51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914c60c
a965b51
 
 
 
914c60c
a965b51
 
914c60c
a965b51
 
914c60c
a965b51
914c60c
 
a965b51
914c60c
a965b51
 
 
 
914c60c
a965b51
 
 
 
 
 
 
 
914c60c
 
 
 
 
 
 
 
 
9418793
914c60c
 
 
 
 
 
 
 
 
 
9418793
914c60c
 
 
c465cb2
 
 
 
a965b51
414b4aa
 
 
 
 
 
 
 
211b028
a965b51
 
 
 
 
211b028
a965b51
414b4aa
 
 
211b028
a965b51
914c60c
414b4aa
 
 
914c60c
414b4aa
 
914c60c
a965b51
414b4aa
914c60c
a965b51
211b028
a965b51
414b4aa
211b028
414b4aa
211b028
414b4aa
211b028
414b4aa
 
 
914c60c
211b028
 
 
414b4aa
 
211b028
914c60c
414b4aa
211b028
 
 
 
a965b51
414b4aa
211b028
a965b51
 
 
 
211b028
 
a965b51
 
211b028
a965b51
211b028
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# 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)