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

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 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.plugin_interface import PluginStatus
@@ -171,17 +171,27 @@ class DouyinParserPlugin(MessagePluginInterface):
img_bytes_list.append(b)
if not img_bytes_list:
return False, "下载图片失败"
merged_pages = self._merge_images_vertical_paged(img_bytes_list, 1242, 65000)
if not merged_pages:
return False, "图片合并失败"
title = media_info.get('title') or ""
# 按你的需求,图文类型不再单独发送一条文本消息。
# 这里把文案直接绘制到合并后第一页的顶部,让“文字 + 图片”作为同一条图片消息的一部分发送
if len(title) > 0:
merged_pages[0] = self._append_title_to_image(merged_pages[0], title)
for page in merged_pages:
await self.bot.send_image_message((roomid if roomid else sender), page)
return True, f"发送合并图片成功({len(merged_pages)}页)"
target_id = roomid if roomid else sender
# 图文作品改回“文本与图片分离发送”:
# 1. 文本单独发送,可读性更强,也方便用户直接复制文案;
# 2. 图片数量较少时保留原始逐张展示,避免小图文被强行拼成长图;
# 3. 图片较多时再合并,兼顾刷屏控制与浏览体验
note_text = self._build_note_text(media_info)
if note_text:
await bot.send_text_message(target_id, note_text)
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:
video_url = media_info.get('url', '')
title = media_info.get('title', '无标题')
@@ -847,95 +857,20 @@ class DouyinParserPlugin(MessagePluginInterface):
return image_bytes
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
2) 为了满足“图文合并发送”,这里把标题渲染为图片顶部文字区域
3) 渲染失败时直接回退原图,避免影响主流程可用性
1) 作者和文案分开展示,用户看到消息时更容易快速理解内容来源
2) 不再把文本写进图片,避免图文较多时首图被额外改造
3) 空字段会自动跳过,防止发出大段无意义占位文本
"""
if not title:
return image_bytes
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)))
author = str(media_info.get("author", "") or "").strip()
title = str(media_info.get("title", "") or "").strip()
lines: List[str] = []
for para in text.splitlines():
if not para:
lines.append("")
continue
current = ""
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
if author:
lines.append(f"作者:{author}")
if title:
lines.append(f"文案:{title}")
return "\n".join(lines).strip()