From d1f440a8a0bdbd08d30c98009b844c13c118bf94 Mon Sep 17 00:00:00 2001 From: admins Date: Thu, 14 May 2026 01:25:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=92=8C=E5=B0=BA=E5=AF=B8=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 支持图片格式控制:webp, jpg, png - 新增#生图格式指令 - 新增尺寸快捷方式:正方形、横图、竖图、宽屏、手机、小图 - 使用 PIL 进行格式转换,webp 默认质量 85 - 优化文件体积:webp 比 PNG 小 14 倍(93KB vs 1.3MB) 测试结果: - ✅ webp 格式生成成功(1280*720 横图) - ✅ 文件大小:93KB - ✅ 格式验证通过:RIFF (little-endian) data --- feishu_bot.py | 69 +++++++++++++---- modules/wp_image_generator.py | 142 +++++++++++++++++++++++++++------- scripts/wp_generate_image.py | 23 ++++-- 3 files changed, 186 insertions(+), 48 deletions(-) diff --git a/feishu_bot.py b/feishu_bot.py index 2aa3b70..0de054e 100644 --- a/feishu_bot.py +++ b/feishu_bot.py @@ -3,7 +3,7 @@ """ 飞书机器人消息接收与处理脚本 接收飞书消息,解析内容,调用 WordPress 发布脚本 -支持 AI 图片生成 +支持 AI 图片生成(含格式和尺寸控制) """ import os @@ -34,6 +34,16 @@ CATEGORY_MAP = { 'guanyu': 1 } +# 尺寸快捷方式 +SIZE_PRESETS = { + '正方形': '1024*1024', + '横图': '1280*720', + '竖图': '720*1280', + '宽屏': '1440*720', + '手机': '720*1440', + '小图': '512*512' +} + # 配置日志 LOG_DIR = os.path.join(BASE_DIR, 'logs') os.makedirs(LOG_DIR, exist_ok=True) @@ -165,7 +175,8 @@ class FeishuBot: count=instruction.get('image_count', 1), model=instruction.get('image_model', None), size=instruction.get('image_size', None), - style=instruction.get('image_style', None) + style=instruction.get('image_style', None), + image_format=instruction.get('image_format', None) ) # 检查是否为发布+生图指令 elif instruction.get('action') == 'publish_with_image': @@ -178,7 +189,8 @@ class FeishuBot: image_count=instruction.get('image_count', 1), model=instruction.get('image_model', None), size=instruction.get('image_size', None), - style=instruction.get('image_style', None) + style=instruction.get('image_style', None), + image_format=instruction.get('image_format', None) ) # 检查是否为发布指令 elif instruction.get('action') == 'publish': @@ -373,7 +385,8 @@ class FeishuBot: 'image_count': 1, 'image_model': None, 'image_size': None, - 'image_style': None + 'image_style': None, + 'image_format': None } lines = content.strip().split('\n') @@ -421,10 +434,18 @@ class FeishuBot: instruction['image_model'] = model elif line.startswith('#生图尺寸') or line.startswith('#图片尺寸'): size = line.replace('#生图尺寸', '').replace('#图片尺寸', '').strip() - instruction['image_size'] = size + # 支持快捷方式 + if size in SIZE_PRESETS: + instruction['image_size'] = SIZE_PRESETS[size] + else: + instruction['image_size'] = size elif line.startswith('#生图风格') or line.startswith('#图片风格'): style = line.replace('#生图风格', '').replace('#图片风格', '').strip() instruction['image_style'] = style + elif line.startswith('#生图格式') or line.startswith('#图片格式'): + fmt = line.replace('#生图格式', '').replace('#图片格式', '').strip().lower() + if fmt in ['jpg', 'png', 'webp']: + instruction['image_format'] = fmt # 发布+生图指令 elif line.startswith('#发布并生图') or line.startswith('#发布+生图'): instruction['action'] = 'publish_with_image' @@ -530,7 +551,7 @@ class FeishuBot: return f"❌ 发布失败:{str(e)}" def _publish_with_generated_image(self, text='', title='', category='', tags='', status='publish', - image_count=1, model=None, size=None, style=None): + image_count=1, model=None, size=None, style=None, image_format=None): """ 发布文章并自动生成配图 @@ -544,6 +565,7 @@ class FeishuBot: model: 生图模型 size: 图片尺寸 style: 图片风格 + image_format: 图片格式 Returns: str: 回复消息 @@ -570,7 +592,8 @@ class FeishuBot: title=title or '自动提取', content=text, count=image_count, - api_key=config.get('dashscope_api_key') + api_key=config.get('dashscope_api_key'), + image_format=image_format ) if not image_result.get('success'): @@ -613,7 +636,7 @@ class FeishuBot: logger.error(f"发布文章(含 AI 配图)失败:{str(e)}", exc_info=True) return f"❌ 发布失败:{str(e)}" - def _generate_image(self, prompt='', count=1, model=None, size=None, style=None): + def _generate_image(self, prompt='', count=1, model=None, size=None, style=None, image_format=None): """ 生成 AI 图片 @@ -623,6 +646,7 @@ class FeishuBot: model: 模型名称 size: 图片尺寸 style: 图片风格 + image_format: 图片格式 Returns: str: 回复消息 @@ -648,14 +672,16 @@ class FeishuBot: model=model, size=size, count=count, - style=style + style=style, + image_format=image_format ) if result.get('success'): paths = result.get('paths', []) reply = f"✅ AI 生图完成!成功生成 {len(paths)} 张图片\n" reply += f"📊 模型:{result.get('model', 'wanx-v1')}\n" - reply += f"📐 尺寸:{result.get('size', '1024*1024')}\n\n" + reply += f"📐 尺寸:{result.get('size', '1024*1024')}\n" + reply += f"🖼️ 格式:{result.get('format', 'webp')}\n\n" reply += "📁 本地路径:\n" for i, path in enumerate(paths): reply += f" {i+1}. {path}\n" @@ -675,6 +701,7 @@ class FeishuBot: 'dashscope_api_key': '', 'image_model': 'wanx-v1', 'image_size': '1024*1024', + 'image_format': 'webp', 'image_count': 1 } @@ -791,7 +818,7 @@ class FeishuBot: 🖼️ **发布图片文章** 发送图片即可发布 -🎨 **AI 图片生成**(新增) +🎨 **AI 图片生成**(支持格式控制) 使用 AI 生成文章配图或独立图片 --- @@ -807,15 +834,24 @@ class FeishuBot: --- -**AI 生图指令**(新增): +**AI 生图指令**: `#生图 图片描述` - 生成指定描述的图片 `#生图数量 2` - 设置生成数量(1-4) `#生图模型 wanx-v1` - 选择模型 -`#生图尺寸 1024*1024` - 设置尺寸 +`#生图尺寸 1024*1024` - 设置尺寸(支持快捷方式) `#生图风格 写实` - 设置风格 +`#生图格式 webp` - 设置格式(webp, jpg, png) -**发布+生图指令**(新增): +**尺寸快捷方式**: +- 正方形 → 1024*1024 +- 横图 → 1280*720 +- 竖图 → 720*1280 +- 宽屏 → 1440*720 +- 手机 → 720*1440 +- 小图 → 512*512 + +**发布+生图指令**: `#发布并生图` - 发布文章并自动生成配图 (与 #标题、#分类 等指令配合使用) @@ -835,7 +871,8 @@ class FeishuBot: ``` #生图 一只可爱的猫咪在草地上晒太阳,高质量插画 #生图数量 2 -#生图尺寸 1024*1024 +#生图尺寸 横图 +#生图格式 webp ``` **发布+生图示例**: @@ -875,7 +912,7 @@ class FeishuBot: - **状态**:✅ 运行中 - **WordPress**:https://www.nanlou.net - **发布脚本**:已就绪 -- **AI 生图**:✅ 支持 +- **AI 生图**:✅ 支持(webp/jpg/png) 发送 `#帮助` 查看使用说明""" diff --git a/modules/wp_image_generator.py b/modules/wp_image_generator.py index f666f00..d1df0eb 100644 --- a/modules/wp_image_generator.py +++ b/modules/wp_image_generator.py @@ -3,7 +3,8 @@ """ WordPress 发布系统 - AI 图片生成模块 基于阿里云 DashScope API 实现文生图功能 -支持:通义万相 (wanx-v1) +支持:通义万相 (wanx-v1, wanx2.1-t2i-turbo, wanx2.1-t2i-plus) +支持格式:jpg, png, webp """ import os @@ -34,21 +35,37 @@ class ImageGenerator: 'wanx2.1-t2i-turbo': { 'name': '通义万相 v2 Turbo', 'base_url': 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis', - 'sizes': ['1024*1024', '720*1280', '1280*720', '512*512'], + 'sizes': ['1024*1024', '720*1280', '1280*720', '512*512', '1440*720', '720*1440'], 'max_images': 4 }, 'wanx2.1-t2i-plus': { 'name': '通义万相 v2 Plus', 'base_url': 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text2image/image-synthesis', - 'sizes': ['1024*1024', '720*1280', '1280*720', '512*512'], + 'sizes': ['1024*1024', '720*1280', '1280*720', '512*512', '1440*720', '720*1440'], 'max_images': 4 } } + # 支持的图片格式 + SUPPORTED_FORMATS = ['jpg', 'png', 'webp'] + # 默认尺寸 DEFAULT_SIZE = '1024*1024' - def __init__(self, api_key=None, model='wanx-v1', size=None): + # 默认格式 + DEFAULT_FORMAT = 'webp' + + # 尺寸快捷方式 + SIZE_PRESETS = { + '正方形': '1024*1024', + '横图': '1280*720', + '竖图': '720*1280', + '宽屏': '1440*720', + '手机': '720*1440', + '小图': '512*512' + } + + def __init__(self, api_key=None, model='wanx-v1', size=None, image_format=None): """ 初始化图片生成器 @@ -56,16 +73,22 @@ class ImageGenerator: api_key: DashScope API Key model: 模型名称 size: 图片尺寸 + image_format: 图片格式 (jpg, png, webp) """ self.api_key = api_key or self._get_api_key_from_env() self.model = model if model in self.SUPPORTED_MODELS else 'wanx-v1' self.size = size or self.DEFAULT_SIZE + self.image_format = image_format or self.DEFAULT_FORMAT # 验证尺寸 model_config = self.SUPPORTED_MODELS[self.model] if self.size not in model_config['sizes']: self.size = self.DEFAULT_SIZE + # 验证格式 + if self.image_format not in self.SUPPORTED_FORMATS: + self.image_format = self.DEFAULT_FORMAT + self.base_url = model_config['base_url'] self.max_images = model_config['max_images'] @@ -88,8 +111,8 @@ class ImageGenerator: if n < 1 or n > self.max_images: n = 1 - self.pl.info(f"🎨 AI 生图开始 - 模型:{self.model}, 尺寸:{self.size}, 数量:{n}") - self.dl.log_step("AI 生图", f"模型:{self.model}, 尺寸:{self.size}") + self.pl.info(f"🎨 AI 生图开始 - 模型:{self.model}, 尺寸:{self.size}, 格式:{self.image_format}, 数量:{n}") + self.dl.log_step("AI 生图", f"模型:{self.model}, 尺寸:{self.size}, 格式:{self.image_format}") self.dl.debug(f"提示词:{prompt}") if negative_prompt: self.dl.debug(f"反向提示词:{negative_prompt}") @@ -107,12 +130,13 @@ class ImageGenerator: self.pl.error("生图任务失败:未获取到图片 URL") return [] - # 下载图片到本地 + # 下载图片到本地(转换为指定格式) local_paths = self._download_images(image_urls, prompt) self.pl.success(f"AI 生图完成 - 成功 {len(local_paths)} 张图片") self.dl.log_result("生图结果", { 'model': self.model, + 'format': self.image_format, 'count': len(local_paths), 'paths': local_paths }) @@ -252,7 +276,7 @@ class ImageGenerator: return [] def _download_images(self, image_urls, prompt): - """下载图片到本地""" + """下载图片到本地并转换为指定格式""" local_paths = [] for i, url in enumerate(image_urls): @@ -260,19 +284,14 @@ class ImageGenerator: response = requests.get(url, timeout=30) if response.status_code == 200: - # 生成文件名 - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - prompt_hash = abs(hash(prompt)) % 10000 - filename = f"ai_image_{timestamp}_{i+1}_{prompt_hash}.png" - filepath = os.path.join(IMAGE_DIR, filename) - - with open(filepath, 'wb') as f: - f.write(response.content) - - file_size = os.path.getsize(filepath) - local_paths.append(filepath) - - self.dl.debug(f"图片已保存:{filepath} ({file_size} 字节)") + # 使用 PIL 进行格式转换 + local_path = self._convert_and_save_image( + response.content, + prompt, + i + 1 + ) + if local_path: + local_paths.append(local_path) else: self.pl.error(f"下载图片失败:{url} (状态码:{response.status_code})") @@ -281,6 +300,68 @@ class ImageGenerator: return local_paths + def _convert_and_save_image(self, image_data, prompt, index): + """ + 将图片数据转换格式并保存 + + Args: + image_data: 图片二进制数据 + prompt: 提示词(用于生成文件名) + index: 图片索引 + + Returns: + str: 保存的文件路径 + """ + try: + from PIL import Image + from io import BytesIO + + # 打开图片 + img = Image.open(BytesIO(image_data)) + + # 确保是 RGB 模式(webp 不支持 RGBA) + if img.mode == 'RGBA': + # 创建白色背景 + background = Image.new('RGB', img.size, (255, 255, 255)) + background.paste(img, mask=img.split()[3]) # 使用 alpha 通道作为 mask + img = background + elif img.mode != 'RGB': + img = img.convert('RGB') + + # 生成文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + prompt_hash = abs(hash(prompt)) % 10000 + ext = self.image_format if self.image_format == 'jpg' else self.image_format + filename = f"ai_image_{timestamp}_{index}_{prompt_hash}.{ext}" + filepath = os.path.join(IMAGE_DIR, filename) + + # 保存图片 + if self.image_format == 'webp': + img.save(filepath, 'WEBP', quality=85) + elif self.image_format == 'jpg': + img.save(filepath, 'JPEG', quality=95) + elif self.image_format == 'png': + img.save(filepath, 'PNG') + + file_size = os.path.getsize(filepath) + self.dl.debug(f"图片已保存:{filepath} ({file_size} 字节, 格式:{self.image_format})") + + return filepath + + except ImportError: + self.pl.error("PIL 未安装,无法转换图片格式,保存原始图片") + # 降级:保存原始图片 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + prompt_hash = abs(hash(prompt)) % 10000 + filename = f"ai_image_{timestamp}_{index}_{prompt_hash}.png" + filepath = os.path.join(IMAGE_DIR, filename) + with open(filepath, 'wb') as f: + f.write(image_data) + return filepath + except Exception as e: + self.pl.error(f"转换图片格式失败:{str(e)}") + return None + def _get_api_key_from_env(self): """从环境变量获取 API Key""" return os.environ.get('DASHSCOPE_API_KEY', '') @@ -294,19 +375,27 @@ class ImageGenerator: if model: return self.SUPPORTED_MODELS.get(model, {}) return self.SUPPORTED_MODELS + + def get_supported_formats(self): + """获取支持的格式列表""" + return self.SUPPORTED_FORMATS + + def get_size_presets(self): + """获取尺寸快捷方式""" + return self.SIZE_PRESETS -def create_image_generator(api_key=None, model='wanx-v1', size=None): +def create_image_generator(api_key=None, model='wanx-v1', size=None, image_format=None): """创建图片生成器实例""" - return ImageGenerator(api_key=api_key, model=model, size=size) + return ImageGenerator(api_key=api_key, model=model, size=size, image_format=image_format) if __name__ == '__main__': import sys if len(sys.argv) < 3: - print("用法:python wp_image_generator.py [model] [size] [count]") - print("示例:python wp_image_generator.py sk-xxx '一只可爱的猫咪' wanx-v1 1024*1024 1") + print("用法:python wp_image_generator.py [model] [size] [count] [format]") + print("示例:python wp_image_generator.py sk-xxx '一只可爱的猫咪' wanx-v1 1024*1024 1 webp") sys.exit(1) api_key = sys.argv[1] @@ -314,8 +403,9 @@ if __name__ == '__main__': model = sys.argv[3] if len(sys.argv) > 3 else 'wanx-v1' size = sys.argv[4] if len(sys.argv) > 4 else '1024*1024' count = int(sys.argv[5]) if len(sys.argv) > 5 else 1 + image_format = sys.argv[6] if len(sys.argv) > 6 else 'webp' - generator = create_image_generator(api_key=api_key, model=model, size=size) + generator = create_image_generator(api_key=api_key, model=model, size=size, image_format=image_format) paths = generator.generate_image(prompt, n=count) print(f"\n生成结果:") diff --git a/scripts/wp_generate_image.py b/scripts/wp_generate_image.py index 1278e28..ad756bd 100644 --- a/scripts/wp_generate_image.py +++ b/scripts/wp_generate_image.py @@ -3,6 +3,7 @@ """ WordPress 发布系统 - AI 图片生成脚本 支持命令行调用和模块调用 +支持格式控制:jpg, png, webp """ import os @@ -24,6 +25,7 @@ def load_config(): 'dashscope_api_key': '', 'image_model': 'wanx-v1', 'image_size': '1024*1024', + 'image_format': 'webp', 'image_count': 1, 'image_style': None } @@ -40,7 +42,7 @@ def load_config(): def generate_image(prompt, api_key=None, model=None, size=None, count=None, - negative_prompt=None, style=None): + negative_prompt=None, style=None, image_format=None): """ 生成图片(主入口函数) @@ -52,6 +54,7 @@ def generate_image(prompt, api_key=None, model=None, size=None, count=None, count: 生成数量(可选) negative_prompt: 反向提示词(可选) style: 风格(可选) + image_format: 图片格式 (jpg, png, webp)(可选) Returns: dict: 生成结果 @@ -68,6 +71,7 @@ def generate_image(prompt, api_key=None, model=None, size=None, count=None, size = size or config.get('image_size', '1024*1024') count = count or config.get('image_count', 1) style = style or config.get('image_style', None) + image_format = image_format or config.get('image_format', 'webp') if not api_key: pl.error("未配置 DashScope API Key") @@ -80,7 +84,8 @@ def generate_image(prompt, api_key=None, model=None, size=None, count=None, generator = create_image_generator( api_key=api_key, model=model, - size=size + size=size, + image_format=image_format ) # 生成图片 @@ -98,7 +103,8 @@ def generate_image(prompt, api_key=None, model=None, size=None, count=None, 'count': len(image_paths), 'paths': image_paths, 'model': model, - 'size': size + 'size': size, + 'format': image_format } else: pl.end_publish(False, error_msg='生成图片失败') @@ -116,7 +122,7 @@ def generate_image(prompt, api_key=None, model=None, size=None, count=None, } -def generate_images_for_article(title, content, count=1, api_key=None): +def generate_images_for_article(title, content, count=1, api_key=None, image_format='webp'): """ 根据文章标题和内容生成配图 @@ -125,6 +131,7 @@ def generate_images_for_article(title, content, count=1, api_key=None): content: 文章内容 count: 生成数量 api_key: DashScope API Key(可选) + image_format: 图片格式 (jpg, png, webp) Returns: dict: 生成结果 @@ -135,6 +142,7 @@ def generate_images_for_article(title, content, count=1, api_key=None): # 加载配置 config = load_config() api_key = api_key or config.get('dashscope_api_key', '') + image_format = image_format or config.get('image_format', 'webp') if not api_key: pl.error("未配置 DashScope API Key") @@ -152,7 +160,8 @@ def generate_images_for_article(title, content, count=1, api_key=None): result = generate_image( prompt=prompt, api_key=api_key, - count=count + count=count, + image_format=image_format ) return result @@ -164,6 +173,7 @@ def main(): parser.add_argument('prompt', help='图片描述') parser.add_argument('--model', '-m', default=None, help='模型名称 (wanx-v1, wanx2.1-t2i-turbo, wanx2.1-t2i-plus)') parser.add_argument('--size', '-s', default=None, help='图片尺寸 (1024*1024, 720*1280, 1280*720 等)') + parser.add_argument('--format', '-f', default=None, choices=['jpg', 'png', 'webp'], help='图片格式 (jpg, png, webp)') parser.add_argument('--count', '-n', type=int, default=None, help='生成数量 (1-4)') parser.add_argument('--style', default=None, help='图片风格 (写实、动漫、水彩等)') parser.add_argument('--negative', default=None, help='反向提示词') @@ -178,7 +188,8 @@ def main(): size=args.size, count=args.count, negative_prompt=args.negative, - style=args.style + style=args.style, + image_format=args.format ) # 输出 JSON 结果