From 66c0eb27c88bb1486728b6326b8894b06d904b55 Mon Sep 17 00:00:00 2001 From: wuzhuorong <973204353@qq.com> Date: Tue, 16 Jun 2026 11:41:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(server):=20=E5=AE=8C=E5=96=84=20LLM=20?= =?UTF-8?q?=E9=9B=86=E6=88=90=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD=E4=B8=8E?= =?UTF-8?q?=E8=A7=84=E5=88=99=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/.env.example | 63 +++++++++++++++++ apps/server/config/rules/fight.yaml | 10 +-- apps/server/main.py | 28 ++++++++ apps/server/services/event/__init__.py | 20 ++++-- apps/web/src/api/detection.js | 94 ++++++++++++++++++++++++++ 5 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 apps/server/.env.example diff --git a/apps/server/.env.example b/apps/server/.env.example new file mode 100644 index 0000000..edc779d --- /dev/null +++ b/apps/server/.env.example @@ -0,0 +1,63 @@ +# ============================================ +# JC Video Recognize - 服务端配置模板 +# ============================================ +# 使用方法: 复制本文件为 .env 并填入真实配置 +# cp .env.example .env +# ⚠️ 严禁将真实 API Key / 密码 提交到 Git! +# ============================================ + +# --- 服务器 --- +API_HOST=0.0.0.0 +API_PORT=8000 + +# --- 检测 --- +DETECTION_DEFAULT_CONFIDENCE=0.5 +DETECTION_MIN_CONFIDENCE=0.3 + +# --- LLM 二次判断 (MVP-3) --- +# 支持的 provider: openai (兼容协议) / mock (离线测试) +# 火山方舟接入示例: +# LLM_API_BASE=https://ark.cn-beijing.volces.com/api/v3 +# LLM_MODEL=<你在火山方舟控制台开通的 Endpoint ID> +LLM_ENABLED=false +LLM_PROVIDER=mock +LLM_API_BASE=https://ark.cn-beijing.volces.com/api/v3 +LLM_API_KEY=your-api-key-here +LLM_MODEL=doubao-vision-pro-32k +LLM_TIMEOUT=20.0 +LLM_MAX_RETRIES=2 +LLM_MAX_TOKENS=512 +LLM_TEMPERATURE=0.0 +LLM_MAX_CONCURRENCY=2 +LLM_IMAGE_MAX_SIDE=768 + +# --- LLM 触发器 (D26-D28) --- +# [生产模式] 推荐: HITS=3, CONFIDENCE=0.55, COOLDOWN=20.0 +# [测试模式] 调低阈值: HITS=1, CONFIDENCE=0.1, COOLDOWN=2.0 +LLM_TRIGGER_ENABLED=true +LLM_TRIGGER_WINDOW_SECONDS=3.0 +LLM_TRIGGER_MIN_CONSECUTIVE_HITS=3 +LLM_TRIGGER_MIN_AVG_CONFIDENCE=0.55 +LLM_TRIGGER_COOLDOWN_SECONDS=20.0 + +# --- 融合策略 (D30) --- +# strategy: weighted / conservative / llm_priority +FUSION_STRATEGY=weighted +FUSION_YOLO_WEIGHT=0.4 +FUSION_LLM_WEIGHT=0.6 +FUSION_SUPPRESS_ON_LLM_NEGATIVE=true +FUSION_FALLBACK_TO_YOLO=true + +# --- 成本追踪 / 降级 (D34) --- +# daily_budget_usd=0.0 表示不限制预算 +LLM_COST_DAILY_BUDGET_USD=0.0 +LLM_COST_ERROR_RATE_THRESHOLD=0.5 +LLM_COST_COOLDOWN_SECONDS=60.0 + +# --- 事件引擎 --- +EVENT_DEDUP_WINDOW_SECONDS=30.0 +EVENT_RULES_DIR=config/rules +EVENT_MAX_ACTIVE_EVENTS=1000 + +# --- MQTT (可选) --- +MQTT_ENABLED=false diff --git a/apps/server/config/rules/fight.yaml b/apps/server/config/rules/fight.yaml index 40989de..5b2d6e7 100644 --- a/apps/server/config/rules/fight.yaml +++ b/apps/server/config/rules/fight.yaml @@ -1,17 +1,19 @@ # 打架检测预警规则 (MVP-1) +# [测试模式] min_confidence 已调低到 0.3 用于触发测试 +# [生产模式] 推荐: high=0.55, critical=0.75 rules: - name: fight_high event_type: fight enabled: true - min_confidence: 0.55 + min_confidence: 0.3 severity: high - min_bbox_area: 800 + min_bbox_area: 100 description: 检测到打架/暴力行为,触发高级预警 - name: fight_critical_continuous event_type: fight enabled: true - min_confidence: 0.75 + min_confidence: 0.5 severity: critical - min_bbox_area: 1200 + min_bbox_area: 100 description: 检测到高置信度打架行为,立即触发最高级别预警 diff --git a/apps/server/main.py b/apps/server/main.py index e0a1b16..2e401c0 100644 --- a/apps/server/main.py +++ b/apps/server/main.py @@ -11,8 +11,13 @@ import logging from api import detection, models from api.alerts import get_broadcaster from api.rtsp import init_stream_manager +from api.llm import init_llm_api, router as llm_router +from api.rules import init_rules_api, router as rules_router +from core.settings import get_settings from services.model_service import ModelService from services.camera_service import CameraService +from services.llm_analysis_service import create_llm_service_from_settings +from services.llm_cost_tracker import init_global_tracker logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -74,6 +79,29 @@ async def lifespan(app: FastAPI): app.include_router(rtsp_router, prefix="/api") app.include_router(alerts_router, prefix="") # WebSocket 路径已带 /ws 前缀 + # 初始化 LLM 集成 (MVP-3) + settings = get_settings() + llm_service = create_llm_service_from_settings(settings) + cost_tracker = init_global_tracker( + daily_budget_usd=settings.llm_cost.daily_budget_usd, + history_days=settings.llm_cost.history_days, + recent_window=settings.llm_cost.recent_window, + error_rate_threshold=settings.llm_cost.error_rate_threshold, + cooldown_seconds=settings.llm_cost.cooldown_seconds, + ) + init_llm_api(llm_service, cost_tracker) + app.include_router(llm_router, prefix="/api") + logger.info( + "LLM 集成已加载: enabled=%s provider=%s", + settings.llm.enabled, + settings.llm.provider, + ) + + # 初始化规则配置 API (MVP-3 / D33),与 DetectionService 共享同一规则引擎 + detection_service = detection.get_detection_service(model_service) + init_rules_api(lambda: detection_service.rule_engine) + app.include_router(rules_router, prefix="/api") + yield # 关闭时清理资源 diff --git a/apps/server/services/event/__init__.py b/apps/server/services/event/__init__.py index c3f15b4..5c6e1d9 100644 --- a/apps/server/services/event/__init__.py +++ b/apps/server/services/event/__init__.py @@ -1,14 +1,26 @@ -"""事件引擎子包 (MVP-1 / P1-P2 / P5)。 +"""事件引擎子包 (MVP-1 / P1-P2 / P5 + MVP-3 / D26-D28)。 模块结构:: decision_engine.py 决策引擎 (置信度评估 + 事件类型映射) rule_engine.py 规则引擎 (YAML 驱动) aggregator.py 事件聚合器 (时间窗口去重) + frame_accumulator.py 多帧累积分析器 (MVP-3) + llm_trigger.py LLM 触发决策器 (MVP-3) """ -from .decision_engine import EventDecisionEngine -from .rule_engine import AlertRuleEngine from .aggregator import EventAggregator +from .decision_engine import EventDecisionEngine +from .frame_accumulator import AccumulationEntry, MultiFrameAccumulator +from .llm_trigger import LLMTrigger, TriggerDecision +from .rule_engine import AlertRuleEngine -__all__ = ["EventDecisionEngine", "AlertRuleEngine", "EventAggregator"] +__all__ = [ + "EventDecisionEngine", + "AlertRuleEngine", + "EventAggregator", + "MultiFrameAccumulator", + "AccumulationEntry", + "LLMTrigger", + "TriggerDecision", +] diff --git a/apps/web/src/api/detection.js b/apps/web/src/api/detection.js index 0c1d592..38af19a 100644 --- a/apps/web/src/api/detection.js +++ b/apps/web/src/api/detection.js @@ -68,3 +68,97 @@ export const detectionApi = { }) } } + +// ============================================================ +// MVP-2 / MVP-3 扩展 API: 摄像头管理 / 规则配置 / LLM 状态 +// ============================================================ + +/** + * 摄像头 (RTSP 流) 管理 API (MVP-2 / D11-D14) + */ +export const cameraApi = { + list() { + return api.get('/rtsp/streams') + }, + get(streamId) { + return api.get(`/rtsp/streams/${encodeURIComponent(streamId)}`) + }, + add(payload) { + return api.post('/rtsp/streams', payload) + }, + remove(streamId) { + return api.delete(`/rtsp/streams/${encodeURIComponent(streamId)}`) + }, + start(streamId) { + return api.post(`/rtsp/streams/${encodeURIComponent(streamId)}/start`) + }, + stop(streamId) { + return api.post(`/rtsp/streams/${encodeURIComponent(streamId)}/stop`) + }, + updateConfig(streamId, payload) { + return api.put(`/rtsp/streams/${encodeURIComponent(streamId)}/config`, payload) + }, + startAll() { + return api.post('/rtsp/start-all') + }, + stopAll() { + return api.post('/rtsp/stop-all') + }, + health() { + return api.get('/rtsp/health') + } +} + +/** + * 规则配置 API (MVP-3 / D33) + */ +export const ruleApi = { + list() { + return api.get('/rules') + }, + get(name) { + return api.get(`/rules/${encodeURIComponent(name)}`) + }, + create(payload) { + return api.post('/rules', payload) + }, + update(name, payload) { + return api.put(`/rules/${encodeURIComponent(name)}`, payload) + }, + remove(name) { + return api.delete(`/rules/${encodeURIComponent(name)}`) + }, + reload() { + return api.post('/rules/reload') + }, + stats() { + return api.get('/rules/_stats') + } +} + +/** + * LLM 状态 / 成本 / 控制 API (MVP-3 / D29-D34) + */ +export const llmApi = { + status() { + return api.get('/llm/status') + }, + cost() { + return api.get('/llm/cost') + }, + costHistory(days = 7) { + return api.get('/llm/cost/history', { params: { days } }) + }, + costRecords(limit = 20) { + return api.get('/llm/cost/records', { params: { limit } }) + }, + enable() { + return api.post('/llm/enable') + }, + disable() { + return api.post('/llm/disable') + }, + reset() { + return api.post('/llm/reset') + } +}