调整抖音图文文本与图片分离发送策略

This commit is contained in:
liuwei
2026-05-06 09:54:07 +08:00
parent b526f8f398
commit e414562378

View File

@@ -11,7 +11,7 @@ from urllib.parse import urlparse
from loguru import logger from loguru import logger
from pathlib import Path from pathlib import Path
from PIL import Image, ImageDraw, ImageFont from PIL import Image
from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus from base.plugin_common.plugin_interface import PluginStatus
@@ -171,17 +171,27 @@ class DouyinParserPlugin(MessagePluginInterface):
img_bytes_list.append(b) img_bytes_list.append(b)
if not img_bytes_list: if not img_bytes_list:
return False, "下载图片失败" return False, "下载图片失败"
merged_pages = self._merge_images_vertical_paged(img_bytes_list, 1242, 65000) target_id = roomid if roomid else sender
if not merged_pages:
return False, "图片合并失败" # 图文作品改回“文本与图片分离发送”:
title = media_info.get('title') or "" # 1. 文本单独发送,可读性更强,也方便用户直接复制文案;
# 按你的需求,图文类型不再单独发送一条文本消息。 # 2. 图片数量较少时保留原始逐张展示,避免小图文被强行拼成长图;
# 这里把文案直接绘制到合并后第一页的顶部,让“文字 + 图片”作为同一条图片消息的一部分发送 # 3. 图片较多时再合并,兼顾刷屏控制与浏览体验
if len(title) > 0: note_text = self._build_note_text(media_info)
merged_pages[0] = self._append_title_to_image(merged_pages[0], title) if note_text:
for page in merged_pages: await bot.send_text_message(target_id, note_text)
await self.bot.send_image_message((roomid if roomid else sender), page)
return True, f"发送合并图片成功({len(merged_pages)}页)" if len(img_bytes_list) > 3:
merged_pages = self._merge_images_vertical_paged(img_bytes_list, 1242, 65000)
if not merged_pages:
return False, "图片合并失败"
for page in merged_pages:
await bot.send_image_message(target_id, page)
return True, f"发送合并图片成功({len(merged_pages)}页)"
for image_bytes in img_bytes_list:
await bot.send_image_message(target_id, image_bytes)
return True, f"发送原图成功({len(img_bytes_list)}张)"
else: else:
video_url = media_info.get('url', '') video_url = media_info.get('url', '')
title = media_info.get('title', '无标题') title = media_info.get('title', '无标题')
@@ -847,95 +857,20 @@ class DouyinParserPlugin(MessagePluginInterface):
return image_bytes return image_bytes
return None return None
def _append_title_to_image(self, image_bytes: bytes, title: str) -> bytes: def _build_note_text(self, media_info: Dict[str, Any]) -> str:
""" """
将标题绘制到图片顶部,返回新的图片二进制数据 构建图文作品的单独文本说明
设计说明: 设计说明:
1) 微信接口没有“单条消息同时携带纯文本+图片”的通用发送 API 1) 作者和文案分开展示,用户看到消息时更容易快速理解内容来源
2) 为了满足“图文合并发送”,这里把标题渲染为图片顶部文字区域 2) 不再把文本写进图片,避免图文较多时首图被额外改造
3) 渲染失败时直接回退原图,避免影响主流程可用性 3) 空字段会自动跳过,防止发出大段无意义占位文本
""" """
if not title: author = str(media_info.get("author", "") or "").strip()
return image_bytes title = str(media_info.get("title", "") or "").strip()
try:
source = Image.open(io.BytesIO(image_bytes))
if source.mode in ("RGBA", "P"):
source = source.convert("RGB")
width, height = source.size
# 文字区域留出左右/上下内边距,保证可读性。
pad_x = 36
pad_y = 26
font = self._load_chinese_font(44)
wrapped_lines = self._wrap_text_for_image(title.strip(), font, max(100, width - pad_x * 2))
if not wrapped_lines:
return image_bytes
# 行高按字体大小动态计算,并增加少量行间距。
line_height = max(44, int(font.size * 1.4))
text_block_height = pad_y * 2 + line_height * len(wrapped_lines)
# 新建画布:上方白底承载文案,下方保留原图内容。
canvas = Image.new("RGB", (width, height + text_block_height), (255, 255, 255))
canvas.paste(source, (0, text_block_height))
draw = ImageDraw.Draw(canvas)
y = pad_y
for line in wrapped_lines:
draw.text((pad_x, y), line, font=font, fill=(34, 34, 34))
y += line_height
output = io.BytesIO()
canvas.save(output, format="JPEG", quality=88)
return output.getvalue()
except Exception as e:
self.LOG.warning(f"标题绘制失败,回退原图: {e}")
return image_bytes
def _load_chinese_font(self, size: int) -> ImageFont.FreeTypeFont:
"""
尝试加载常见中文字体,保证标题在不同系统尽量可读。
如果都不可用,则回退到 Pillow 默认字体(可能不支持完整中文)。
"""
font_candidates = [
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/System/Library/Fonts/PingFang.ttc",
]
for font_path in font_candidates:
if os.path.exists(font_path):
try:
return ImageFont.truetype(font_path, size=size)
except Exception:
continue
return ImageFont.load_default()
def _wrap_text_for_image(self, text: str, font: ImageFont.ImageFont, max_width: int) -> List[str]:
"""
按像素宽度将文本自动换行,避免标题超宽被截断。
实现策略:
- 逐字追加,超过最大宽度就换行;
- 保留原有换行语义(按行分段后再逐字处理)。
"""
draw = ImageDraw.Draw(Image.new("RGB", (10, 10)))
lines: List[str] = [] lines: List[str] = []
for para in text.splitlines(): if author:
if not para: lines.append(f"作者:{author}")
lines.append("") if title:
continue lines.append(f"文案:{title}")
current = "" return "\n".join(lines).strip()
for ch in para:
test = current + ch
text_width = int(draw.textlength(test, font=font))
if current and text_width > max_width:
lines.append(current)
current = ch
else:
current = test
if current:
lines.append(current)
return lines