'commit'
This commit is contained in:
167
dsLightRag/QWenImage/QWenImageEditKit.py
Normal file
167
dsLightRag/QWenImage/QWenImageEditKit.py
Normal file
@@ -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"]}')
|
Binary file not shown.
@@ -10,7 +10,7 @@ from fastapi import APIRouter, HTTPException
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from Config.Config import OBS_PREFIX, OBS_BUCKET, OBS_SERVER
|
from Config.Config import OBS_PREFIX, OBS_BUCKET, OBS_SERVER
|
||||||
# 导入QwenImageGenerator类
|
from QWenImage.QWenImageEditKit import QwenImageEditor
|
||||||
from QWenImage.QwenImageKit import QwenImageGenerator
|
from QWenImage.QwenImageKit import QwenImageGenerator
|
||||||
from Util.ObsUtil import ObsUploader
|
from Util.ObsUtil import ObsUploader
|
||||||
|
|
||||||
@@ -20,8 +20,9 @@ router = APIRouter(prefix="/api/qwenImage", tags=["千问生图"])
|
|||||||
# 配置日志
|
# 配置日志
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 初始化图片生成器
|
# 初始化图片生成器和编辑器
|
||||||
image_generator = QwenImageGenerator()
|
image_generator = QwenImageGenerator()
|
||||||
|
image_editor = QwenImageEditor()
|
||||||
|
|
||||||
|
|
||||||
class GenerateImageRequest(BaseModel):
|
class GenerateImageRequest(BaseModel):
|
||||||
@@ -31,6 +32,23 @@ class GenerateImageRequest(BaseModel):
|
|||||||
size: str = Field(default="1328*1328", description="图片尺寸")
|
size: str = Field(default="1328*1328", description="图片尺寸")
|
||||||
api_key: Optional[str] = Field(default=None, description="自定义API密钥")
|
api_key: Optional[str] = Field(default=None, description="自定义API密钥")
|
||||||
|
|
||||||
|
# 添加图片编辑请求模型
|
||||||
|
class EditImageRequest(BaseModel):
|
||||||
|
"""编辑图片请求模型"""
|
||||||
|
prompt: str = Field(..., description="编辑提示词")
|
||||||
|
size: str = Field(default="1328*1328", description="图片尺寸")
|
||||||
|
api_key: Optional[str] = Field(default=None, description="自定义API密钥")
|
||||||
|
# 支持URL或Base64两种格式,二选一
|
||||||
|
image_url: Optional[str] = Field(default=None, description="原始图片URL")
|
||||||
|
image_base64: Optional[str] = Field(default=None, description="原始图片Base64编码")
|
||||||
|
|
||||||
|
@root_validator(pre=True)
|
||||||
|
def check_image_source(cls, values):
|
||||||
|
"""验证图片来源,确保提供了URL或Base64中的一种"""
|
||||||
|
if not values.get("image_url") and not values.get("image_base64"):
|
||||||
|
raise ValueError("必须提供image_url或image_base64中的一种")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
@router.post("/generate")
|
@router.post("/generate")
|
||||||
async def generate_image(request: GenerateImageRequest):
|
async def generate_image(request: GenerateImageRequest):
|
||||||
@@ -116,6 +134,100 @@ async def generate_image(request: GenerateImageRequest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/edit")
|
||||||
|
async def edit_image(request: EditImageRequest):
|
||||||
|
"""编辑图片API接口
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: 包含编辑图片参数的请求对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含编辑结果的字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info(f"接收到图片编辑请求: image_url={request.image_url[:50]}..., prompt={request.prompt[:50]}...")
|
||||||
|
|
||||||
|
# 如果提供了自定义API密钥,创建新的编辑器实例
|
||||||
|
editor = QwenImageEditor(api_key=request.api_key) if request.api_key else image_editor
|
||||||
|
|
||||||
|
# 调用图片编辑API
|
||||||
|
result = editor.edit_image(
|
||||||
|
image_url=request.image_url,
|
||||||
|
prompt=request.prompt,
|
||||||
|
negative_prompt=request.negative_prompt
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理结果
|
||||||
|
if result['success']:
|
||||||
|
logger.info(f"图片编辑成功")
|
||||||
|
|
||||||
|
# 上传到OBS
|
||||||
|
obs_urls = []
|
||||||
|
uploader = ObsUploader()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 下载编辑后的图片
|
||||||
|
import requests
|
||||||
|
response_img = requests.get(result['image_url'], timeout=10)
|
||||||
|
response_img.raise_for_status()
|
||||||
|
bytes_data = response_img.content
|
||||||
|
|
||||||
|
# 生成UUID文件名并上传
|
||||||
|
jpg_file_name = f"{str(uuid.uuid4())}.jpg"
|
||||||
|
object_key = f"{OBS_PREFIX}/QWen3Image/edited/{jpg_file_name}"
|
||||||
|
success, upload_result = uploader.upload_base64_image(object_key, bytes_data)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
obs_url = f"https://{OBS_BUCKET}.{OBS_SERVER}/{object_key}"
|
||||||
|
obs_urls.append(obs_url)
|
||||||
|
logger.info(f"编辑图片上传OBS成功: {obs_url}")
|
||||||
|
else:
|
||||||
|
logger.error(f"编辑图片上传OBS失败: {upload_result}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理编辑图片时出错: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail={
|
||||||
|
"code": 500,
|
||||||
|
"message": "图片处理失败",
|
||||||
|
"error_detail": str(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 构造返回响应
|
||||||
|
response = {
|
||||||
|
"code": 200,
|
||||||
|
"message": "图片编辑成功",
|
||||||
|
"data": {
|
||||||
|
"original_image": request.image_url,
|
||||||
|
"edited_image": result['image_url'],
|
||||||
|
"obs_url": obs_urls[0] if obs_urls else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
logger.error(f"图片编辑失败: {result['error_msg']}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail={
|
||||||
|
"code": 500,
|
||||||
|
"message": "图片编辑失败",
|
||||||
|
"error_detail": result['error_msg']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"处理图片编辑请求时发生异常: {str(e)}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail={
|
||||||
|
"code": 500,
|
||||||
|
"message": "处理请求时发生异常",
|
||||||
|
"error_detail": str(e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config")
|
@router.get("/config")
|
||||||
async def get_image_config():
|
async def get_image_config():
|
||||||
"""获取图片生成配置信息
|
"""获取图片生成配置信息
|
||||||
|
Binary file not shown.
154
dsLightRag/Test/TestQWen3ImageEdit.py
Normal file
154
dsLightRag/Test/TestQWen3ImageEdit.py
Normal file
@@ -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()
|
Reference in New Issue
Block a user