[3.1-3.5] FAM-Edge 全链路 - API-Gateway/Video-Preprocessor/AI-Orchestrator/模型适配器(基类+Ollama+Gemini)/熔断器/JSON解析容错 + 配置

This commit is contained in:
ericwyuan
2026-08-19 22:25:38 +08:00
parent da6b1c8d39
commit cdd1f21d4c
20 changed files with 1384 additions and 0 deletions

View File

@@ -0,0 +1,156 @@
"""
GeminiAdapter - Google Gemini 云端模型适配器
provider_name = "gemini"
模型: gemini-1.5-flash
健康检查: GET models API
熔断器: 启用,连续 5 次失败 -> OPEN 15 分钟
"""
import os
import base64
import requests
from typing import List, Optional
from .base_adapter import BaseModelAdapter
from .circuit_breaker import CircuitBreaker
from ..logger import setup_logger
logger = setup_logger('fam-edge.gemini_adapter')
class GeminiAdapter(BaseModelAdapter):
"""Gemini 云端 VLM 适配器"""
def __init__(self, config: dict):
super().__init__("gemini", config)
self.model_name = config.get('model_name', 'gemini-1.5-flash')
self.api_key = config.get('api_key', '')
self.timeout = config.get('timeout', 8)
cb_cfg = config.get('circuit_breaker', {})
self._cb = CircuitBreaker(
threshold=cb_cfg.get('threshold', 5),
cooldown=cb_cfg.get('cooldown', 900),
enabled=cb_cfg.get('enabled', True) # 云端默认启用
)
self._base_url = "https://generativelanguage.googleapis.com/v1beta"
def health_check(self) -> bool:
"""GET models API检查可用性"""
if not self.api_key:
logger.warning("Gemini API Key 未配置,健康检查失败")
return False
try:
resp = requests.get(
f"{self._base_url}/models?key={self.api_key}",
timeout=10
)
if resp.status_code == 200:
models = resp.json().get('models', [])
model_names = [m.get('name', '') for m in models]
has_model = any(self.model_name in name for name in model_names)
if has_model:
logger.info(f"Gemini 健康检查通过: 模型 {self.model_name} 可用")
return True
else:
logger.warning(f"Gemini 健康检查失败: 模型 {self.model_name} 未找到")
return False
return False
except Exception as e:
logger.error(f"Gemini 健康检查异常: {e}")
return False
def analyze_frames(self, frame_paths: List[str],
frame_timestamps: List[str],
known_members_context: str) -> Optional[str]:
"""调用 Gemini 视觉分析"""
if self._cb.is_open():
logger.warning("Gemini 熔断器 OPEN跳过调用")
return None
if not self.api_key:
logger.warning("Gemini API Key 未配置,跳过调用")
return None
# 构建 Prompt
n = len(frame_paths)
prompt = self._build_visual_prompt(n, frame_timestamps, known_members_context)
# 构建 inline_data
parts = [{"text": prompt}]
for path in frame_paths:
try:
with open(path, 'rb') as f:
img_data = base64.b64encode(f.read()).decode('utf-8')
parts.append({
"inline_data": {
"mime_type": "image/jpeg",
"data": img_data
}
})
except Exception as e:
logger.error(f"读取图片失败 {path}: {e}")
try:
resp = requests.post(
f"{self._base_url}/models/{self.model_name}:generateContent?key={self.api_key}",
json={
"contents": [{"parts": parts}],
"generationConfig": {"temperature": 0.2, "topP": 0.8}
},
timeout=self.timeout
)
if resp.status_code == 200:
data = resp.json()
candidates = data.get('candidates', [])
if candidates:
output = candidates[0].get('content', {}).get('parts', [{}])[0].get('text', '')
self._cb.record_success()
logger.info(f"Gemini 视觉分析完成,输出长度={len(output)}")
return output
else:
logger.warning("Gemini 返回空 candidates")
self._cb.record_failure()
return None
else:
logger.error(f"Gemini 调用失败: {resp.status_code} {resp.text[:200]}")
self._cb.record_failure()
return None
except requests.Timeout:
logger.warning(f"Gemini 调用超时 ({self.timeout}s),降级跳过")
self._cb.record_failure()
return None
except Exception as e:
logger.error(f"Gemini 调用异常: {e}")
self._cb.record_failure()
return None
def get_timeout(self) -> int:
return self.timeout
def get_circuit_breaker(self) -> CircuitBreaker:
return self._cb
def _build_visual_prompt(self, n: int, timestamps: List[str], known_members: str) -> str:
ts_lines = '\n'.join(
f"[Image {i+1}] Time: {ts}" for i, ts in enumerate(timestamps)
)
return f"""You are a home surveillance video analysis assistant. Describe what you see in the following {n} images chronologically. Be objective.
Timestamps:
{ts_lines}
For each image, report:
1. People: count, clothing (color + type), visible actions
2. Objects: toys, bottles, furniture, etc.
3. Interactions: between people or people and objects
Known family members (match by features, use real_name if matched, otherwise "PersonX"):
{known_members or 'None'}
Output format (plain text, one paragraph per image, keep timestamp markers):
[Image 1] Time: {timestamps[0] if timestamps else ''}
Description: ...
Be concise and objective. Do not output JSON or markdown."""