fix(fam-edge): 事件时间戳改相对时间定位 - prompt 要求输出视频内相对时间 HH:MM:SS(模型对相对位置判断更准),后端按 开始时间+偏移 精确计算绝对时间落库;事件截图直接用偏移跳帧,消除模型绝对时间推算误差导致的图文不符

This commit is contained in:
ericwyuan
2026-08-21 13:48:46 +08:00
parent 05f727a9ef
commit 5233296dd3
3 changed files with 54 additions and 37 deletions

View File

@@ -319,7 +319,7 @@ class GeminiAdapter(BaseModelAdapter):
def _build_video_prompt(self, known_members: str, event_start_time: str) -> str:
start_hint = ""
if event_start_time:
start_hint = f"\n视频开始时间(北京时间)约为:{event_start_time}请据此推算每个事件的绝对时间戳。"
start_hint = f"\n视频开始时间(北京时间)约为:{event_start_time}"
return f"""你是家庭监控视频分析助手。下面是一段完整监控录像(已整段上传)。
请观看整段视频,提取其中有用的信息,只输出合法 JSON不要 markdown、不要任何解释文字结构如下
@@ -327,7 +327,7 @@ class GeminiAdapter(BaseModelAdapter):
"global_summary": "整个时段的整体摘要简体中文2-4 句,客观描述人物与主要活动",
"events": [
{{
"timestamp": "事件发生时的绝对北京时间(格式 YYYY-MM-DD HH:MM:SS)",
"timestamp": "事件在视频内的相对时间(格式 HH:MM:SS从视频开头 00:00:00 算起)",
"description": "该时间点的画面/动作信息摘要(谁、在做什么、位置)",
"people": ["出现在该时刻的人物,用已知成员真名或'人物A'/'人物B'"],
"is_attention_event": false
@@ -338,7 +338,7 @@ class GeminiAdapter(BaseModelAdapter):
规则:
1. 只描述客观画面,不要猜测或想象。
2. events 提取视频中"有意义的时间点"(人物出现/动作变化/异常不要逐秒罗列timestamp 用绝对北京时间。
2. events 提取视频中"有意义的时间点"(人物出现/动作变化/异常不要逐秒罗列timestamp 必须是"视频内相对时间"(如 00:05:23 表示视频开始后 5 分 23 秒),不要输出绝对日期时间。
3. 已知家庭成员(按特征匹配,匹配到用 real_name否则用"人物X"
{known_members or '(暂无已知成员)'}
4. is_attention_event是否为跌倒、危险、异常哭闹等需关注事件没有则为 false

View File

@@ -246,7 +246,7 @@ class NvidiaVisionAdapter(BaseModelAdapter):
def _build_video_prompt(self, known_members: str, event_start_time: str) -> str:
start_hint = ""
if event_start_time:
start_hint = f"\n视频开始时间(北京时间)约为:{event_start_time}请据此推算每个事件的绝对时间戳。"
start_hint = f"\n视频开始时间(北京时间)约为:{event_start_time}"
return f"""你是家庭监控视频分析助手。下面是一段完整监控录像(已整段上传)。
请观看整段视频,提取其中有用的信息,只输出合法 JSON不要 markdown、不要解释结构如下
@@ -254,7 +254,7 @@ class NvidiaVisionAdapter(BaseModelAdapter):
"global_summary": "整个时段的整体摘要简体中文2-4 句",
"events": [
{{
"timestamp": "事件发生时的绝对北京时间(YYYY-MM-DD HH:MM:SS)",
"timestamp": "事件在视频内的相对时间点(格式 HH:MM:SS从视频开头 00:00:00 算起)",
"description": "该时刻画面/动作信息摘要",
"people": ["出现在该时刻的人物,用已知成员真名或'人物A'"],
"is_attention_event": false
@@ -265,7 +265,7 @@ class NvidiaVisionAdapter(BaseModelAdapter):
规则:
1. 只描述客观画面,不猜测。
2. events 提取有意义的时间点(人物出现/动作变化/异常timestamp 用绝对北京时间。
2. events 提取有意义的时间点(人物出现/动作变化/异常timestamp 必须是"视频内相对时间"(如 00:05:23 表示视频开始后 5 分 23 秒),不要输出绝对日期时间。
3. 已知家庭成员(按特征匹配,匹配到用 real_name否则用"人物X"
{known_members or '(暂无已知成员)'}
4. is_attention_event跌倒、危险、异常哭闹等需关注事件没有则为 false"""

View File

@@ -40,6 +40,29 @@ def _parse_event_start_from_filename(filename: str) -> str:
return ''
def _parse_event_ts(ts: str, start_dt):
"""解析事件时间戳 -> (绝对时间显示串, 视频内偏移秒)。
优先识别"视频内相对时间" HH:MM:SS新 prompt 要求,定位最准);
兼容旧数据的绝对时间 YYYY-MM-DD HH:MM:SS偏移=绝对-视频开始)。
"""
ts = (ts or '').strip()
m = re.match(r'^(\d{1,2}):(\d{2}):(\d{2})$', ts)
if m:
off = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3))
if start_dt is not None:
abs_ts = (start_dt + timedelta(seconds=off)).strftime('%Y-%m-%d %H:%M:%S')
return abs_ts, float(off)
return ts, float(off)
if start_dt is not None:
try:
ev_dt = datetime.strptime(ts[:19], '%Y-%m-%d %H:%M:%S')
return ts, (ev_dt - start_dt).total_seconds()
except ValueError:
pass
return ts, 0.0
class VideoProcessor:
def __init__(self, db: oracle_db.OracleDB):
self.config = load_config()
@@ -129,22 +152,32 @@ class VideoProcessor:
summary = result.get('global_summary', '')
provider = result.get('compute_provider', 'unknown')
# 归一化 events 时间戳(若模型给的是相对偏移,这里不强制;以模型输出为准
# 视频开始时间(绝对时间由后端精确计算:开始时间 + 相对偏移
start_dt = None
vrow = self.db.get_video_by_id(video_id)
if vrow and vrow['event_start_time']:
try:
start_dt = datetime.strptime(vrow['event_start_time'], '%Y-%m-%d %H:%M:%S')
except ValueError:
pass
norm_events = []
offsets = []
for ev in events:
abs_ts, off = _parse_event_ts(ev.get('timestamp'), start_dt)
norm_events.append({
"timestamp": str(ev.get('timestamp', '')),
"timestamp": abs_ts,
"description": str(ev.get('description', '')),
"people": [str(p) for p in ev.get('people', []) if p],
"is_attention_event": bool(ev.get('is_attention_event', False)),
})
offsets.append(off)
event_ids = self.db.mark_video_processed(video_id, summary, norm_events, people, provider)
# 缩略图 + 每个事件对应时间点的画面截图(供前端展示;失败不影响主流程
row = self.db.get_video_by_id(video_id)
if row and row['local_path']:
self._generate_thumb(video_id, row['local_path'])
self._generate_event_thumbs(video_id, row['local_path'], norm_events, event_ids)
# 缩略图 + 每个事件对应时间点的画面截图(用相对偏移直接定位,避免模型绝对时间误差
if vrow and vrow['local_path']:
self._generate_thumb(video_id, vrow['local_path'])
self._generate_event_thumbs(video_id, vrow['local_path'], offsets, event_ids)
# 更新 people 表(标签级,待 person_service 合并)
for p in people:
@@ -184,50 +217,34 @@ class VideoProcessor:
return False
def _generate_event_thumbs(self, video_id: int, video_path: str,
events: List[dict], event_ids: List[int]):
"""按事件时间戳定位视频帧,生成事件画面截图 ev_{event_id}.jpg"""
offsets: List[float], event_ids: List[int]):
"""按事件在视频内的偏移秒定位帧,生成事件画面截图 ev_{event_id}.jpg"""
try:
import cv2
from datetime import datetime as _dt
except Exception as e:
logger.warning(f"事件截图依赖缺失 video_id={video_id}: {e}")
return
try:
start_str = self.db.get_video_by_id(video_id)['event_start_time'] or ''
start_dt = None
if start_str:
try:
start_dt = _dt.strptime(start_str, '%Y-%m-%d %H:%M:%S')
except ValueError:
pass
thumbs = self._thumbs_dir()
cap = cv2.VideoCapture(video_path)
try:
for ev, eid in zip(events, event_ids):
offset = 0.0
ts = str(ev.get('timestamp', ''))
if start_dt and ts:
try:
ev_dt = _dt.strptime(ts[:19], '%Y-%m-%d %H:%M:%S')
offset = (ev_dt - start_dt).total_seconds()
except ValueError:
pass
if offset < 0:
offset = 0.0
cap.set(cv2.CAP_PROP_POS_MSEC, int(offset * 1000))
for off, eid in zip(offsets, event_ids):
if off < 0:
off = 0.0
cap.set(cv2.CAP_PROP_POS_MSEC, int(off * 1000))
ok, frame = cap.read()
if not ok or frame is None:
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
ok, frame = cap.read()
if not ok or frame is None:
logger.warning(f"事件截图失败 ev_{eid}: 无法读取 offset={offset:.0f}s")
logger.warning(f"事件截图失败 ev_{eid}: 无法读取 offset={off:.0f}s")
continue
h, w = frame.shape[:2]
if w > 640:
frame = cv2.resize(frame, (640, int(h * 640 / w)))
out = os.path.join(thumbs, f"ev_{eid}.jpg")
cv2.imwrite(out, frame, [cv2.IMWRITE_JPEG_QUALITY, 65])
logger.info(f"事件截图已生成 ev_{eid}.jpg (offset={offset:.0f}s)")
logger.info(f"事件截图已生成 ev_{eid}.jpg (offset={off:.0f}s)")
finally:
cap.release()
except Exception as e: