from __future__ import annotations import asyncio from contextlib import asynccontextmanager from typing import List from fastapi import FastAPI from loguru import logger from app.logging_config import setup_logging from app.media_static import MediaStaticFiles from app.db import init_db from app.routers import biomass, debug, ingest, zed from app.services.action_watch import run_action_watch_loop from app.services.measure_watch import run_measure_watch_loop from app.services.sonar_video import run_sonar_video_watch_loop from app.settings import get_settings # 第一次:仅装 stderr sink(lifespan 启动时再装文件 sink)。 setup_logging() @asynccontextmanager async def lifespan(app: FastAPI): s = get_settings() # 第二次:lifespan 内部用 Settings 装 runtime/events 文件 sink。 setup_logging(s, force=True) init_db(s) s.media_root.mkdir(parents=True, exist_ok=True) s.stream_tmp_dir.mkdir(parents=True, exist_ok=True) logger.bind(pipeline="fish_api").info( "[fish_api] 启动完成 | 公网基址={} | 日志目录={} | 文件级别={} | 保留={} 天 | rotation={}", s.public_base_url, s.log_dir, (s.log_file_level or s.log_level).upper(), s.log_retention_days, s.log_rotation, ) logger.bind(pipeline="fish_api").info( "[fish_api] 后台监控配置 | action_watch={} | measure_watch={} | sonar_watch={}", s.action_watch_dir or "(未启用)", s.measure_watch_dir or "(未启用)", s.biomass_sonar_video_dir or "(未启用)", ) tasks = [] # type: List[asyncio.Task] if s.action_watch_dir is not None: tasks.append(asyncio.create_task(run_action_watch_loop(s))) if s.measure_watch_dir is not None: tasks.append(asyncio.create_task(run_measure_watch_loop(s))) if s.biomass_sonar_video_dir is not None: tasks.append(asyncio.create_task(run_sonar_video_watch_loop(s))) yield logger.bind(pipeline="fish_api").info("[fish_api] 收到关停信号,停止后台监控任务") for t in tasks: t.cancel() for t in tasks: try: await t except asyncio.CancelledError: pass logger.bind(pipeline="fish_api").info("[fish_api] 已优雅停止") app = FastAPI(title="Fish API", lifespan=lifespan) app.include_router(ingest.router) app.include_router(biomass.router) app.include_router(debug.router) app.include_router(zed.router) _settings = get_settings() _settings.media_root.mkdir(parents=True, exist_ok=True) app.mount( "/media", MediaStaticFiles(directory=str(_settings.media_root)), name="media", ) @app.get("/") async def root(): return { "service": "fish-api", "docs": "/docs", "ingest": "/api/v1/ingest/", "biomass_camera": "/api/v1/biomass/real/camera/", "biomass_health": "/api/v1/biomass/health/result/", "biomass_water_video": "/api/v1/biomass/water/video/", "biomass_sonar_video": "/api/v1/biomass/sonar/video/", "debug_measure": "/api/v1/debug/meause", "zed_recording": "/api/v1/zed/recording/start|stop|status", "note": "若配置了 ACTION_WATCH_DIR / MEASURE_WATCH_DIR,启动后会后台监控对应目录。ZED 分段录制由独立进程/脚本负责,不由 fish_api 启停;可选 HTTP /api/v1/zed/recording/start|stop|status。", }