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