呀,柒柒,这次我们对表情包系统进行了一次超级棒的大升级哦!它现在变得更聪明、更懂我们的心意啦!就像我一样,总能找到最完美的表情来点亮对话!♪~

这是我为你准备的提交信息,你看看喜不喜欢~

feat(chat): 使用 LLM 优化表情包选择与分析

本次提交对表情包系统进行了核心重构,从原有的基于关键词相似度匹配的简单算法,升级为由大型语言模型(LLM)驱动的智能决策流程。这使得表情包的选择和分析更加精准、智能和人性化。

主要变更包括:

1.  **引入 LLM 进行表情包选择**
    -   重写了 `get_emoji_for_text` 方法,废弃了原有的编辑距离算法。
    -   新流程会根据配置随机抽取一部分表情包作为候选,并构建一个精细的 Prompt,引导 LLM 根据输入的“情感描述”选择最匹配的表情包。这让选择不再局限于字面匹配,而是能理解更深层次的语境和情绪。

2.  **优化表情包描述与分析流程**
    -   大幅改进了 `build_emoji_description` 中的 VLM 和 LLM 提示词,使其能生成更懂网络文化、更详细的表情包描述,并提炼出更精准的情感关键词。
    -   为动态图(GIF)和静态图设计了不同的分析策略,以获得更高质量的描述结果。

3.  **增强 Planner 动作连贯性**
    -   更新了 `planner_prompts`,明确要求当 `reply` 和 `emoji` 动作同时触发时,`emoji` 的选择必须基于 `reply` 动作生成的最终文本内容。这确保了文字和表情包的表达高度一致。

4.  **逻辑与配置微调**
    -   在 `utils_image` 中,现在只有当“偷表情包”功能开启时,才会保存接收到的表情包,避免了不必要的文件存储。
    -   将表情包检查间隔 `check_interval` 的类型从 `int` 改为 `float`,允许更灵活的配置。
This commit is contained in:
tt-P607
2025-09-14 16:44:35 +08:00
committed by Windpicker-owo
parent 00c00f1068
commit 4d30040f59
4 changed files with 182 additions and 177 deletions

View File

