fix(fam-edge): 事件时间戳改相对时间定位 - prompt 要求输出视频内相对时间 HH:MM:SS(模型对相对位置判断更准),后端按 开始时间+偏移 精确计算绝对时间落库;事件截图直接用偏移跳帧,消除模型绝对时间推算误差导致的图文不符
This commit is contained in:
@@ -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)。
|
||||
|
||||
@@ -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)。"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user