File size: 7,568 Bytes
cd5aabe
 
 
 
7396c20
 
 
cd5aabe
7396c20
cd5aabe
7396c20
 
cd5aabe
 
 
 
 
 
 
 
 
 
 
017b111
cd5aabe
 
4d8766b
cd5aabe
 
fae1594
017b111
 
 
 
 
 
 
 
 
 
 
 
4d8766b
fae1594
 
 
 
 
 
 
 
 
 
cd5aabe
 
7c323a5
cd5aabe
 
 
 
 
7c323a5
 
 
 
 
 
 
 
 
 
 
 
 
cd5aabe
 
 
 
 
 
 
 
 
 
 
d11ff01
cd5aabe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7396c20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd5aabe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import os
import time
from contextlib import asynccontextmanager

import base64
import secrets

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response, PlainTextResponse

from cleanup_scheduler import start_cleanup_scheduler, stop_cleanup_scheduler
from config import (
    logger,
    OUTPUT_DIR,
    DEEPFACE_AVAILABLE,
    DLIB_AVAILABLE,
    MODELS_PATH,
    IMAGES_DIR,
    YOLO_AVAILABLE,
    ENABLE_LOGGING,
    HUGGINGFACE_SYNC_ENABLED,
)
from database import close_mysql_pool, init_mysql_pool
from utils import ensure_bos_resources, ensure_huggingface_models

logger.info("Starting to import api_routes module...")

if HUGGINGFACE_SYNC_ENABLED:
    try:
        t_hf_start = time.perf_counter()
        if not ensure_huggingface_models():
            raise RuntimeError("无法从 HuggingFace 同步模型,请检查配置与网络")
        hf_time = time.perf_counter() - t_hf_start
        logger.info("HuggingFace 模型同步完成,用时 %.3fs", hf_time)
    except Exception as exc:
        logger.error(f"HuggingFace model preparation failed: {exc}")
        raise
else:
    logger.info("已关闭 HuggingFace 模型同步开关,跳过启动阶段的同步步骤")

try:
    t_bos_start = time.perf_counter()
    if not ensure_bos_resources():
        raise RuntimeError("无法从 BOS 同步模型与数据,请检查凭证与网络")
    bos_time = time.perf_counter() - t_bos_start
    logger.info(f"BOS resources synchronized successfully, time: {bos_time:.3f}s")
except Exception as exc:
    logger.error(f"BOS resource preparation failed: {exc}")
    raise

try:
    t_start = time.perf_counter()
    from api_routes import api_router, extract_chinese_celeb_dataset_sync
    import_time = time.perf_counter() - t_start
    logger.info(f"api_routes module imported successfully, time: {import_time:.3f}s")
except Exception as e:
    import_time = time.perf_counter() - t_start
    logger.error(f"api_routes module import failed, time: {import_time:.3f}s, error: {e}")
    raise

try:
    t_extract_start = time.perf_counter()
    extract_result = extract_chinese_celeb_dataset_sync()
    extract_time = time.perf_counter() - t_extract_start
    logger.info(
        "Chinese celeb dataset extracted successfully, time: %.3fs, target: %s",
        extract_time,
        extract_result.get("target_dir"),
    )
except Exception as exc:
    logger.error(f"Failed to extract Chinese celeb dataset automatically: {exc}")
    raise


