Files
2026-01-07 15:29:35 +08:00

407 lines
14 KiB
Python

"""
阿里云百炼 wanx2.1-imageedit API 客户端
封装异步任务提交、轮询查询、结果获取等功能
"""
import os
import time
import requests
from typing import Dict, Any, Optional, Tuple
from enum import Enum
class TaskStatus(Enum):
"""任务状态枚举"""
PENDING = "PENDING" # 任务排队中
RUNNING = "RUNNING" # 任务处理中
SUCCEEDED = "SUCCEEDED" # 任务执行成功
FAILED = "FAILED" # 任务执行失败
CANCELED = "CANCELED" # 任务已取消
UNKNOWN = "UNKNOWN" # 任务不存在或状态未知
class WanxImageEditFunction(Enum):
"""图像编辑功能枚举"""
STYLIZATION_ALL = "stylization_all" # 全局风格化
STYLIZATION_LOCAL = "stylization_local" # 局部风格化
DESCRIPTION_EDIT = "description_edit" # 指令编辑
DESCRIPTION_EDIT_WITH_MASK = "description_edit_with_mask" # 局部重绘
REMOVE_WATERMARK = "remove_watermark" # 去文字水印
EXPAND = "expand" # 扩图
SUPER_RESOLUTION = "super_resolution" # 图像超分
COLORIZATION = "colorization" # 图像上色
DOODLE = "doodle" # 线稿生图
CONTROL_CARTOON_FEATURE = "control_cartoon_feature" # 参考卡通形象生图
class WanxAPIError(Exception):
"""API调用错误"""
def __init__(self, code: str, message: str, request_id: str = None):
self.code = code
self.message = message
self.request_id = request_id
super().__init__(f"[{code}] {message}" + (f" (request_id: {request_id})" if request_id else ""))
class WanxImageEditClient:
"""
通义万相2.1图像编辑API客户端
使用示例:
client = WanxImageEditClient(api_key="sk-xxx")
result = client.edit_image(
function="description_edit",
prompt="给她戴上一副墨镜",
base_image_url="https://example.com/image.jpg"
)
"""
# API端点
BASE_URL = "https://dashscope.aliyuncs.com/api/v1"
SUBMIT_ENDPOINT = "/services/aigc/image2image/image-synthesis"
TASK_ENDPOINT = "/tasks/{task_id}"
# 默认配置
DEFAULT_TIMEOUT = 180 # 默认超时时间(秒)
DEFAULT_POLL_INTERVAL_FAST = 3 # 前30秒的轮询间隔
DEFAULT_POLL_INTERVAL_SLOW = 5 # 30秒后的轮询间隔
DEFAULT_FAST_POLL_DURATION = 30 # 快速轮询持续时间
MAX_RETRIES = 3 # 最大重试次数
def __init__(self, api_key: str = None):
"""
初始化客户端
Args:
api_key: 阿里云百炼API Key,如果不提供则从环境变量DASHSCOPE_API_KEY获取
"""
self.api_key = api_key or os.getenv("DASHSCOPE_API_KEY")
if not self.api_key:
raise ValueError(
"API Key未配置!请通过参数传入或设置环境变量DASHSCOPE_API_KEY。\n"
"获取API Key: https://bailian.console.aliyun.com/?tab=model#/api-key"
)
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
})
def submit_task(
self,
function: str,
prompt: str,
base_image_url: str,
mask_image_url: str = None,
n: int = 1,
seed: int = None,
watermark: bool = False,
strength: float = None,
top_scale: float = None,
bottom_scale: float = None,
left_scale: float = None,
right_scale: float = None,
upscale_factor: int = None,
is_sketch: bool = None,
) -> str:
"""
提交图像编辑任务
Args:
function: 编辑功能
prompt: 提示词
base_image_url: 输入图像URL或Base64
mask_image_url: 涂抹区域图像(局部重绘时需要)
n: 生成图片数量 (1-4)
seed: 随机种子
watermark: 是否添加水印
strength: 修改幅度 (0.0-1.0)
top_scale/bottom_scale/left_scale/right_scale: 扩图比例
upscale_factor: 超分倍数 (1-4)
is_sketch: 是否为涂鸦模式
Returns:
task_id: 任务ID
"""
# 构建请求体
input_data = {
"function": function,
"prompt": prompt,
"base_image_url": base_image_url,
}
# 局部重绘需要mask
if mask_image_url:
input_data["mask_image_url"] = mask_image_url
# 构建parameters
parameters = {"n": n}
if seed is not None:
parameters["seed"] = seed
if watermark:
parameters["watermark"] = True
if strength is not None:
parameters["strength"] = strength
if top_scale is not None:
parameters["top_scale"] = top_scale
if bottom_scale is not None:
parameters["bottom_scale"] = bottom_scale
if left_scale is not None:
parameters["left_scale"] = left_scale
if right_scale is not None:
parameters["right_scale"] = right_scale
if upscale_factor is not None:
parameters["upscale_factor"] = upscale_factor
if is_sketch is not None:
parameters["is_sketch"] = is_sketch
request_body = {
"model": "wanx2.1-imageedit",
"input": input_data,
"parameters": parameters,
}
# 发送请求
url = f"{self.BASE_URL}{self.SUBMIT_ENDPOINT}"
headers = {"X-DashScope-Async": "enable"}
response = self._request_with_retry("POST", url, json=request_body, headers=headers)
# 解析响应
data = response.json()
if "code" in data:
raise WanxAPIError(
code=data.get("code", "UNKNOWN"),
message=data.get("message", "未知错误"),
request_id=data.get("request_id")
)
task_id = data.get("output", {}).get("task_id")
if not task_id:
raise WanxAPIError(
code="NO_TASK_ID",
message="响应中未包含task_id",
request_id=data.get("request_id")
)
print(f"[Wanx] 任务已提交: {task_id}")
return task_id
def query_task(self, task_id: str) -> Dict[str, Any]:
"""
查询任务状态和结果
Args:
task_id: 任务ID
Returns:
任务信息字典,包含status, results等
"""
url = f"{self.BASE_URL}{self.TASK_ENDPOINT.format(task_id=task_id)}"
response = self._request_with_retry("GET", url)
data = response.json()
if "code" in data and data["code"]:
raise WanxAPIError(
code=data.get("code", "UNKNOWN"),
message=data.get("message", "未知错误"),
request_id=data.get("request_id")
)
output = data.get("output", {})
return {
"task_id": output.get("task_id"),
"task_status": output.get("task_status"),
"results": output.get("results", []),
"code": output.get("code"),
"message": output.get("message"),
"submit_time": output.get("submit_time"),
"scheduled_time": output.get("scheduled_time"),
"end_time": output.get("end_time"),
"task_metrics": output.get("task_metrics", {}),
"usage": data.get("usage", {}),
"request_id": data.get("request_id"),
}
def wait_for_result(
self,
task_id: str,
timeout: int = None,
poll_interval_fast: float = None,
poll_interval_slow: float = None,
) -> Dict[str, Any]:
"""
等待任务完成并返回结果
Args:
task_id: 任务ID
timeout: 超时时间(秒)
poll_interval_fast: 前30秒的轮询间隔
poll_interval_slow: 30秒后的轮询间隔
Returns:
任务结果
"""
timeout = timeout or self.DEFAULT_TIMEOUT
poll_interval_fast = poll_interval_fast or self.DEFAULT_POLL_INTERVAL_FAST
poll_interval_slow = poll_interval_slow or self.DEFAULT_POLL_INTERVAL_SLOW
start_time = time.time()
poll_count = 0
while True:
elapsed = time.time() - start_time
if elapsed > timeout:
raise WanxAPIError(
code="TIMEOUT",
message=f"任务超时(已等待{timeout}秒)"
)
# 查询任务状态
result = self.query_task(task_id)
status = result.get("task_status")
poll_count += 1
print(f"[Wanx] 轮询 #{poll_count}: 状态={status}, 耗时={elapsed:.1f}秒")
if status == TaskStatus.SUCCEEDED.value:
print(f"[Wanx] 任务完成!总耗时: {elapsed:.1f}秒")
return result
elif status == TaskStatus.FAILED.value:
error_code = result.get("code", "TASK_FAILED")
error_msg = result.get("message", "任务执行失败")
raise WanxAPIError(
code=error_code,
message=error_msg,
request_id=result.get("request_id")
)
elif status == TaskStatus.CANCELED.value:
raise WanxAPIError(
code="TASK_CANCELED",
message="任务已被取消"
)
elif status in [TaskStatus.PENDING.value, TaskStatus.RUNNING.value]:
# 根据已等待时间选择轮询间隔
if elapsed < self.DEFAULT_FAST_POLL_DURATION:
interval = poll_interval_fast
else:
interval = poll_interval_slow
time.sleep(interval)
else:
# 未知状态,继续等待
time.sleep(poll_interval_slow)
def edit_image(
self,
function: str,
prompt: str,
base_image_url: str,
mask_image_url: str = None,
n: int = 1,
seed: int = None,
watermark: bool = False,
strength: float = None,
top_scale: float = None,
bottom_scale: float = None,
left_scale: float = None,
right_scale: float = None,
upscale_factor: int = None,
is_sketch: bool = None,
timeout: int = None,
) -> Dict[str, Any]:
"""
完整的图像编辑流程:提交任务 -> 等待完成 -> 返回结果
返回结果包含:
- results: 结果图像URL列表
- usage: 使用量信息
- task_metrics: 任务统计信息
"""
# 提交任务
task_id = self.submit_task(
function=function,
prompt=prompt,
base_image_url=base_image_url,
mask_image_url=mask_image_url,
n=n,
seed=seed,
watermark=watermark,
strength=strength,
top_scale=top_scale,
bottom_scale=bottom_scale,
left_scale=left_scale,
right_scale=right_scale,
upscale_factor=upscale_factor,
is_sketch=is_sketch,
)
# 等待结果
result = self.wait_for_result(task_id, timeout=timeout)
return result
def _request_with_retry(
self,
method: str,
url: str,
max_retries: int = None,
**kwargs
) -> requests.Response:
"""
带重试的HTTP请求
"""
max_retries = max_retries or self.MAX_RETRIES
last_error = None
# 合并headers
if "headers" in kwargs:
merged_headers = dict(self.session.headers)
merged_headers.update(kwargs["headers"])
kwargs["headers"] = merged_headers
for attempt in range(max_retries):
try:
response = self.session.request(method, url, timeout=30, **kwargs)
# 检查HTTP状态码
if response.status_code == 200:
return response
# 处理错误响应
try:
error_data = response.json()
error_code = error_data.get("code", f"HTTP_{response.status_code}")
error_msg = error_data.get("message", response.text)
except:
error_code = f"HTTP_{response.status_code}"
error_msg = response.text
# 某些错误不重试
if response.status_code in [400, 401, 403, 404]:
raise WanxAPIError(code=error_code, message=error_msg)
last_error = WanxAPIError(code=error_code, message=error_msg)
except requests.exceptions.RequestException as e:
last_error = WanxAPIError(
code="NETWORK_ERROR",
message=f"网络请求失败: {str(e)}"
)
# 重试前等待
if attempt < max_retries - 1:
wait_time = 2 ** attempt # 指数退避
print(f"[Wanx] 请求失败,{wait_time}秒后重试... (尝试 {attempt + 1}/{max_retries})")
time.sleep(wait_time)
raise last_error