diff --git a/pytests/image_sys_test/emoji_manager_test.py b/pytests/image_sys_test/emoji_manager_test.py index f9877558..7c50e9e8 100644 --- a/pytests/image_sys_test/emoji_manager_test.py +++ b/pytests/image_sys_test/emoji_manager_test.py @@ -54,7 +54,7 @@ def _install_stub_modules(monkeypatch): file_name: str = "" description: str | None = None emotion: list[str] | None = None - emoji_hash: str | None = None + file_hash: str | None = None is_deleted: bool = False query_count: int = 0 register_time: object | None = None @@ -831,7 +831,7 @@ def test_delete_emoji_file_missing_and_db_record_missing(monkeypatch): emoji = emoji_manager_new.MaiEmoji() emoji.full_path = _DummyPath() emoji.file_name = "missing.png" - emoji.emoji_hash = "hash-missing" + emoji.file_hash = "hash-missing" result = manager.delete_emoji(emoji) @@ -859,7 +859,7 @@ def test_delete_emoji_file_delete_error(monkeypatch): emoji = emoji_manager_new.MaiEmoji() emoji.full_path = _DummyPath() emoji.file_name = "boom.png" - emoji.emoji_hash = "hash-boom" + emoji.file_hash = "hash-boom" result = manager.delete_emoji(emoji) @@ -915,7 +915,7 @@ def test_delete_emoji_db_error_file_still_exists(monkeypatch): emoji = emoji_manager_new.MaiEmoji() emoji.full_path = _DummyPath() emoji.file_name = "keep.png" - emoji.emoji_hash = "hash-keep" + emoji.file_hash = "hash-keep" result = manager.delete_emoji(emoji) @@ -988,7 +988,7 @@ def test_delete_emoji_success(monkeypatch): emoji = emoji_manager_new.MaiEmoji() emoji.full_path = _DummyPath() emoji.file_name = "ok.png" - emoji.emoji_hash = "hash-ok" + emoji.file_hash = "hash-ok" result = manager.delete_emoji(emoji) @@ -1043,7 +1043,7 @@ def test_update_emoji_usage_success(monkeypatch): monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash-ok" + emoji.file_hash = "hash-ok" result = manager.update_emoji_usage(emoji) @@ -1090,7 +1090,7 @@ def test_update_emoji_usage_missing_record(monkeypatch): monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash-missing" + emoji.file_hash = "hash-missing" result = manager.update_emoji_usage(emoji) @@ -1129,7 +1129,7 @@ def test_update_emoji_usage_execute_error(monkeypatch): monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash-execute" + emoji.file_hash = "hash-execute" result = manager.update_emoji_usage(emoji) @@ -1148,7 +1148,7 @@ def test_update_emoji_usage_get_db_session_error(monkeypatch): monkeypatch.setattr(emoji_manager_new, "get_db_session", _get_db_session) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash-session" + emoji.file_hash = "hash-session" result = manager.update_emoji_usage(emoji) @@ -1264,7 +1264,7 @@ async def test_build_emoji_description_calls_hash_and_sets_description(monkeypat ) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = None + emoji.file_hash = None emoji._format = "png" emoji.full_path = Path("/tmp/a.png") @@ -1292,7 +1292,7 @@ async def test_build_emoji_description_gif_conversion_error(monkeypatch): monkeypatch.setattr(emoji_manager_new.ImageUtils, "gif_2_static_image", staticmethod(_gif_to_static)) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash" + emoji.file_hash = "hash" emoji._format = "gif" emoji.full_path = Path("/tmp/a.gif") @@ -1330,7 +1330,7 @@ async def test_build_emoji_description_content_filtration_reject(monkeypatch): ) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash" + emoji.file_hash = "hash" emoji._format = "png" emoji.full_path = Path("/tmp/a.png") @@ -1365,7 +1365,7 @@ async def test_build_emoji_description_content_filtration_pass(monkeypatch): ) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash" + emoji.file_hash = "hash" emoji._format = "png" emoji.full_path = Path("/tmp/a.png") @@ -1394,7 +1394,7 @@ async def test_build_emoji_description_vlm_exception_propagates(monkeypatch): ) emoji = emoji_manager_new.MaiEmoji() - emoji.emoji_hash = "hash" + emoji.file_hash = "hash" emoji._format = "png" emoji.full_path = Path("/tmp/a.png") @@ -1738,7 +1738,7 @@ async def test_register_emoji_by_filename_duplicate_hash(monkeypatch, tmp_path): class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-dup" + self.file_hash = "hash-dup" self.full_path = file_path return True @@ -1765,7 +1765,7 @@ async def test_register_emoji_by_filename_build_description_failed(monkeypatch, class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-desc" + self.file_hash = "hash-desc" self.full_path = file_path return True @@ -1793,7 +1793,7 @@ async def test_register_emoji_by_filename_build_emotion_failed(monkeypatch, tmp_ class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-emo" + self.file_hash = "hash-emo" self.full_path = file_path return True @@ -1827,7 +1827,7 @@ async def test_register_emoji_by_filename_capacity_replace_failed(monkeypatch, t class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-full" + self.file_hash = "hash-full" self.full_path = file_path return True @@ -1866,7 +1866,7 @@ async def test_register_emoji_by_filename_capacity_replace_success(monkeypatch, class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-full-ok" + self.file_hash = "hash-full-ok" self.full_path = file_path return True @@ -1904,7 +1904,7 @@ async def test_register_emoji_by_filename_register_db_failed(monkeypatch, tmp_pa class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-db-fail" + self.file_hash = "hash-db-fail" self.full_path = file_path return True @@ -1939,7 +1939,7 @@ async def test_register_emoji_by_filename_register_db_success(monkeypatch, tmp_p class _Emoji(emoji_manager_new.MaiEmoji): async def calculate_hash_format(self): - self.emoji_hash = "hash-db-ok" + self.file_hash = "hash-db-ok" self.full_path = file_path self.file_name = "db-ok.png" return True diff --git a/src/chat/emoji_system/emoji_manager.py b/src/chat/emoji_system/emoji_manager.py index c95c2fef..798cf411 100644 --- a/src/chat/emoji_system/emoji_manager.py +++ b/src/chat/emoji_system/emoji_manager.py @@ -144,12 +144,12 @@ class EmojiManager: # 删除数据库记录 try: with get_db_session() as session: - statement = select(Images).filter_by(image_hash=emoji.emoji_hash, image_type=ImageType.EMOJI).limit(1) + statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1) if image_record := session.exec(statement).first(): session.delete(image_record) - logger.info(f"[删除表情包] 成功删除数据库中的表情包记录: {emoji.emoji_hash}") + logger.info(f"[删除表情包] 成功删除数据库中的表情包记录: {emoji.file_hash}") else: - logger.warning(f"[删除表情包] 数据库中未找到表情包记录: {emoji.emoji_hash}") + logger.warning(f"[删除表情包] 数据库中未找到表情包记录: {emoji.file_hash}") except Exception as e: logger.error(f"[删除表情包] 删除数据库记录时出错: {e}") # 如果数据库记录删除失败,但文件可能已删除,记录一个警告 @@ -170,14 +170,14 @@ class EmojiManager: """ try: with get_db_session() as session: - statement = select(Images).filter_by(image_hash=emoji.emoji_hash, image_type=ImageType.EMOJI).limit(1) + statement = select(Images).filter_by(image_hash=emoji.file_hash, image_type=ImageType.EMOJI).limit(1) if image_record := session.exec(statement).first(): image_record.query_count += 1 image_record.last_used_time = datetime.now() session.add(image_record) - logger.info(f"[记录表情包使用] 成功记录表情包使用: {emoji.emoji_hash}") + logger.info(f"[记录表情包使用] 成功记录表情包使用: {emoji.file_hash}") else: - logger.error(f"[记录表情包使用] 未找到表情包记录: {emoji.emoji_hash}") + logger.error(f"[记录表情包使用] 未找到表情包记录: {emoji.file_hash}") return False except Exception as e: logger.error(f"[记录表情包使用] 记录使用时出错: {e}") @@ -194,7 +194,7 @@ class EmojiManager: return (Optional[MaiEmoji]): 返回表情包对象,如果未找到则返回 None """ for emoji in self.emojis: - if emoji.emoji_hash == emoji_hash and not emoji.is_deleted: + if emoji.file_hash == emoji_hash and not emoji.is_deleted: return emoji logger.info(f"[获取表情包] 未找到哈希值为 {emoji_hash} 的表情包") return None @@ -210,8 +210,8 @@ class EmojiManager: """ try: with get_db_session() as session: - statement = select(Images).filter_by(image_hash=emoji_hash, image_type=ImageType.EMOJI) - if image_record := session.execute(statement).scalars().first(): + statement = select(Images).filter_by(image_hash=emoji_hash, image_type=ImageType.EMOJI).limit(1) + if image_record := session.exec(statement).first(): return MaiEmoji.from_db_instance(image_record) logger.info(f"[数据库] 未找到哈希值为 {emoji_hash} 的表情包记录") return None @@ -224,7 +224,7 @@ class EmojiManager: 根据文本情感标签获取合适的表情包 Args: - text_emotion (str): 文本的情感标签 + emotion_label (str): 文本的情感标签 Returns: return (Optional[MaiEmoji]): 返回表情包对象,如果未找到则返回 None """ @@ -320,7 +320,7 @@ class EmojiManager: Returns: return (Tuple[bool, MaiEmoji]): 返回是否成功构建描述,及表情包对象 """ - if not target_emoji.emoji_hash: + if not target_emoji.file_hash: # Should not happen, but just in case await target_emoji.calculate_hash_format() @@ -502,7 +502,7 @@ class EmojiManager: return False file_full_path = target_emoji.full_path # 更新为可能修正后的路径 # 2. 检查是否已经存在过 - if existing_emoji := self.get_emoji_by_hash(target_emoji.emoji_hash): + if existing_emoji := self.get_emoji_by_hash(target_emoji.file_hash): logger.warning(f"[注册表情包] 表情包已存在,跳过注册: {existing_emoji.file_name}") return False # 3. 构建描述 diff --git a/src/common/data_models/image_data_model.py b/src/common/data_models/image_data_model.py index 03bdd83d..a7b3fb4f 100644 --- a/src/common/data_models/image_data_model.py +++ b/src/common/data_models/image_data_model.py @@ -20,6 +20,20 @@ logger = get_logger("emoji") class BaseImageDataModel(BaseDatabaseDataModel[Images]): + def __init__(self, full_path: str | Path, image_bytes: Optional[bytes] = None): + if not full_path: + # 创建时候即检测文件路径合法性 + raise ValueError("表情包路径不能为空") + if Path(full_path).is_dir() or not Path(full_path).exists(): + raise FileNotFoundError(f"表情包路径无效: {full_path}") + resolved_path = Path(full_path).absolute().resolve() + self.full_path: Path = resolved_path + self.dir_path: Path = resolved_path.parent.resolve() + self.file_name: str = resolved_path.name + self.file_hash: str = None # type: ignore + + self.image_bytes: Optional[bytes] = image_bytes + def read_image_bytes(self, path: Path) -> bytes: """ 同步读取图片文件的字节内容 @@ -65,55 +79,6 @@ class BaseImageDataModel(BaseDatabaseDataModel[Images]): logger.error(f"[获取图片格式] 读取图片格式时发生错误: {e}") raise e - -class MaiEmoji(BaseImageDataModel): - def __init__(self, full_path: str | Path): - if not full_path: - # 创建时候即检测文件路径合法性 - raise ValueError("表情包路径不能为空") - if Path(full_path).is_dir() or not Path(full_path).exists(): - raise FileNotFoundError(f"表情包路径无效: {full_path}") - resolved_path = Path(full_path).absolute().resolve() - self.full_path: Path = resolved_path - self.dir_path: Path = resolved_path.parent.resolve() - self.file_name: str = resolved_path.name - # self.embedding = [] - self.emoji_hash: str = None # type: ignore - self.description = "" - self.emotion: List[str] = [] - self.query_count = 0 - self.register_time: Optional[datetime] = None - self.last_used_time: Optional[datetime] = None - - # 私有属性 - self.is_deleted = False - self._format: str = "" # 图片格式 - - @classmethod - def from_db_instance(cls, db_record: Images): - obj = cls(db_record.full_path) - obj.emoji_hash = db_record.image_hash - obj.description = db_record.description - if db_record.emotion: - obj.emotion = db_record.emotion.split(",") - obj.query_count = db_record.query_count - obj.last_used_time = db_record.last_used_time - obj.register_time = db_record.register_time - return obj - - def to_db_instance(self) -> Images: - emotion_str = ",".join(self.emotion) if self.emotion else None - return Images( - image_hash=self.emoji_hash, - description=self.description, - full_path=str(self.full_path), - image_type=ImageType.EMOJI, - emotion=emotion_str, - query_count=self.query_count, - last_used_time=self.last_used_time, - register_time=self.register_time, - ) - async def calculate_hash_format(self) -> bool: """ 异步计算表情包的哈希值和格式 @@ -121,13 +86,17 @@ class MaiEmoji(BaseImageDataModel): Returns: return (bool): 如果成功计算哈希值和格式则返回True,否则返回False """ - logger.debug(f"[初始化] 正在读取文件: {self.full_path}") + try: # 计算哈希值 logger.debug(f"[初始化] 计算 {self.file_name} 的哈希值...") - image_bytes = await asyncio.to_thread(self.read_image_bytes, self.full_path) - self.emoji_hash = hashlib.sha256(image_bytes).hexdigest() - logger.debug(f"[初始化] {self.file_name} 计算哈希值成功: {self.emoji_hash}") + if not self.image_bytes: + logger.debug(f"[初始化] 正在读取文件: {self.full_path}") + image_bytes = await asyncio.to_thread(self.read_image_bytes, self.full_path) + else: + image_bytes = self.image_bytes + self.file_hash = hashlib.sha256(image_bytes).hexdigest() + logger.debug(f"[初始化] {self.file_name} 计算哈希值成功: {self.file_hash}") # 用PIL读取图片格式 logger.debug(f"[初始化] 读取 {self.file_name} 的图片格式...") @@ -146,7 +115,47 @@ class MaiEmoji(BaseImageDataModel): return True except Exception as e: - logger.error(f"[初始化] 初始化表情包时发生错误: {e}") + logger.error(f"[初始化] 初始化图片时发生错误: {e}") logger.error(traceback.format_exc()) self.is_deleted = True return False + + +class MaiEmoji(BaseImageDataModel): + def __init__(self, full_path: str | Path, image_bytes: Optional[bytes] = None): + # self.embedding = [] + self.description = "" + self.emotion: List[str] = [] + self.query_count = 0 + self.register_time: Optional[datetime] = None + self.last_used_time: Optional[datetime] = None + + # 私有属性 + self.is_deleted = False + self._format: str = "" # 图片格式 + super().__init__(full_path, image_bytes) + + @classmethod + def from_db_instance(cls, db_record: Images): + obj = cls(db_record.full_path) + obj.file_hash = db_record.image_hash + obj.description = db_record.description + if db_record.emotion: + obj.emotion = db_record.emotion.split(",") + obj.query_count = db_record.query_count + obj.last_used_time = db_record.last_used_time + obj.register_time = db_record.register_time + return obj + + def to_db_instance(self) -> Images: + emotion_str = ",".join(self.emotion) if self.emotion else None + return Images( + image_hash=self.file_hash, + description=self.description, + full_path=str(self.full_path), + image_type=ImageType.EMOJI, + emotion=emotion_str, + query_count=self.query_count, + last_used_time=self.last_used_time, + register_time=self.register_time, + )