@asynccontextmanager
async def lifespan(app: FastAPI):
    start_time = time.perf_counter()
    logger.info("FaceScore service starting...")
    logger.info(f"Output directory: {OUTPUT_DIR}")
    logger.info(f"DeepFace available: {DEEPFACE_AVAILABLE}")
    logger.info(f"YOLO available: {YOLO_AVAILABLE}")
    logger.info(f"MediaPipe available: {DLIB_AVAILABLE}")
    logger.info(f"Archive directory: {IMAGES_DIR}")
    os.makedirs(OUTPUT_DIR, exist_ok=True)

    # 初始化数据库连接池
    try:
      await init_mysql_pool()
      logger.info("MySQL 连接池初始化完成")
    except Exception as exc:
      logger.error(f"初始化 MySQL 连接池失败: {exc}")
      raise

    # 启动图片清理定时任务
    logger.info("Starting image cleanup scheduled task...")
    try:
        start_cleanup_scheduler()
        logger.info("Image cleanup scheduled task started successfully")
    except Exception as e:
        logger.error(f"Failed to start image cleanup scheduled task: {e}")

    # 记录启动完成时间
    total_startup_time = time.perf_counter() - start_time
    logger.info(f"FaceScore service startup completed, total time: {total_startup_time:.3f}s")

    yield

    # 应用关闭时停止定时任务
    logger.info("Stopping image cleanup scheduled task...")
    try:
        stop_cleanup_scheduler()
        logger.info("Image cleanup scheduled task stopped")
    except Exception as e:
        logger.error(f"Failed to stop image cleanup scheduled task: {e}")

    # 关闭数据库连接池
    try:
      await close_mysql_pool()
    except Exception as exc:
      logger.warning(f"关闭 MySQL 连接池失败: {exc}")


# 创建 FastAPI 应用
app = FastAPI(
    title="Enhanced FaceScore 服务",
    description="支持多模型的人脸分析REST API服务,包含五官评分功能。支持混合模式:HowCuteAmI(颜值+性别)+ DeepFace(年龄+情绪)",
    version="3.0.0",
    docs_url="/cp_docs",
    redoc_url="/cp_redoc",
    lifespan=lifespan,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# 注册路由
app.include_router(api_router)


_default_frontend_dir = os.path.abspath(
    os.path.join(os.path.dirname(__file__), "facelist-web")
)
FRONTEND_DIR = os.path.abspath(
    os.path.expanduser(os.environ.get("FACELIST_FRONTEND_DIR", _default_frontend_dir))
)

_basic_user = os.environ.get("FACELIST_BASIC_USER", "admin")
_basic_pass = os.environ.get("FACELIST_BASIC_PASS", "admin")
_basic_secret = f"{_basic_user}:{_basic_pass}"
_basic_token = "Basic " + base64.b64encode(_basic_secret.encode()).decode()


class FacelistBasicAuthMiddleware(BaseHTTPMiddleware):
    """仅保护 /facelist 前缀的 Basic Auth"""

    async def dispatch(self, request, call_next):
        path = request.url.path
        if path.startswith("/facelist"):
            auth = request.headers.get("Authorization", "")
            if not secrets.compare_digest(auth, _basic_token):
                return PlainTextResponse(
                    "Unauthorized",
                    status_code=401,
                    headers={"WWW-Authenticate": 'Basic realm="facelist"'},
                )
        return await call_next(request)


class SPAStaticFiles(StaticFiles):
    """支持SPA路由回退到index.html的静态文件服务"""

    async def get_response(self, path: str, scope):
        response = await super().get_response(path, scope)
        if response.status_code == 404:
            # 未匹配到具体文件时回退到前端入口
            response = await super().get_response("index.html", scope)
        return response

app.add_middleware(FacelistBasicAuthMiddleware)

if os.path.isdir(FRONTEND_DIR):
    app.mount(
        "/facelist",
        SPAStaticFiles(directory=FRONTEND_DIR, html=True),
        name="facelist",
    )
    logger.info("前端静态资源已挂载在 /facelist ,目录: %s", FRONTEND_DIR)
else:
    logger.warning("未找到前端目录 %s ,跳过前端静态资源挂载", FRONTEND_DIR)

# 添加根路径处理
@app.get("/")
async def root():
    return "UP"


if __name__ == "__main__":
    import uvicorn

    if not os.path.exists(MODELS_PATH):
        logger.critical(
            "Warning: 'models' directory not found. Please ensure it exists and contains model files."
        )
        logger.critical(
            "Exiting application as FaceAnalyzer cannot be initialized without models."
        )
        exit(1)

    # 根据日志开关配置 Uvicorn 日志
    if ENABLE_LOGGING:
        uvicorn.run(app, host="0.0.0.0", port=8080, reload=False)
    else:
        # 禁用 Uvicorn 的访问日志和错误日志
        uvicorn.run(
            app,
            host="0.0.0.0",
            port=8080,
            reload=False,
            access_log=False,  # 禁用访问日志
            log_level="critical"  # 只显示严重错误
        )