diff --git a/TODO.md b/TODO.md index 714cd7f51..d90d810d4 100644 --- a/TODO.md +++ b/TODO.md @@ -24,7 +24,7 @@ - [ ] 对聊天信息的视频增加一个videoid(就像imageid一样) - [ ] 修复generate_responce_for_image方法有的时候会对同一张图片生成两次描述的问题 - [ ] 主动思考的通用提示词改进 -- [ ] 添加贴表情聊天流判断,过滤好友 +- [x] 添加贴表情聊天流判断,过滤好友 - 大工程 diff --git a/src/common/database/sqlalchemy_models.py b/src/common/database/sqlalchemy_models.py index 5bd342a88..2e1665dd2 100644 --- a/src/common/database/sqlalchemy_models.py +++ b/src/common/database/sqlalchemy_models.py @@ -217,6 +217,33 @@ class ImageDescriptions(Base): ) +class Videos(Base): + """视频信息模型""" + __tablename__ = 'videos' + + id = Column(Integer, primary_key=True, autoincrement=True) + video_id = Column(Text, nullable=False, default="") + video_hash = Column(get_string_field(64), nullable=False, index=True, unique=True) + description = Column(Text, nullable=True) + path = Column(get_string_field(500), nullable=False, unique=True) + count = Column(Integer, nullable=False, default=1) + timestamp = Column(Float, nullable=False) + vlm_processed = Column(Boolean, nullable=False, default=False) + + # 视频特有属性 + duration = Column(Float, nullable=True) # 视频时长(秒) + frame_count = Column(Integer, nullable=True) # 总帧数 + fps = Column(Float, nullable=True) # 帧率 + resolution = Column(Text, nullable=True) # 分辨率 + file_size = Column(Integer, nullable=True) # 文件大小(字节) + + __table_args__ = ( + Index('idx_videos_video_hash', 'video_hash'), + Index('idx_videos_path', 'path'), + Index('idx_videos_timestamp', 'timestamp'), + ) + + class OnlineTime(Base): """在线时长记录模型""" __tablename__ = 'online_time' diff --git a/src/multimodal/video_analyzer.py b/src/multimodal/video_analyzer.py index 45c7e65bd..04d0d8c75 100644 --- a/src/multimodal/video_analyzer.py +++ b/src/multimodal/video_analyzer.py @@ -10,6 +10,8 @@ import cv2 import tempfile import asyncio import base64 +import hashlib +import time from PIL import Image from pathlib import Path from typing import List, Tuple, Optional, Dict @@ -18,6 +20,7 @@ import io from src.llm_models.utils_model import LLMRequest from src.config.config import global_config, model_config from src.common.logger import get_logger +from src.common.database.sqlalchemy_models import get_db_session, Videos logger = get_logger("src.multimodal.video_analyzer") @@ -98,6 +101,44 @@ class VideoAnalyzer: logger.info(f"✅ 视频分析器初始化完成,分析模式: {self.analysis_mode}") + def _calculate_video_hash(self, video_data: bytes) -> str: + """计算视频文件的hash值""" + hash_obj = hashlib.sha256() + hash_obj.update(video_data) + return hash_obj.hexdigest() + + def _check_video_exists(self, video_hash: str) -> Optional[Videos]: + """检查视频是否已经分析过""" + try: + with get_db_session() as session: + return session.query(Videos).filter(Videos.video_hash == video_hash).first() + except Exception as e: + self.logger.warning(f"检查视频是否存在时出错: {e}") + return None + + def _store_video_result(self, video_hash: str, description: str, path: str = "", metadata: Optional[Dict] = None) -> Optional[Videos]: + """存储视频分析结果到数据库""" + try: + with get_db_session() as session: + # 如果path为空,使用hash作为路径 + if not path: + path = f"video_{video_hash[:16]}.unknown" + + video_record = Videos( + video_hash=video_hash, + description=description, + path=path, + timestamp=time.time() + ) + session.add(video_record) + session.commit() + session.refresh(video_record) + self.logger.info(f"✅ 视频分析结果已保存到数据库,hash: {video_hash[:16]}...") + return video_record + except Exception as e: + self.logger.error(f"存储视频分析结果时出错: {e}") + return None + def set_analysis_mode(self, mode: str): """设置分析模式""" if mode in ["batch", "sequential", "auto"]: @@ -309,6 +350,16 @@ class VideoAnalyzer: if not video_bytes: return {"summary": "❌ 视频数据为空"} + # 计算视频hash值 + video_hash = self._calculate_video_hash(video_bytes) + logger.info(f"视频hash: {video_hash[:16]}...") + + # 检查数据库中是否已存在该视频的分析结果 + existing_video = self._check_video_exists(video_hash) + if existing_video: + logger.info(f"✅ 找到已存在的视频分析结果,直接返回 (id: {existing_video.id})") + return {"summary": existing_video.description} + # 创建临时文件保存视频数据 with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file: temp_file.write(video_bytes) @@ -321,6 +372,20 @@ class VideoAnalyzer: # 使用临时文件进行分析 result = await self.analyze_video(temp_path, question) + + # 保存分析结果到数据库 + metadata = { + "filename": filename, + "file_size": len(video_bytes), + "analysis_timestamp": time.time() + } + self._store_video_result( + video_hash=video_hash, + description=result, + path=filename or "", + metadata=metadata + ) + return {"summary": result} finally: # 清理临时文件 diff --git a/src/plugins/built_in/core_actions/no_reply.py b/src/plugins/built_in/core_actions/no_reply.py index b9e634253..4aedcf8a9 100644 --- a/src/plugins/built_in/core_actions/no_reply.py +++ b/src/plugins/built_in/core_actions/no_reply.py @@ -3,6 +3,7 @@ from collections import deque # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode +from src.plugin_system.base.component_types import ChatType # 导入依赖的系统组件 from src.common.logger import get_logger @@ -17,6 +18,7 @@ class NoReplyAction(BaseAction): focus_activation_type = ActionActivationType.NEVER normal_activation_type = ActionActivationType.NEVER mode_enable = ChatMode.FOCUS + chat_type_allow = ChatType.GROUP parallel_action = False # 动作基本信息