From c09536bf76efdd3442ffcc52838f1e811dbc1bdb Mon Sep 17 00:00:00 2001 From: Furina-1013-create <189647097+Furina-1013-create@users.noreply.github.com> Date: Sun, 17 Aug 2025 14:00:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0videoid=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E4=B8=AA=E8=A7=86=E9=A2=91=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E7=9A=84=E9=97=AE=E9=A2=98=20ps=EF=BC=9A?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E6=95=B0=E6=8D=AE=E5=BA=93=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E7=9A=84=E5=8E=BB=E9=87=8D=EF=BC=9A=20=E8=A7=86=E9=A2=91hash?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=EF=BC=9A=E4=BD=BF=E7=94=A8SHA256=E7=AE=97?= =?UTF-8?q?=E6=B3=95=E8=AE=A1=E7=AE=97=E8=A7=86=E9=A2=91=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E5=94=AF=E4=B8=80=E6=A0=87=E8=AF=86=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E5=8E=BB=E9=87=8D=E6=A3=80=E6=9F=A5=EF=BC=9A?= =?UTF-8?q?=E5=9C=A8=E5=88=86=E6=9E=90=E5=89=8D=E5=85=88=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E4=B8=AD=E6=98=AF=E5=90=A6=E5=B7=B2?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9B=B8=E5=90=8Chash=E7=9A=84=E8=A7=86?= =?UTF-8?q?=E9=A2=91=20=E7=BB=93=E6=9E=9C=E7=BC=93=E5=AD=98=EF=BC=9A?= =?UTF-8?q?=E5=B0=86=E5=88=86=E6=9E=90=E7=BB=93=E6=9E=9C=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E5=88=B0Videos=E8=A1=A8=E4=B8=AD=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E5=88=86=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/database/sqlalchemy_models.py | 27 ++++++++++ src/multimodal/video_analyzer.py | 65 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+) 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: # 清理临时文件 From 4f1e59abf7cc53d106a7e461395780094a153be2 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sun, 17 Aug 2025 14:16:49 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(core):=20=E5=9C=A8=20`no=5Freply`=20?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=8A=A8=E4=BD=9C=E4=B8=AD=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E5=AF=B9=E8=81=8A=E5=A4=A9=E7=B1=BB=E5=9E=8B=E7=9A=84?= =?UTF-8?q?=E5=88=A4=E6=96=AD=EF=BC=8C=E4=BD=BF=E5=85=B6=E4=BB=85=E5=9C=A8?= =?UTF-8?q?=E7=BE=A4=E8=81=8A=E4=B8=AD=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 2 +- src/plugins/built_in/core_actions/no_reply.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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/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 # 动作基本信息