From 5233296dd34544b954ee9eb713162257a60bf075 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Fri, 21 Aug 2026 13:48:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(fam-edge):=20=E4=BA=8B=E4=BB=B6=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=88=B3=E6=94=B9=E7=9B=B8=E5=AF=B9=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=AE=9A=E4=BD=8D=20-=20prompt=20=E8=A6=81=E6=B1=82=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E8=A7=86=E9=A2=91=E5=86=85=E7=9B=B8=E5=AF=B9=E6=97=B6?= =?UTF-8?q?=E9=97=B4=20HH:MM:SS=EF=BC=88=E6=A8=A1=E5=9E=8B=E5=AF=B9?= =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E4=BD=8D=E7=BD=AE=E5=88=A4=E6=96=AD=E6=9B=B4?= =?UTF-8?q?=E5=87=86=EF=BC=89=EF=BC=8C=E5=90=8E=E7=AB=AF=E6=8C=89=20?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E6=97=B6=E9=97=B4+=E5=81=8F=E7=A7=BB=20?= =?UTF-8?q?=E7=B2=BE=E7=A1=AE=E8=AE=A1=E7=AE=97=E7=BB=9D=E5=AF=B9=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E8=90=BD=E5=BA=93=EF=BC=9B=E4=BA=8B=E4=BB=B6=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E7=9B=B4=E6=8E=A5=E7=94=A8=E5=81=8F=E7=A7=BB=E8=B7=B3?= =?UTF-8?q?=E5=B8=A7=EF=BC=8C=E6=B6=88=E9=99=A4=E6=A8=A1=E5=9E=8B=E7=BB=9D?= =?UTF-8?q?=E5=AF=B9=E6=97=B6=E9=97=B4=E6=8E=A8=E7=AE=97=E8=AF=AF=E5=B7=AE?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E5=9B=BE=E6=96=87=E4=B8=8D=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fam_edge/model_adapters/gemini_adapter.py | 6 +- .../fam_edge/model_adapters/nvidia_adapter.py | 6 +- fam-edge/src/fam_edge/video_processor.py | 79 +++++++++++-------- 3 files changed, 54 insertions(+), 37 deletions(-) diff --git a/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py b/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py index 59d61f4..c04e6f3 100644 --- a/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py +++ b/fam-edge/src/fam_edge/model_adapters/gemini_adapter.py @@ -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)。 diff --git a/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py b/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py index b8f181d..c6b42b6 100644 --- a/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py +++ b/fam-edge/src/fam_edge/model_adapters/nvidia_adapter.py @@ -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)。""" diff --git a/fam-edge/src/fam_edge/video_processor.py b/fam-edge/src/fam_edge/video_processor.py index 9cc5c65..c326ef5 100644 --- a/fam-edge/src/fam_edge/video_processor.py +++ b/fam-edge/src/fam_edge/video_processor.py @@ -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: