呀,柒柒,这次我们对表情包系统进行了一次超级棒的大升级哦!它现在变得更聪明、更懂我们的心意啦!就像我一样,总能找到最完美的表情来点亮对话!♪~
这是我为你准备的提交信息,你看看喜不喜欢~
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:
@@ -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:
|
||||||
|
|||||||
@@ -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():
|
|||||||
|
|
||||||
## 任务
|
## 任务
|
||||||
你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。
|
你现在要决定是否主动说些什么。就像一个真实的人一样,有时候会突然想起之前聊到的话题,或者对朋友的近况感到好奇,想主动询问或关心一下。
|
||||||
|
**重要提示**:你的日程安排仅供你个人参考,不应作为主动聊天话题的主要来源。请更多地从聊天内容和朋友的动态中寻找灵感。
|
||||||
|
|
||||||
请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考:
|
请基于聊天内容,用你的判断力来决定是否要主动发言。不要按照固定规则,而是像人类一样自然地思考:
|
||||||
- 是否想起了什么之前提到的事情,想问问后来怎么样了?
|
- 是否想起了什么之前提到的事情,想问问后来怎么样了?
|
||||||
|
|||||||
@@ -242,8 +242,9 @@ 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:
|
||||||
|
logger.debug(f"偷取表情包功能已开启,保存表情包: {image_hash}")
|
||||||
current_timestamp = time.time()
|
current_timestamp = time.time()
|
||||||
filename = f"{int(current_timestamp)}_{image_hash[:8]}.{image_format}"
|
filename = f"{int(current_timestamp)}_{image_hash[:8]}.{image_format}"
|
||||||
emoji_dir = os.path.join(self.IMAGE_DIR, "emoji")
|
emoji_dir = os.path.join(self.IMAGE_DIR, "emoji")
|
||||||
@@ -278,12 +279,13 @@ class ImageManager:
|
|||||||
)
|
)
|
||||||
session.add(new_img)
|
session.add(new_img)
|
||||||
session.commit()
|
session.commit()
|
||||||
# 会在上下文管理器中自动调用
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存到Images表失败: {str(e)}")
|
logger.error(f"保存到Images表失败: {str(e)}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"保存表情包文件或元数据失败: {str(e)}")
|
logger.error(f"保存表情包文件或元数据失败: {str(e)}")
|
||||||
|
else:
|
||||||
|
logger.debug("偷取表情包功能已关闭,跳过保存。")
|
||||||
|
|
||||||
# 保存最终的情感标签到缓存 (ImageDescriptions表)
|
# 保存最终的情感标签到缓存 (ImageDescriptions表)
|
||||||
self._save_description_to_db(image_hash, final_emotion, "emoji")
|
self._save_description_to_db(image_hash, final_emotion, "emoji")
|
||||||
|
|||||||
@@ -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="过滤提示")
|
||||||
|
|||||||
Reference in New Issue
Block a user