@@ -439,105 +439,103 @@ class EmojiManager:
logger.error(f"记录表情使用失败: {str(e)}") logger.error(f"记录表情使用失败: {str(e)}")
async def get_emoji_for_text(self, text_emotion: str) -> Optional[Tuple[str, str, str]]: async def get_emoji_for_text(self, text_emotion: str) -> Optional[Tuple[str, str, str]]:
"""根据文本内容获取相关表情包 """
根据文本内容使用LLM选择一个合适的表情包。
Args: Args:
text_emotion: 输入的情感描述文本 text_emotion (str): LLM希望表达的情感或意图的文本描述。
Returns: Returns:
Optional[Tuple[str, str]]: (表情包完整文件路径, 表情包描述)如果没有找到则返回None Optional[Tuple[str, str, str]]: 返回一个元组,包含所选表情包的 (文件路径, 描述, 匹配的情感描述)
如果未找到合适的表情包,则返回 None。
""" """
try: try:
_time_start = time.time() _time_start = time.time()
# 获取所有表情包 (从内存缓存中获取) # 1. 从内存中获取所有可用的表情包对象
all_emojis = self.emoji_objects all_emojis = [emoji for emoji in self.emoji_objects if not emoji.is_deleted and emoji.description]
if not all_emojis: if not all_emojis:
logger.warning("内存中没有任何表情包对象") logger.warning("内存中没有任何可用的表情包对象")
return None return None
# 计算每个表情包与输入文本的最大情感相似度 # 2. 根据全局配置决定候选表情包的数量
emoji_similarities = [] max_candidates = global_config.emoji.max_emoji_for_llm_select
for emoji in all_emojis:
# 跳过已标记为删除的对象
if emoji.is_deleted:
continue
emotions = emoji.emotion # 如果配置为0或者大于等于总数则选择所有表情包
if not emotions: if max_candidates <= 0 or max_candidates >= len(all_emojis):
continue candidate_emojis = all_emojis
else:
# 否则,从所有表情包中随机抽取指定数量
candidate_emojis = random.sample(all_emojis, max_candidates)
# 计算与每个emotion标签的相似度取最大值 # 确保候选列表不为空
max_similarity = 0 if not candidate_emojis:
best_matching_emotion = "" logger.warning("未能选出任何候选表情包")
for emotion in emotions:
# 使用编辑距离计算相似度
distance = self._levenshtein_distance(text_emotion, emotion)
max_len = max(len(text_emotion), len(emotion))
similarity = 1 - (distance / max_len if max_len > 0 else 0)
if similarity > max_similarity:
max_similarity = similarity
best_matching_emotion = emotion
if best_matching_emotion:
emoji_similarities.append((emoji, max_similarity, best_matching_emotion))
# 按相似度降序排序
emoji_similarities.sort(key=lambda x: x[1], reverse=True)
# 获取前10个最相似的表情包
top_emojis = emoji_similarities[:10] if len(emoji_similarities) > 10 else emoji_similarities
if not top_emojis:
logger.warning("未找到匹配的表情包")
return None return None
# 从前几个中随机选择一个 # 3. 构建用于LLM决策的prompt
selected_emoji, similarity, matched_emotion = random.choice(top_emojis) emoji_options_str = ""
for i, emoji in enumerate(candidate_emojis):
# 为每个表情包创建一个编号和它的详细描述
emoji_options_str += f"编号: {i+1}\n描述: {emoji.description}\n\n"
# 更新使用次数 # 精心设计的prompt引导LLM做出选择
prompt = f"""
你是一个聊天机器人,你需要根据你想要表达的情感,从一个表情包列表中选择最合适的一个。
# 你的任务
根据下面提供的“你想表达的描述”,在“表情包选项”中选择一个最符合该描述的表情包。
# 你想表达的描述
{text_emotion}
# 表情包选项
{emoji_options_str}
# 规则
1. 仔细阅读“你想表达的描述”和每一个“表情包选项”的详细描述。
2. 选择一个编号,该编号对应的表情包必须最贴切地反映出你想表达的情感、内容或网络文化梗。
3. 你的回答必须且只能是一个格式为 "选择编号X" 的字符串其中X是你选择的表情包编号。
4. 不要输出任何其他解释或无关内容。
现在,请做出你的选择:
"""
# 4. 调用LLM进行决策
decision, _ = await self.llm_emotion_judge.generate_response_async(prompt, temperature=0.5, max_tokens=20)
logger.info(f"LLM选择的描述: {text_emotion}")
logger.info(f"LLM决策结果: {decision}")
# 5. 解析LLM的决策结果
match = re.search(r"(\d+)", decision)
if not match:
logger.error(f"无法从LLM的决策中解析出编号: {decision}")
return None
selected_index = int(match.group(1)) - 1
# 6. 验证选择的编号是否有效
if not (0 <= selected_index < len(candidate_emojis)):
logger.error(f"LLM返回了无效的表情包编号: {selected_index + 1}")
return None
# 7. 获取选中的表情包并更新使用记录
selected_emoji = candidate_emojis[selected_index]
self.record_usage(selected_emoji.hash) self.record_usage(selected_emoji.hash)
_time_end = time.time() _time_end = time.time()
logger.info( logger.info(
f"为[{text_emotion}]找到表情包: {matched_emotion} ({selected_emoji.filename}), Similarity: {similarity:.4f}" f"找到匹配描述的表情包: {selected_emoji.description}, 耗时: {(_time_end - _time_start):.2f}s"
) )
# 返回完整文件路径和描述
return selected_emoji.full_path, f"[ {selected_emoji.description} ]", matched_emotion # 8. 返回选中的表情包信息
return selected_emoji.full_path, f"[表情包:{selected_emoji.description}]", text_emotion
except Exception as e: except Exception as e:
logger.error(f"[错误] 获取表情包失败: {str(e)}") logger.error(f"使用LLM获取表情包时发生错误: {str(e)}")
logger.error(traceback.format_exc())
return None return None
def _levenshtein_distance(self, s1: str, s2: str) -> int:
# sourcery skip: simplify-empty-collection-comparison, simplify-len-comparison, simplify-str-len-comparison
"""计算两个字符串的编辑距离
Args:
s1: 第一个字符串
s2: 第二个字符串
Returns:
int: 编辑距离
"""
if len(s1) < len(s2):
return self._levenshtein_distance(s2, s1)
if len(s2) == 0:
return len(s1)
previous_row = range(len(s2) + 1)
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]
async def check_emoji_file_integrity(self) -> None: async def check_emoji_file_integrity(self) -> None:
"""检查表情包文件完整性 """检查表情包文件完整性
遍历self.emoji_objects中的所有对象检查文件是否存在 遍历self.emoji_objects中的所有对象检查文件是否存在
@@ -627,11 +625,10 @@ class EmojiManager:
await asyncio.sleep(global_config.emoji.check_interval * 60) await asyncio.sleep(global_config.emoji.check_interval * 60)
continue continue
# 检查是否需要处理表情包(数量超过最大值或不足) # 无论steal_emoji是否开启都检查emoji文件夹以支持手动注册
if global_config.emoji.steal_emoji and ( # 只有在需要腾出空间或填充表情库时,才真正执行注册
(self.emoji_num > self.emoji_num_max and global_config.emoji.do_replace) if (self.emoji_num > self.emoji_num_max and global_config.emoji.do_replace) or \
or (self.emoji_num < self.emoji_num_max) (self.emoji_num < self.emoji_num_max):
):
try: try:
# 获取目录下所有图片文件 # 获取目录下所有图片文件
files_to_process = [ files_to_process = [
@@ -646,7 +643,7 @@ class EmojiManager:
# 尝试注册表情包 # 尝试注册表情包
success = await self.register_emoji_by_filename(filename) success = await self.register_emoji_by_filename(filename)
if success: if success:
# 注册成功则跳出循环 # 注册成功则跳出循环,等待下一个检查周期
break break
# 注册失败则删除对应文件 # 注册失败则删除对应文件
@@ -914,110 +911,114 @@ class EmojiManager:
return False return False
async def build_emoji_description(self, image_base64: str) -> Tuple[str, List[str]]: async def build_emoji_description(self, image_base64: str) -> Tuple[str, List[str]]:
"""获取表情包描述和情感列表,优化复用已有描述 """
获取表情包的详细描述和情感关键词列表。
该函数首先使用VLM视觉语言模型对图片进行深入分析生成一份包含文化、Meme内涵的详细描述。
然后它会调用另一个LLM基于这份详细描述提炼出几个核心的、简洁的情感关键词。
最终返回详细描述和关键词列表,为后续的表情包选择提供丰富且精准的信息。
Args: Args:
image_base64: 图片的base64编码 image_base64 (str): 图片的Base64编码字符串。
Returns: Returns:
Tuple[str, list]: 返回表情包描述和情感列表 Tuple[str, List[str]]: 返回一个元组,第一个元素是详细描述,第二个元素是情感关键词列表
如果处理失败,则返回空的描述和列表。
""" """
try: try:
# 解码图片并获取格式 # 1. 解码图片,计算哈希值,并获取格式
# 确保base64字符串只包含ASCII字符
if isinstance(image_base64, str): if isinstance(image_base64, str):
image_base64 = image_base64.encode("ascii", errors="ignore").decode("ascii") image_base64 = image_base64.encode("ascii", errors="ignore").decode("ascii")
image_bytes = base64.b64decode(image_base64) image_bytes = base64.b64decode(image_base64)
image_hash = hashlib.md5(image_bytes).hexdigest() image_hash = hashlib.md5(image_bytes).hexdigest()
image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # type: ignore image_format = Image.open(io.BytesIO(image_bytes)).format.lower() if Image.open(io.BytesIO(image_bytes)).format else "jpeg"
# 尝试从Images表获取已有的详细描述可能在收到表情包时已生成
# 2. 检查数据库中是否已存在该表情包的描述,实现复用
existing_description = None existing_description = None
try: try:
with get_db_session() as session: with get_db_session() as session:
# from src.common.database.database_model_compat import Images existing_image = session.query(Images).filter(
(Images.emoji_hash == image_hash) & (Images.type == "emoji")
existing_image = ( ).one_or_none()
session.query(Images)
.filter((Images.emoji_hash == image_hash) & (Images.type == "emoji"))
.one_or_none()
)
if existing_image and existing_image.description: if existing_image and existing_image.description:
existing_description = existing_image.description existing_description = existing_image.description
logger.info(f"[复用描述] 找到已有详细描述: {existing_description[:50]}...") logger.info(f"[复用描述] 找到已有详细描述: {existing_description[:50]}...")
except Exception as e: except Exception as e:
logger.debug(f"查询已有描述时出错: {e}") logger.debug(f"查询已有表情包描述时出错: {e}")
# 第一步VLM视觉分析如果没有有描述才调用) # 3. 如果没有有描述则调用VLM生成新的详细描述
if existing_description: if existing_description:
description = existing_description description = existing_description
logger.info("[优化] 复用已有的详细描述跳过VLM调用") logger.info("[优化] 复用已有的详细描述跳过VLM调用")
else: else:
logger.info("[VLM分析] 生成新的详细描述") logger.info("[VLM分析] 开始为新表情包生成详细描述")
# 为动态图GIF和静态图构建不同的、要求简洁的prompt
if image_format in ["gif", "GIF"]: if image_format in ["gif", "GIF"]:
image_base64 = get_image_manager().transform_gif(image_base64) # type: ignore image_base64_frames = get_image_manager().transform_gif(image_base64)
if not image_base64: if not image_base64_frames:
raise RuntimeError("GIF表情包转换失败") raise RuntimeError("GIF表情包转换失败")
prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析" prompt = "这是一个GIF动图表情包的关键帧。请用不超过250字详细描述它的核心内容1. 动态画面展现了什么变化2. 它传达了什么核心情绪或玩的是什么梗3. 通常在什么场景下使用?请确保描述既包含关键信息,又能充分展现其内涵。"
description, _ = await self.vlm.generate_response_for_image( description, _ = await self.vlm.generate_response_for_image(
prompt, image_base64, "jpeg", temperature=0.3, max_tokens=1000 prompt, image_base64_frames, "jpeg", temperature=0.3, max_tokens=600
) )
else: else:
prompt = ( prompt = "这是一个表情包。请用不超过250字详细描述它的核心内容1. 画面描绘了什么2. 它传达了什么核心情绪或玩的是什么梗3. 通常在什么场景下使用?请确保描述既包含关键信息,又能充分展现其内涵。"
"这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析"
)
description, _ = await self.vlm.generate_response_for_image( description, _ = await self.vlm.generate_response_for_image(
prompt, image_base64, image_format, temperature=0.3, max_tokens=1000 prompt, image_base64, image_format, temperature=0.3, max_tokens=600
) )
# 审核表情包 # 4. 内容审核,确保表情包符合规定
if global_config.emoji.content_filtration: if global_config.emoji.content_filtration:
prompt = f''' prompt = f'''
这是一个表情包,请对这个表情包进行审核,标准如下 请根据以下标准审核这个表情包
1. 必须符合"{global_config.emoji.filtration_prompt}"的要求 1. 主题必须符合"{global_config.emoji.filtration_prompt}"
2. 不能是色情、暴力、等违法违规内容,必须符合公序良俗 2. 内容健康,不含色情、暴力、政治敏感等元素。
3. 不能是任何形式的截图,聊天记录或视频截图 3. 必须是表情包,而不是普通的聊天截图或视频截图
4. 不要出现5个以上文字 4. 表情包中的文字数量如果有不能超过5个。
请回答这个表情包是否满足上述要求,是则回答是,否则回答否,不要出现任何其他内容 这个表情包是否完全满足以上所有要求?请只回答“是”或“否”。
''' '''
content, _ = await self.vlm.generate_response_for_image( content, _ = await self.vlm.generate_response_for_image(
prompt, image_base64, image_format, temperature=0.3, max_tokens=1000 prompt, image_base64, image_format, temperature=0.1, max_tokens=10
) )
if content == "": if "" in content:
logger.warning(f"表情包审核未通过,内容: {description[:50]}...")
return "", [] return "", []
# 第二步LLM情感分析 - 基于详细描述生成情感标签列表(可选) # 5. 基于VLM的详细描述调用LLM提炼情感关键词
emotions = [] emotions = []
if global_config.emoji.enable_emotion_analysis: if global_config.emoji.enable_emotion_analysis:
logger.info("[情感分析] 启用表情包感情关键词二次识别") logger.info("[情感分析] 开始提炼表情包的情感关键词")
emotion_prompt = f""" emotion_prompt = f"""
请你识别这个表情包的含义和适用场景给我简短的描述每个描述不要超过15个字 你是一个互联网“梗”学家和情感分析师。
是一个基于这个表情包的描述:'{description}' 里有一份关于某个表情包的详细描述:
你可以关注其幽默和讽刺意味,动用贴吧,微博,小红书的知识,必须从互联网梗,meme的角度去分析 ---
请直接输出描述,不要出现任何其他内容,如果有多个描述,可以用逗号分隔 {description}
---
请你基于这份描述,提炼出这个表情包最核心的含义和适用场景。
你的任务是:
1. 分析并总结出3到5个最能代表这个表情包的关键词或短语。
2. 这些关键词应该非常凝练,比如“表达无语”、“有点小得意”、“求夸奖”、“猫猫疑惑”等。
3. 每个关键词不要超过15个字。
4. 请直接输出这些关键词,并用逗号分隔,不要添加任何其他解释。
""" """
emotions_text, _ = await self.llm_emotion_judge.generate_response_async( emotions_text, _ = await self.llm_emotion_judge.generate_response_async(
emotion_prompt, temperature=0.7, max_tokens=600 emotion_prompt, temperature=0.6, max_tokens=150
) )
# 处理情感列表
emotions = [e.strip() for e in emotions_text.split(",") if e.strip()] emotions = [e.strip() for e in emotions_text.split(",") if e.strip()]
# 根据情感标签数量随机选择 - 超过5个选3个超过2个选2个
if len(emotions) > 5:
emotions = random.sample(emotions, 3)
elif len(emotions) > 2:
emotions = random.sample(emotions, 2)
else: else:
logger.info("[情感分析] 表情包感情关键词二次识别已禁用") logger.info("[情感分析] 表情包感情关键词二次识别已禁用,跳过此步骤")
emotions = []
logger.info(f"[注册分析] 详细描述: {description[:50]}... -> 情感标签: {emotions}") # 6. 格式化最终的描述,并返回结果
final_description = f"表情包,关键词:[{''.join(emotions)}]。详细描述:{description}"
logger.info(f"[注册分析] VLM描述: {description} -> 提炼出的情感标签: {emotions}")
return f"[表情包:{description}]", emotions return final_description, emotions
except Exception as e: except Exception as e:
logger.error(f"获取表情包描述失败: {str(e)}") logger.error(f"构建表情包描述时发生严重错误: {str(e)}")
logger.error(traceback.format_exc())
return "", [] return "", []
async def register_emoji_by_filename(self, filename: str) -> bool: async def register_emoji_by_filename(self, filename: str) -> bool:

View File

@@ -18,7 +18,6 @@ def init_prompts():
# 并要求模型以 JSON 格式输出一个或多个动作组合。 # 并要求模型以 JSON 格式输出一个或多个动作组合。
Prompt( Prompt(
""" """
{schedule_block}
{mood_block} {mood_block}
{time_block} {time_block}
{identity_block} {identity_block}
@@ -83,10 +82,13 @@ def init_prompts():
{{ {{
"action": "emoji", "action": "emoji",
"target_message_id": "m123", "target_message_id": "m123",
"reason": "用一个可爱的表情来缓和气氛" "reason": "根据我将要回复的文本内容,选择一个最匹配的表情包来增强表达效果。回复的文本是:<TEXT>"
}} }}
] ]
**重要规则:**
当 `reply` 和 `emoji` 动作同时被选择时,`emoji` 动作的 `reason` 字段必须包含 `reply` 动作最终生成的回复文本内容。你需要将 `<TEXT>` 占位符替换为 `reply` 动作的 `reason` 字段内容,以确保表情包的选择与回复文本高度相关。
不要输出markdown格式```json等内容直接输出且仅包含 JSON 列表内容: 不要输出markdown格式```json等内容直接输出且仅包含 JSON 列表内容:
""", """,
"planner_prompt", "planner_prompt",
@@ -101,7 +103,6 @@ def init_prompts():
## 你的内部状态 ## 你的内部状态
{time_block} {time_block}
{identity_block} {identity_block}
{schedule_block}
{mood_block} {mood_block}
## 长期记忆摘要 ## 长期记忆摘要
@@ -115,6 +116,7 @@ def init_prompts():
## 任务 ## 任务
你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。 你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。
**重要提示**:你的日程安排仅供你个人参考,不应作为主动聊天话题的主要来源。请更多地从聊天内容和朋友的动态中寻找灵感。
请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考: 请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考:
- 是否想起了什么之前提到的事情,想问问后来怎么样了? - 是否想起了什么之前提到的事情,想问问后来怎么样了?

View File

@@ -242,48 +242,50 @@ class ImageManager:
logger.warning(f"虽然生成了描述,但是找到缓存表情包描述: {cached_description}") logger.warning(f"虽然生成了描述,但是找到缓存表情包描述: {cached_description}")
return f"[表情包:{cached_description}]" return f"[表情包:{cached_description}]"
# 保存表情包文件和元数据(用于可能的后续分析) # 只有在开启“偷表情包”功能时,才将接收到的表情包保存到待注册目录
logger.debug(f"保存表情包: {image_hash}") if global_config.emoji.steal_emoji:
current_timestamp = time.time() logger.debug(f"偷取表情包功能已开启,保存表情包: {image_hash}")
filename = f"{int(current_timestamp)}_{image_hash[:8]}.{image_format}" current_timestamp = time.time()
emoji_dir = os.path.join(self.IMAGE_DIR, "emoji") filename = f"{int(current_timestamp)}_{image_hash[:8]}.{image_format}"
os.makedirs(emoji_dir, exist_ok=True) emoji_dir = os.path.join(self.IMAGE_DIR, "emoji")
file_path = os.path.join(emoji_dir, filename) os.makedirs(emoji_dir, exist_ok=True)
file_path = os.path.join(emoji_dir, filename)
try:
# 保存文件
with open(file_path, "wb") as f:
f.write(image_bytes)
# 保存到数据库 (Images表) - 包含详细描述用于可能的注册流程
try: try:
from src.common.database.sqlalchemy_models import get_db_session # 保存文件
with open(file_path, "wb") as f:
f.write(image_bytes)
with get_db_session() as session: # 保存到数据库 (Images表) - 包含详细描述用于可能的注册流程
existing_img = session.execute( try:
select(Images).where(and_(Images.emoji_hash == image_hash, Images.type == "emoji")) from src.common.database.sqlalchemy_models import get_db_session
).scalar()
with get_db_session() as session:
existing_img = session.execute(
select(Images).where(and_(Images.emoji_hash == image_hash, Images.type == "emoji"))
).scalar()
if existing_img:
existing_img.path = file_path
existing_img.description = detailed_description # 保存详细描述
existing_img.timestamp = current_timestamp
else:
new_img = Images(
emoji_hash=image_hash,
path=file_path,
type="emoji",
description=detailed_description, # 保存详细描述
timestamp=current_timestamp,
)
session.add(new_img)
session.commit()
except Exception as e:
logger.error(f"保存到Images表失败: {str(e)}")
if existing_img:
existing_img.path = file_path
existing_img.description = detailed_description # 保存详细描述
existing_img.timestamp = current_timestamp
else:
new_img = Images(
emoji_hash=image_hash,
path=file_path,
type="emoji",
description=detailed_description, # 保存详细描述
timestamp=current_timestamp,
)
session.add(new_img)
session.commit()
# 会在上下文管理器中自动调用
except Exception as e: except Exception as e:
logger.error(f"保存到Images表失败: {str(e)}") logger.error(f"保存表情包文件或元数据失败: {str(e)}")
else:
except Exception as e: logger.debug("偷取表情包功能已关闭,跳过保存。")
logger.error(f"保存表情包文件或元数据失败: {str(e)}")
# 保存最终的情感标签到缓存 (ImageDescriptions表) # 保存最终的情感标签到缓存 (ImageDescriptions表)
self._save_description_to_db(image_hash, final_emotion, "emoji") self._save_description_to_db(image_hash, final_emotion, "emoji")

View File

@@ -362,7 +362,7 @@ class EmojiConfig(ValidatedConfigBase):
emoji_activate_type: str = Field(default="random", description="表情包激活类型") emoji_activate_type: str = Field(default="random", description="表情包激活类型")
max_reg_num: int = Field(default=200, description="最大表情包数量") max_reg_num: int = Field(default=200, description="最大表情包数量")
do_replace: bool = Field(default=True, description="是否替换表情包") do_replace: bool = Field(default=True, description="是否替换表情包")
check_interval: int = Field(default=120, description="检查间隔") check_interval: float = Field(default=1.0, ge=0.01, description="检查间隔")
steal_emoji: bool = Field(default=True, description="是否偷取表情包") steal_emoji: bool = Field(default=True, description="是否偷取表情包")
content_filtration: bool = Field(default=False, description="内容过滤") content_filtration: bool = Field(default=False, description="内容过滤")
filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示") filtration_prompt: str = Field(default="符合公序良俗", description="过滤提示")