From 12d722ea9c1268071227a1893d26e7eb0ff5e09d Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Wed, 27 Aug 2025 11:35:01 +0800 Subject: [PATCH] 'commit' --- dsLightRag/QWenImage/QWenImageEditKit.py | 167 ++++++++++++++++++ .../QWenImageEditKit.cpython-310.pyc | Bin 0 -> 4095 bytes dsLightRag/Routes/QWenImageRoute.py | 116 +++++++++++- .../QWenImageRoute.cpython-310.pyc | Bin 3989 -> 6150 bytes dsLightRag/Test/TestQWen3ImageEdit.py | 154 ++++++++++++++++ 5 files changed, 435 insertions(+), 2 deletions(-) create mode 100644 dsLightRag/QWenImage/QWenImageEditKit.py create mode 100644 dsLightRag/QWenImage/__pycache__/QWenImageEditKit.cpython-310.pyc create mode 100644 dsLightRag/Test/TestQWen3ImageEdit.py diff --git a/dsLightRag/QWenImage/QWenImageEditKit.py b/dsLightRag/QWenImage/QWenImageEditKit.py new file mode 100644 index 00000000..65369893 --- /dev/null +++ b/dsLightRag/QWenImage/QWenImageEditKit.py @@ -0,0 +1,167 @@ +from http import HTTPStatus +from urllib.parse import urlparse, unquote +from pathlib import PurePosixPath +import requests +from dashscope import MultiModalConversation +from Config.Config import ALY_LLM_API_KEY + +class QwenImageEditor: + """Qwen Image Editing Toolkit""" + def __init__(self, api_key=None): + """初始化图片编辑器 + + Args: + api_key: 阿里云API密钥,如果为None则使用配置文件中的密钥 + """ + self.api_key = api_key or ALY_LLM_API_KEY + self.model = "qwen-image-edit" + + def edit_image(self, image_url, prompt, negative_prompt="", stream=False, watermark=True): + """编辑图片 + + Args: + image_url: 原始图片URL + prompt: 编辑指令描述 + negative_prompt: 负面提示词 + stream: 是否流式返回 + watermark: 是否添加水印 + + Returns: + dict: 包含编辑结果的字典 + """ + try: + # 构建对话消息 + messages = [{ + "role": "user", + "content": [ + {"image": image_url}, + {"text": prompt} + ] + }] + + # 调用图片编辑API + response = MultiModalConversation.call( + api_key=self.api_key, + model=self.model, + messages=messages, + result_format='message', + stream=stream, + watermark=watermark, + negative_prompt=negative_prompt + ) + + # 处理响应结果 + if response.status_code == HTTPStatus.OK: + # 提取编辑后的图片URL + edited_image_url = response.output.choices[0].message.content[0].get("image") + + if not edited_image_url: + return { + 'success': False, + 'image_url': None, + 'error_msg': 'API返回空结果,图片编辑失败' + } + + return { + 'success': True, + 'image_url': edited_image_url, + 'error_msg': None + } + else: + error_msg = f'API调用失败: status_code={response.status_code}, code={response.code}, message={response.message}' + return { + 'success': False, + 'image_url': None, + 'error_msg': error_msg + } + except Exception as e: + return { + 'success': False, + 'image_url': None, + 'error_msg': f'发生异常: {str(e)}' + } + + def save_edited_image(self, image_url, save_dir='./'): + """保存编辑后的图片到本地 + + Args: + image_url: 编辑后的图片URL + save_dir: 保存目录 + + Returns: + dict: 包含保存结果的字典 + """ + try: + # 从URL解析文件名 + file_name = PurePosixPath(unquote(urlparse(image_url).path)).parts[-1] + save_path = f'{save_dir}{file_name}' + + # 下载并保存图片 + with open(save_path, 'wb+') as f: + f.write(requests.get(image_url).content) + + return { + 'success': True, + 'file_path': save_path, + 'error_msg': None + } + except Exception as e: + return { + 'success': False, + 'file_path': None, + 'error_msg': f'保存图片失败: {str(e)}' + } + + def edit_and_save_image(self, image_url, prompt, save_dir='./', negative_prompt=""): + """编辑图片并保存到本地 + + Args: + image_url: 原始图片URL + prompt: 编辑指令描述 + save_dir: 保存目录 + negative_prompt: 负面提示词 + + Returns: + dict: 包含编辑和保存结果的字典 + """ + # 编辑图片 + edit_result = self.edit_image(image_url, prompt, negative_prompt) + + if not edit_result['success']: + return edit_result + + # 保存编辑后的图片 + save_result = self.save_edited_image(edit_result['image_url'], save_dir) + + if save_result['success']: + return { + 'success': True, + 'image_url': edit_result['image_url'], + 'file_path': save_result['file_path'], + 'error_msg': None + } + else: + return { + 'success': False, + 'image_url': edit_result['image_url'], + 'file_path': None, + 'error_msg': save_result['error_msg'] + } + +# 示例使用 +if __name__ == "__main__": + # 创建编辑器实例 + editor = QwenImageEditor() + + # 示例参数 + image_url = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg" + edit_prompt = "将图中的人物改为站立姿势,弯腰握住狗的前爪" + + print('----编辑图片,请等待任务执行----') + result = editor.edit_and_save_image(image_url, edit_prompt) + + if result['success']: + print(f'编辑成功,图片URL: {result["image_url"]}') + print(f'保存成功,文件路径: {result["file_path"]}') + else: + print(f'编辑失败: {result["error_msg"]}') diff --git a/dsLightRag/QWenImage/__pycache__/QWenImageEditKit.cpython-310.pyc b/dsLightRag/QWenImage/__pycache__/QWenImageEditKit.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..58c6553e540a832584e5d5ec81ca14397fc482a6 GIT binary patch literal 4095 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 diff --git a/dsLightRag/Test/TestQWen3ImageEdit.py b/dsLightRag/Test/TestQWen3ImageEdit.py new file mode 100644 index 00000000..5fce73ac --- /dev/null +++ b/dsLightRag/Test/TestQWen3ImageEdit.py @@ -0,0 +1,154 @@ +import requests +import json +import logging +import time +import os +import base64 + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger("TestQWenImageEdit") + +# API基础URL +base_url = "http://localhost:8200/api/qwenImage" + + +def test_get_config(): + """测试获取配置接口""" + try: + url = f"{base_url}/config" + logger.info(f"调用获取配置接口: {url}") + response = requests.get(url) + + if response.status_code == 200: + result = response.json() + logger.info(f"获取配置成功: {json.dumps(result, ensure_ascii=False, indent=2)}") + return True, result + else: + logger.error(f"获取配置失败: HTTP状态码={response.status_code}, 响应内容={response.text}") + return False, None + except Exception as e: + logger.exception(f"获取配置时发生异常: {str(e)}") + return False, None + + +def get_test_image_base64(image_path=None): + """获取测试图片的base64编码""" + try: + # 默认使用测试目录下的示例图片 + if not image_path: + test_dir = os.path.dirname(os.path.abspath(__file__)) + image_path = os.path.join(test_dir, "test_image.jpg") + + # 如果默认图片不存在,创建一个简单的base64字符串 + if not os.path.exists(image_path): + logger.warning(f"测试图片不存在,使用默认base64字符串: {image_path}") + return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + + # 读取图片文件并转换为base64 + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode('utf-8') + except Exception as e: + logger.exception(f"获取图片base64编码时发生异常: {str(e)}") + return "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + + +def test_edit_image(prompt, image_base64=None, size='1328*1328', save_local=True): + """测试编辑图片接口(仅支持base64流式上传)""" + try: + url = f"{base_url}/edit" + headers = {"Content-Type": "application/json"} + + # 如果未提供base64,使用默认测试图片 + if not image_base64: + image_base64 = get_test_image_base64() + logger.info("使用默认测试图片base64数据") + + data = { + "prompt": prompt, + "image_base64": image_base64, + "size": size, + "save_local": save_local + } + + logger.info(f"调用编辑图片接口: {url}") + logger.info(f"请求参数: prompt={prompt[:50]}..., size={size}, save_local={save_local}") + + # 记录开始时间 + start_time = time.time() + + # 发送请求 + response = requests.post(url, headers=headers, data=json.dumps(data, ensure_ascii=False)) + + # 计算耗时 + elapsed_time = time.time() - start_time + logger.info(f"请求耗时: {elapsed_time:.2f}秒") + + if response.status_code == 200: + result = response.json() + logger.info(f"编辑图片成功: {json.dumps(result, ensure_ascii=False, indent=2)}") + + # 检查返回的数据 + if result.get("code") == 200 and "data" in result: + images = result["data"].get("images", []) + logger.info(f"成功编辑{len(images)}张图片") + + # 如果保存了本地文件,检查文件是否存在 + if save_local and "local_file_paths" in result["data"]: + for file_path in result["data"]["local_file_paths"]: + full_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), file_path.lstrip('.')) + if os.path.exists(full_path): + logger.info(f"本地文件已保存: {full_path}") + else: + logger.warning(f"本地文件不存在: {full_path}") + + return True, result + else: + logger.error(f"编辑图片失败: HTTP状态码={response.status_code}, 响应内容={response.text}") + return False, None + except Exception as e: + logger.exception(f"编辑图片时发生异常: {str(e)}") + return False, None + + +def main(): + """主函数,运行单元测试""" + logger.info("===== 开始测试QWenImage编辑接口 ====") + + # 1. 测试获取配置接口 + logger.info("\n1. 测试获取配置接口") + config_success, config_data = test_get_config() + + # 2. 测试编辑图片接口 - 基本测试 + logger.info("\n2. 测试编辑图片接口 - 基本测试") + basic_prompt = "将图片转换为水彩画风格,增加明亮度" + edit_success, edit_data = test_edit_image( + prompt=basic_prompt, + size="1328*1328", + save_local=True + ) + + # 3. 测试编辑图片接口 - 不同参数 + if config_success: + supported_sizes = config_data["data"].get("supported_sizes", ["1328*1328"]) + + logger.info(f"\n3. 测试编辑图片接口 - 不同参数(size={supported_sizes[0]})") + different_prompt = "添加下雪效果,转为冷色调" + test_edit_image( + prompt=different_prompt, + size=supported_sizes[0], + save_local=True + ) + + logger.info("\n===== QWenImage编辑接口测试完成 =====") + + # 输出测试结果摘要 + success_count = sum([config_success, edit_success]) + logger.info(f"测试结果: {success_count} 接口测试成功") + + +if __name__ == "__main__": + main() \ No newline at end of file