Files
DPM/backend/app/main.py
T
Pine 9f64b4273b feat(s2s): 后端嵌入实时语音栈(云 ASR/LLM/TTS + 本地 VAD)
- s2s_bridge:同进程构建 s2s 管线池(silero VAD + dashscope ASR + qwen-plus + qwen3-tts)并起 RealtimeServer(:8765)
- ThreadManager 启动管线 handler 线程;启动重试与完整配置日志
- config:DPM_S2S_* 系列配置(开关/端口/模型/重试/日志级别)
- main lifespan 按 DPM_S2S_ENABLED 启停;pyproject 增加 s2s 依赖(httpx/nltk/scipy/torch 等)
2026-08-18 01:37:48 +08:00

121 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""OPC 智能园区后端 —— FastAPI 入口
启动:uvicorn app.main:app --host 0.0.0.0 --port 8000
或:python main.py
"""
import asyncio
import logging
import threading
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from .admin import admin_login, admin_logout, admin_page
from .config import settings
from .event_bus import bus
from .mqtt import hub
from .routers import router
from .sim_engine import sim_engine
from .s2s_bridge import start_s2s_backend
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s")
log = logging.getLogger("dpm.main")
DIST = Path(settings.DIST_DIR)
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动准备
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
bus.set_loop(asyncio.get_running_loop())
hub.start()
stop = threading.Event()
# s2s 实时语音栈(可选):后台线程构建管线池并起 RealtimeServer(:8765)
if settings.S2S_ENABLED:
start_s2s_backend(stop)
def tick_loop():
"""数据引擎每 2.2s 推进一次;MQTT 连接时同步推送快照"""
while not stop.is_set():
try:
snap = sim_engine.tick()
hub.publish_tick(snap)
except Exception as e: # noqa: BLE001
log.warning("数据 tick 异常: %s", e)
stop.wait(settings.MQTT_TICK_INTERVAL)
threading.Thread(target=tick_loop, daemon=True).start()
log.info("OPC 智能园区后端启动于 http://%s:%s", settings.HOST, settings.PORT)
if settings.MQTT_ENABLED:
log.info("MQTT Broker: %s:%s(前端 WS: %s", settings.MQTT_HOST, settings.MQTT_PORT, settings.MQTT_WS_URL)
try:
yield
finally:
stop.set()
hub.stop()
log.info("OPC 智能园区后端已停止")
app = FastAPI(title="OPC 智能园区后端", version="1.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def no_cache_admin_static(request: Request, call_next):
"""管理后台页面与静态资源禁用缓存,避免旧 JS/CSS 残留导致按钮无响应"""
response = await call_next(request)
if request.url.path.startswith(("/static/", "/admin")):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
app.include_router(router)
# 媒体资源静态服务(上传/播放的文件)
settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/file", StaticFiles(directory=str(settings.MEDIA_DIR)), name="media")
# 管理后台静态资源(admin.css / admin.js
_STATIC_DIR = Path(__file__).resolve().parent.parent / "static"
app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
# 管理后台(FastAPI + Jinja2 服务端渲染)—— 必须在 SPA 回退之前注册
app.add_api_route("/admin", admin_page, methods=["GET"], include_in_schema=False)
app.add_api_route("/admin/login", admin_login, methods=["POST"], include_in_schema=False)
app.add_api_route("/admin/logout", admin_logout, methods=["GET"], include_in_schema=False)
# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ----------
@app.get("/{path:path}", include_in_schema=False)
async def spa_fallback(path: str):
if not DIST.exists():
return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404)
dist = DIST.resolve()
if not path:
target = dist / "index.html"
else:
target = (dist / path).resolve()
if target.is_file() and target.is_relative_to(dist):
return FileResponse(target)
index = dist / "index.html"
if index.exists():
return FileResponse(index)
return JSONResponse({"detail": "Not Found"}, status_code=404)