From c1f4af7e94918045f1b14cf4fc45181ea7e5d0a2 Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:49:48 +0800 Subject: [PATCH 01/12] Create qwen_image_edit_new.py --- .../components/generations/qwen_image_edit_new.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/agentscope_bricks/components/generations/qwen_image_edit_new.py diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -0,0 +1 @@ + From fc2fa768679953cee053d5eafb404ffb2e7f774f Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:03:33 +0800 Subject: [PATCH 02/12] Implement QwenImageEdit component for image editing This component allows users to edit images using prompts and returns the edited image URLs. --- .../generations/qwen_image_edit_new.py | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index 8b13789..d78334f 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -1 +1,257 @@ +# -*- coding: utf-8 -*- +import asyncio +import os +import uuid +from typing import Any, Optional +from dashscope import AioMultiModalConversation +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class QwenImageEditInput(BaseModel): + """ + Qwen Image Edit Input + """ + + image_url: str = Field( + ..., + description="输入图像的URL地址,需为公网可访问地址,支持 HTTP 或 HTTPS " + "协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384," + "3072],大小不超过10MB。URL不能包含中文字符。", + ) + prompt: str = Field( + ..., + description="正向提示词,用来描述生成图像中期望包含的元素和视觉特点, 超过800个字符自动截断", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制,超过500个字符自动截断", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否添加水印,默认不设置。可设置为true或false。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing headers for mcp only, " + "don't generate it", + ) + + +class QwenImageEditOutput(BaseModel): + """ + Qwen Image Edit Output + """ + + results: list[str] = Field( + title="Results", + description="输出的图片url列表", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="请求ID", + ) + + +class QwenImageEdit(Component[QwenImageEditInput, QwenImageEditOutput]): + """ + Qwen Image Edit Component for AI-powered image editing. + """ + + name: str = "modelstudio_qwen_image_edit" + description: str = ( + "通义千问-图像编辑模型支持精准的中英双语文字编辑、调色、细节增强、风格迁移、增删物体、改变位置和动作等操作,可实现复杂的图文编辑。" + ) + + @trace(trace_type="AIGC", trace_name="qwen_image_edit") + async def arun( + self, + args: QwenImageEditInput, + **kwargs: Any, + ) -> QwenImageEditOutput: + """Qwen Image Edit using MultiModalConversation API + + This method uses DashScope's MultiModalConversation service to edit + images based on text prompts. The API supports various image editing + operations through natural language instructions. + + Args: + args: QwenImageEditInput containing image_url, text_prompt, + watermark, and negative_prompt. + **kwargs: Additional keyword arguments including request_id, + trace_event, model_name, api_key. + + Returns: + QwenImageEditOutput containing the edited image URL and request ID. + + Raises: + ValueError: If DASHSCOPE_API_KEY is not set or invalid. + RuntimeError: If the API call fails or returns an error. + """ + + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + model_name = kwargs.get( + "model_name", + os.getenv("QWEN_IMAGE_EDIT_MODEL_NAME", "qwen-image-edit"), + ) + + # Prepare messages in the format expected by MultiModalConversation + messages = [ + { + "role": "user", + "content": [ + {"image": args.image_url}, + {"text": args.prompt}, + ], + }, + ] + + parameters = {} + if args.negative_prompt: + parameters["negative_prompt"] = args.negative_prompt + if args.watermark is not None: + parameters["watermark"] = args.watermark + + # Call the AioMultiModalConversation API asynchronously + try: + response = await AioMultiModalConversation.call( + api_key=api_key, + model=model_name, + messages=messages, + **parameters, + ) + except Exception as e: + raise RuntimeError(f"Failed to call Qwen Image Edit API: {str(e)}") + + # Check response status + if response.status_code != 200 or not response.output: + raise RuntimeError(f"Failed to generate: {response}") + + # Extract the edited image URLs from response + try: + # The response structure may vary, try different possible locations + results = [] + + # Try to get from output.choices[0].message.content + if hasattr(response, "output") and response.output: + choices = getattr(response.output, "choices", []) + if choices: + message = getattr(choices[0], "message", {}) + if hasattr(message, "content"): + content = message.content + if isinstance(content, list): + # Look for image content in the list + for item in content: + if isinstance(item, dict) and "image" in item: + results.append(item["image"]) + elif isinstance(content, str): + results.append(content) + elif isinstance(content, dict) and "image" in content: + results.append(content["image"]) + + if not results: + raise RuntimeError( + f"Could not extract edited image URLs from response: " + f"{response}", + ) + + except Exception as e: + raise RuntimeError( + f"Failed to parse response from Qwen Image Edit API: {str(e)}", + ) + + # Get request ID + if request_id == "": + request_id = getattr(response, "request_id", None) or str( + uuid.uuid4(), + ) + + # Log trace event if provided + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "qwen_image_edit_result": { + "status_code": response.status_code, + "results": results, + }, + }, + }, + ) + + return QwenImageEditOutput( + results=results, + request_id=request_id, + ) + + +if __name__ == "__main__": + qwen_image_edit = QwenImageEdit() + + async def main() -> None: + import time + + base_image_url = ( + "https://dashscope.oss-cn-beijing.aliyuncs.com/" + "images/dog_and_girl.jpeg" + ) + test_inputs = [ + QwenImageEditInput( + image_url=base_image_url, + prompt="将图中的人物改为站立姿势,弯腰握住狗的前爪", + negative_prompt="", + ), + # QwenImageEditInput( + # base_image_url=base_image_url, + # "dog_and_girl.jpeg", + # prompt="给图中的小狗戴上一顶红色的帽子", + # negative_prompt="blurry, low quality", + # ), + ] + + start_time = time.time() + + try: + tasks = [ + qwen_image_edit.arun(test_input) for test_input in test_inputs + ] + + results = await asyncio.gather(*tasks, return_exceptions=True) + + end_time = time.time() + total_time = end_time - start_time + + print(f"\nAll calls completed in {total_time:.2f} seconds") + print("=" * 60) + + # Process and display results + for i, result in enumerate(results, 1): + print(f" 🆔 Request ID: {result.request_id}") + print(f"\n📝 Call {i} Result:") + if isinstance(result, Exception): + print(f" ❌ Error: {str(result)}") + else: + print(f" 🔗 Results: {result.results}") + print("-" * 40) + + except Exception as e: + print(f"❌ Unexpected error during concurrent execution: {str(e)}") + + asyncio.run(main()) From 5d09c0dd2e5560e6da159786361b5fc2f6e709a8 Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:29:09 +0800 Subject: [PATCH 03/12] Refactor QwenImageEdit for batch image processing Refactor Qwen image editing component to support batch processing of multiple images. Update input and output models to handle lists of image URLs and adjust method implementations accordingly. --- .../generations/qwen_image_edit_new.py | 253 ++++++++---------- 1 file changed, 118 insertions(+), 135 deletions(-) diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index d78334f..18d144b 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -2,7 +2,7 @@ import asyncio import os import uuid -from typing import Any, Optional +from typing import Any, Optional, List from dashscope import AioMultiModalConversation from mcp.server.fastmcp import Context @@ -14,20 +14,20 @@ from agentscope_bricks.utils.tracing_utils import TracingUtil -class QwenImageEditInput(BaseModel): +class QwenImageEditNewInput(BaseModel): """ - Qwen Image Edit Input + Qwen Image Edit New Input (Supports multiple images) """ - image_url: str = Field( + image_urls: List[str] = Field( ..., - description="输入图像的URL地址,需为公网可访问地址,支持 HTTP 或 HTTPS " - "协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384," - "3072],大小不超过10MB。URL不能包含中文字符。", + description="输入图像的URL地址列表,每个URL需为公网可访问地址,支持 HTTP 或 HTTPS " + "协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384, 3072],大小不超过10MB。" + "URL不能包含中文字符。", ) prompt: str = Field( ..., - description="正向提示词,用来描述生成图像中期望包含的元素和视觉特点, 超过800个字符自动截断", + description="正向提示词,用来描述生成图像中期望包含的元素和视觉特点,超过800个字符自动截断", ) negative_prompt: Optional[str] = Field( default=None, @@ -39,19 +39,18 @@ class QwenImageEditInput(BaseModel): ) ctx: Optional[Context] = Field( default=None, - description="HTTP request context containing headers for mcp only, " - "don't generate it", + description="HTTP request context containing headers for mcp only, don't generate it", ) -class QwenImageEditOutput(BaseModel): +class QwenImageEditNewOutput(BaseModel): """ - Qwen Image Edit Output + Qwen Image Edit New Output """ - results: list[str] = Field( + results: List[str] = Field( title="Results", - description="输出的图片url列表", + description="输出的编辑后图片URL列表,顺序与输入 image_urls 一致", ) request_id: Optional[str] = Field( default=None, @@ -60,42 +59,39 @@ class QwenImageEditOutput(BaseModel): ) -class QwenImageEdit(Component[QwenImageEditInput, QwenImageEditOutput]): +class QwenImageEditNew(Component[QwenImageEditNewInput, QwenImageEditNewOutput]): """ - Qwen Image Edit Component for AI-powered image editing. + Qwen Image Edit New Component for AI-powered batch image editing. + Supports multiple input images with the same editing instruction. """ - name: str = "modelstudio_qwen_image_edit" + name: str = "modelstudio_qwen_image_edit_new" # ⚠️ 必须唯一! description: str = ( - "通义千问-图像编辑模型支持精准的中英双语文字编辑、调色、细节增强、风格迁移、增删物体、改变位置和动作等操作,可实现复杂的图文编辑。" + "通义千问-图像编辑模型(新版),支持批量处理多张图像。" + "通过统一的文本指令对多张图像执行相同的编辑操作,如增删物体、调色、风格迁移等。" ) - @trace(trace_type="AIGC", trace_name="qwen_image_edit") + @trace(trace_type="AIGC", trace_name="qwen_image_edit_new") async def arun( self, - args: QwenImageEditInput, + args: QwenImageEditNewInput, **kwargs: Any, - ) -> QwenImageEditOutput: - """Qwen Image Edit using MultiModalConversation API + ) -> QwenImageEditNewOutput: + """Batch edit multiple images using Qwen Image Edit API. - This method uses DashScope's MultiModalConversation service to edit - images based on text prompts. The API supports various image editing - operations through natural language instructions. + Each image in `image_urls` will be edited independently using the same prompt. Args: - args: QwenImageEditInput containing image_url, text_prompt, - watermark, and negative_prompt. - **kwargs: Additional keyword arguments including request_id, - trace_event, model_name, api_key. + args: Contains image_urls (list), prompt, negative_prompt, watermark. + **kwargs: Includes request_id, trace_event, model_name, api_key. Returns: - QwenImageEditOutput containing the edited image URL and request ID. + QwenImageEditNewOutput with list of edited image URLs. Raises: - ValueError: If DASHSCOPE_API_KEY is not set or invalid. - RuntimeError: If the API call fails or returns an error. + ValueError: If DASHSCOPE_API_KEY is missing. + RuntimeError: If any API call fails or response is invalid. """ - trace_event = kwargs.pop("trace_event", None) request_id = TracingUtil.get_request_id() @@ -109,78 +105,80 @@ async def arun( os.getenv("QWEN_IMAGE_EDIT_MODEL_NAME", "qwen-image-edit"), ) - # Prepare messages in the format expected by MultiModalConversation - messages = [ - { - "role": "user", - "content": [ - {"image": args.image_url}, - {"text": args.prompt}, - ], - }, - ] - parameters = {} if args.negative_prompt: parameters["negative_prompt"] = args.negative_prompt if args.watermark is not None: parameters["watermark"] = args.watermark - # Call the AioMultiModalConversation API asynchronously - try: - response = await AioMultiModalConversation.call( - api_key=api_key, - model=model_name, - messages=messages, - **parameters, - ) - except Exception as e: - raise RuntimeError(f"Failed to call Qwen Image Edit API: {str(e)}") - - # Check response status - if response.status_code != 200 or not response.output: - raise RuntimeError(f"Failed to generate: {response}") + async def edit_single_image(image_url: str) -> str: + """Edit one image and return its result URL.""" + messages = [ + { + "role": "user", + "content": [ + {"image": image_url}, + {"text": args.prompt}, + ], + }, + ] + try: + response = await AioMultiModalConversation.call( + api_key=api_key, + model=model_name, + messages=messages, + **parameters, + ) + except Exception as e: + raise RuntimeError(f"API call failed for image {image_url}: {str(e)}") - # Extract the edited image URLs from response - try: - # The response structure may vary, try different possible locations - results = [] + if response.status_code != 200 or not response.output: + raise RuntimeError(f"Invalid response for {image_url}: {response}") - # Try to get from output.choices[0].message.content - if hasattr(response, "output") and response.output: + # Parse response to extract image URL + try: choices = getattr(response.output, "choices", []) - if choices: - message = getattr(choices[0], "message", {}) - if hasattr(message, "content"): - content = message.content - if isinstance(content, list): - # Look for image content in the list - for item in content: - if isinstance(item, dict) and "image" in item: - results.append(item["image"]) - elif isinstance(content, str): - results.append(content) - elif isinstance(content, dict) and "image" in content: - results.append(content["image"]) - - if not results: + if not choices: + raise RuntimeError("No choices in response") + + message = getattr(choices[0], "message", {}) + content = getattr(message, "content", []) + + if isinstance(content, str): + return content + elif isinstance(content, dict) and "image" in content: + return content["image"] + elif isinstance(content, list): + for item in content: + if isinstance(item, dict) and "image" in item: + return item["image"] + raise RuntimeError("No image found in response content") + except Exception as parse_error: raise RuntimeError( - f"Could not extract edited image URLs from response: " - f"{response}", + f"Failed to parse response for {image_url}: {parse_error}" ) + # Concurrently process all images + try: + tasks = [edit_single_image(url) for url in args.image_urls] + results = await asyncio.gather(*tasks, return_exceptions=True) except Exception as e: - raise RuntimeError( - f"Failed to parse response from Qwen Image Edit API: {str(e)}", - ) + raise RuntimeError(f"Batch processing failed: {str(e)}") + + # Handle exceptions in individual results + final_results = [] + for i, res in enumerate(results): + if isinstance(res, Exception): + # You may choose to skip, raise, or use placeholder + raise RuntimeError( + f"Image {i} ({args.image_urls[i]}) failed: {res}" + ) + else: + final_results.append(res) - # Get request ID if request_id == "": - request_id = getattr(response, "request_id", None) or str( - uuid.uuid4(), - ) + request_id = str(uuid.uuid4()) - # Log trace event if provided if trace_event: trace_event.on_log( "", @@ -188,70 +186,55 @@ async def arun( "step_suffix": "results", "payload": { "request_id": request_id, - "qwen_image_edit_result": { - "status_code": response.status_code, - "results": results, + "qwen_image_edit_new_result": { + "status": "success", + "result_count": len(final_results), }, }, }, ) - return QwenImageEditOutput( - results=results, + return QwenImageEditNewOutput( + results=final_results, request_id=request_id, ) if __name__ == "__main__": - qwen_image_edit = QwenImageEdit() + editor = QwenImageEditNew() async def main() -> None: - import time - - base_image_url = ( - "https://dashscope.oss-cn-beijing.aliyuncs.com/" - "images/dog_and_girl.jpeg" - ) - test_inputs = [ - QwenImageEditInput( - image_url=base_image_url, - prompt="将图中的人物改为站立姿势,弯腰握住狗的前爪", - negative_prompt="", - ), - # QwenImageEditInput( - # base_image_url=base_image_url, - # "dog_and_girl.jpeg", - # prompt="给图中的小狗戴上一顶红色的帽子", - # negative_prompt="blurry, low quality", - # ), + # 示例:使用公开可访问的测试图片(请替换为你自己的公开图片) + test_image_urls = [ + "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 + # 建议改用你自己上传的公开图片,例如: + # "https://your-public-bucket.oss-cn-shanghai.aliyuncs.com/test1.jpg", + # "https://your-public-bucket.oss-cn-shanghai.aliyuncs.com/test2.jpg", ] - start_time = time.time() + # 如果没有可用的公开图片,先注释掉上面并使用单图测试 + if not test_image_urls or "dashscope-result" in test_image_urls[0]: + print("⚠️ 警告:示例图片 URL 可能无权限访问,请替换为你的公开图片!") + return - try: - tasks = [ - qwen_image_edit.arun(test_input) for test_input in test_inputs - ] - - results = await asyncio.gather(*tasks, return_exceptions=True) - - end_time = time.time() - total_time = end_time - start_time + input_data = QwenImageEditNewInput( + image_urls=test_image_urls, + prompt="给图中的每只狗戴上一顶红色的帽子", + negative_prompt="模糊, 低质量, 失真", + watermark=False, + ) - print(f"\nAll calls completed in {total_time:.2f} seconds") - print("=" * 60) + try: + start = asyncio.get_event_loop().time() + output = await editor.arun(input_data) + elapsed = asyncio.get_event_loop().time() - start - # Process and display results - for i, result in enumerate(results, 1): - print(f" 🆔 Request ID: {result.request_id}") - print(f"\n📝 Call {i} Result:") - if isinstance(result, Exception): - print(f" ❌ Error: {str(result)}") - else: - print(f" 🔗 Results: {result.results}") - print("-" * 40) + print(f"✅ 成功编辑 {len(output.results)} 张图片,耗时: {elapsed:.2f} 秒") + print(f"🆔 Request ID: {output.request_id}") + for i, url in enumerate(output.results, 1): + print(f"🔗 图片 {i}: {url}") except Exception as e: - print(f"❌ Unexpected error during concurrent execution: {str(e)}") + print(f"❌ 错误: {e}") asyncio.run(main()) From aaed80223e34437768f6abd45876f208b5e43f84 Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:38:27 +0800 Subject: [PATCH 04/12] Add QwenImageEditNew to components initialization --- src/agentscope_bricks/components/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/agentscope_bricks/components/__init__.py b/src/agentscope_bricks/components/__init__.py index 594c3f2..5f5d30b 100644 --- a/src/agentscope_bricks/components/__init__.py +++ b/src/agentscope_bricks/components/__init__.py @@ -18,6 +18,9 @@ from agentscope_bricks.components.generations.qwen_image_edit import ( QwenImageEdit, ) +from agentscope_bricks.components.generations.qwen_image_edit_new import ( # ← 新增导入 + QwenImageEditNew, +) from agentscope_bricks.components.generations.qwen_image_generation import ( QwenImageGen, ) @@ -102,7 +105,7 @@ class McpServerMeta(BaseModel): ), "modelstudio_qwen_image": McpServerMeta( instructions="基于通义千问大模型的智能图像生成服务,提供高质量的图像处理和编辑功能", - components=[QwenImageGen, QwenImageEdit], + components=[QwenImageGen, QwenImageEdit, QwenImageEditNew], # ← 新增 QwenImageEditNew ), "modelstudio_web_search": McpServerMeta( instructions="提供实时互联网搜索服务,提供准确及时的信息检索功能", From b6cda889b36c242881d2a85141627394594b863c Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:53:14 +0800 Subject: [PATCH 05/12] Update qwen_image_edit_new.py --- .../components/generations/qwen_image_edit_new.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index 18d144b..89f55ea 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -67,8 +67,8 @@ class QwenImageEditNew(Component[QwenImageEditNewInput, QwenImageEditNewOutput]) name: str = "modelstudio_qwen_image_edit_new" # ⚠️ 必须唯一! description: str = ( - "通义千问-图像编辑模型(新版),支持批量处理多张图像。" - "通过统一的文本指令对多张图像执行相同的编辑操作,如增删物体、调色、风格迁移等。" + "通义千问-图像编辑模型,支持批量处理多张图像。" + "通义千问-图像编辑模型支持精准的中英双语文字编辑、调色、细节增强、风格迁移、增删物体、改变位置和动作等操作,可实现复杂的图文编辑。" ) @trace(trace_type="AIGC", trace_name="qwen_image_edit_new") From dd82232963cdbf495c4edd8290496edad0b321cf Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:16:16 +0800 Subject: [PATCH 06/12] Update qwen_image_edit_new.py --- .../components/generations/qwen_image_edit_new.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index 89f55ea..a55db91 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -206,10 +206,10 @@ async def edit_single_image(image_url: str) -> str: async def main() -> None: # 示例:使用公开可访问的测试图片(请替换为你自己的公开图片) test_image_urls = [ - "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 + #"https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 # 建议改用你自己上传的公开图片,例如: - # "https://your-public-bucket.oss-cn-shanghai.aliyuncs.com/test1.jpg", - # "https://your-public-bucket.oss-cn-shanghai.aliyuncs.com/test2.jpg", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/beach.jpg", ] # 如果没有可用的公开图片,先注释掉上面并使用单图测试 From 2b362b52cb092faa17975f79cc2e77e085276202 Mon Sep 17 00:00:00 2001 From: lether <104270837+lether@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:28:12 +0800 Subject: [PATCH 07/12] Change List type to built-in list in Qwen model --- .../components/generations/qwen_image_edit_new.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index a55db91..8cef491 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -2,7 +2,7 @@ import asyncio import os import uuid -from typing import Any, Optional, List +from typing import Any, Optional from dashscope import AioMultiModalConversation from mcp.server.fastmcp import Context @@ -19,7 +19,7 @@ class QwenImageEditNewInput(BaseModel): Qwen Image Edit New Input (Supports multiple images) """ - image_urls: List[str] = Field( + image_urls: list[str] = Field( ..., description="输入图像的URL地址列表,每个URL需为公网可访问地址,支持 HTTP 或 HTTPS " "协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384, 3072],大小不超过10MB。" @@ -48,7 +48,7 @@ class QwenImageEditNewOutput(BaseModel): Qwen Image Edit New Output """ - results: List[str] = Field( + results: list[str] = Field( title="Results", description="输出的编辑后图片URL列表,顺序与输入 image_urls 一致", ) @@ -209,7 +209,7 @@ async def main() -> None: #"https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 # 建议改用你自己上传的公开图片,例如: "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/beach.jpg", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", ] # 如果没有可用的公开图片,先注释掉上面并使用单图测试 From 2f4436a0df322dc6a580917b5a8e3364e5a533dc Mon Sep 17 00:00:00 2001 From: lether <769451199@qq.com> Date: Wed, 26 Nov 2025 16:22:12 +0800 Subject: [PATCH 08/12] fix: format long lines for flake8 --- src/agentscope_bricks/components/__init__.py | 6 ++++- .../generations/qwen_image_edit_new.py | 26 +++++++++++++------ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/agentscope_bricks/components/__init__.py b/src/agentscope_bricks/components/__init__.py index 5f5d30b..00d9562 100644 --- a/src/agentscope_bricks/components/__init__.py +++ b/src/agentscope_bricks/components/__init__.py @@ -105,7 +105,11 @@ class McpServerMeta(BaseModel): ), "modelstudio_qwen_image": McpServerMeta( instructions="基于通义千问大模型的智能图像生成服务,提供高质量的图像处理和编辑功能", - components=[QwenImageGen, QwenImageEdit, QwenImageEditNew], # ← 新增 QwenImageEditNew + components=[ + QwenImageGen, + QwenImageEdit, + QwenImageEditNew, + ], # ← 新增 QwenImageEditNew ), "modelstudio_web_search": McpServerMeta( instructions="提供实时互联网搜索服务,提供准确及时的信息检索功能", diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index 8cef491..f38d3b7 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -59,7 +59,9 @@ class QwenImageEditNewOutput(BaseModel): ) -class QwenImageEditNew(Component[QwenImageEditNewInput, QwenImageEditNewOutput]): +class QwenImageEditNew( + Component[QwenImageEditNewInput, QwenImageEditNewOutput] +): """ Qwen Image Edit New Component for AI-powered batch image editing. Supports multiple input images with the same editing instruction. @@ -130,10 +132,14 @@ async def edit_single_image(image_url: str) -> str: **parameters, ) except Exception as e: - raise RuntimeError(f"API call failed for image {image_url}: {str(e)}") + raise RuntimeError( + f"API call failed for image {image_url}: {str(e)}" + ) if response.status_code != 200 or not response.output: - raise RuntimeError(f"Invalid response for {image_url}: {response}") + raise RuntimeError( + f"Invalid response for {image_url}: {response}" + ) # Parse response to extract image URL try: @@ -206,15 +212,17 @@ async def edit_single_image(image_url: str) -> str: async def main() -> None: # 示例:使用公开可访问的测试图片(请替换为你自己的公开图片) test_image_urls = [ - #"https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 + # "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 # 建议改用你自己上传的公开图片,例如: - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", ] # 如果没有可用的公开图片,先注释掉上面并使用单图测试 if not test_image_urls or "dashscope-result" in test_image_urls[0]: - print("⚠️ 警告:示例图片 URL 可能无权限访问,请替换为你的公开图片!") + print( + "⚠️ 警告:示例图片 URL 可能无权限访问,请替换为你的公开图片!" + ) return input_data = QwenImageEditNewInput( @@ -229,7 +237,9 @@ async def main() -> None: output = await editor.arun(input_data) elapsed = asyncio.get_event_loop().time() - start - print(f"✅ 成功编辑 {len(output.results)} 张图片,耗时: {elapsed:.2f} 秒") + print( + f"✅ 成功编辑 {len(output.results)} 张图片,耗时: {elapsed:.2f} 秒" + ) print(f"🆔 Request ID: {output.request_id}") for i, url in enumerate(output.results, 1): print(f"🔗 图片 {i}: {url}") From 745511aa4d0e7551aaa2402cd0bced88855e6c52 Mon Sep 17 00:00:00 2001 From: lether <769451199@qq.com> Date: Wed, 26 Nov 2025 17:04:02 +0800 Subject: [PATCH 09/12] fix: format long lines for flake8 --- src/agentscope_bricks/components/__init__.py | 2 +- .../generations/qwen_image_edit_new.py | 57 ++++++++++++------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/agentscope_bricks/components/__init__.py b/src/agentscope_bricks/components/__init__.py index 00d9562..57643b2 100644 --- a/src/agentscope_bricks/components/__init__.py +++ b/src/agentscope_bricks/components/__init__.py @@ -18,7 +18,7 @@ from agentscope_bricks.components.generations.qwen_image_edit import ( QwenImageEdit, ) -from agentscope_bricks.components.generations.qwen_image_edit_new import ( # ← 新增导入 +from agentscope_bricks.components.generations.qwen_image_edit_new import ( # noqa QwenImageEditNew, ) from agentscope_bricks.components.generations.qwen_image_generation import ( diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index f38d3b7..8dbe9ff 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -21,17 +21,25 @@ class QwenImageEditNewInput(BaseModel): image_urls: list[str] = Field( ..., - description="输入图像的URL地址列表,每个URL需为公网可访问地址,支持 HTTP 或 HTTPS " - "协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384, 3072],大小不超过10MB。" - "URL不能包含中文字符。", + description=( + "输入图像的URL地址列表,每个URL需为公网可访问地址,支持 HTTP 或 " + "HTTPS 协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384, " + "3072],大小不超过10MB。URL不能包含中文字符。" + ), ) prompt: str = Field( ..., - description="正向提示词,用来描述生成图像中期望包含的元素和视觉特点,超过800个字符自动截断", + description=( + "正向提示词,用来描述生成图像中期望包含的元素和视觉特点," + "超过800个字符自动截断" + ), ) negative_prompt: Optional[str] = Field( default=None, - description="反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制,超过500个字符自动截断", + description=( + "反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制," + "超过500个字符自动截断" + ), ) watermark: Optional[bool] = Field( default=None, @@ -39,7 +47,10 @@ class QwenImageEditNewInput(BaseModel): ) ctx: Optional[Context] = Field( default=None, - description="HTTP request context containing headers for mcp only, don't generate it", + description=( + "HTTP request context containing headers for mcp only, " + "don't generate it" + ), ) @@ -60,7 +71,7 @@ class QwenImageEditNewOutput(BaseModel): class QwenImageEditNew( - Component[QwenImageEditNewInput, QwenImageEditNewOutput] + Component[QwenImageEditNewInput, QwenImageEditNewOutput], ): """ Qwen Image Edit New Component for AI-powered batch image editing. @@ -70,7 +81,8 @@ class QwenImageEditNew( name: str = "modelstudio_qwen_image_edit_new" # ⚠️ 必须唯一! description: str = ( "通义千问-图像编辑模型,支持批量处理多张图像。" - "通义千问-图像编辑模型支持精准的中英双语文字编辑、调色、细节增强、风格迁移、增删物体、改变位置和动作等操作,可实现复杂的图文编辑。" + "通义千问-图像编辑模型支持精准的中英双语文字编辑、调色、细节增强、" + "风格迁移、增删物体、改变位置和动作等操作,可实现复杂的图文编辑。" ) @trace(trace_type="AIGC", trace_name="qwen_image_edit_new") @@ -81,10 +93,11 @@ async def arun( ) -> QwenImageEditNewOutput: """Batch edit multiple images using Qwen Image Edit API. - Each image in `image_urls` will be edited independently using the same prompt. + Each image in image_urls will be edited + independently using the same prompt Args: - args: Contains image_urls (list), prompt, negative_prompt, watermark. + args: Contains image_urls (list), prompt, negative_prompt,watermark **kwargs: Includes request_id, trace_event, model_name, api_key. Returns: @@ -133,12 +146,12 @@ async def edit_single_image(image_url: str) -> str: ) except Exception as e: raise RuntimeError( - f"API call failed for image {image_url}: {str(e)}" + f"API call failed for image {image_url}: {str(e)}", ) if response.status_code != 200 or not response.output: raise RuntimeError( - f"Invalid response for {image_url}: {response}" + f"Invalid response for {image_url}: {response}", ) # Parse response to extract image URL @@ -161,7 +174,7 @@ async def edit_single_image(image_url: str) -> str: raise RuntimeError("No image found in response content") except Exception as parse_error: raise RuntimeError( - f"Failed to parse response for {image_url}: {parse_error}" + f"Failed to parse response for {image_url}: {parse_error}", ) # Concurrently process all images @@ -177,7 +190,7 @@ async def edit_single_image(image_url: str) -> str: if isinstance(res, Exception): # You may choose to skip, raise, or use placeholder raise RuntimeError( - f"Image {i} ({args.image_urls[i]}) failed: {res}" + f"Image {i} ({args.image_urls[i]}) failed: {res}", ) else: final_results.append(res) @@ -212,16 +225,20 @@ async def edit_single_image(image_url: str) -> str: async def main() -> None: # 示例:使用公开可访问的测试图片(请替换为你自己的公开图片) test_image_urls = [ - # "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/1x6k9vz8h4b3a0/7c8e4f2a-9b1d-4f3e-8c7a-1e2d3f4g5h6i.png?Expires=...&OSSAccessKeyId=...&Signature=...", # ❌ 注意:此链接可能失效 - # 建议改用你自己上传的公开图片,例如: - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg", + ( + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/" + "dog_and_girl.jpeg" + ), + ( + "https://dashscope.oss-cn-beijing.aliyuncs.com/images/" + "dog_and_girl.jpeg" + ), ] # 如果没有可用的公开图片,先注释掉上面并使用单图测试 if not test_image_urls or "dashscope-result" in test_image_urls[0]: print( - "⚠️ 警告:示例图片 URL 可能无权限访问,请替换为你的公开图片!" + "⚠️ 警告:示例图片 URL 可能无权限访问,请替换为你的公开图片!", ) return @@ -238,7 +255,7 @@ async def main() -> None: elapsed = asyncio.get_event_loop().time() - start print( - f"✅ 成功编辑 {len(output.results)} 张图片,耗时: {elapsed:.2f} 秒" + f"✅ 成功编辑 {len(output.results)} 张图片,耗时: {elapsed:.2f} 秒", ) print(f"🆔 Request ID: {output.request_id}") for i, url in enumerate(output.results, 1): From 0876702afc442114f044c1f574e38c35662271e1 Mon Sep 17 00:00:00 2001 From: lether <769451199@qq.com> Date: Tue, 23 Dec 2025 14:14:12 +0800 Subject: [PATCH 10/12] wan2.6 and qwem_image_edit_new --- src/agentscope_bricks/components/__init__.py | 25 +- .../generations/async_image_to_video_wan26.py | 294 +++++++++++++++++ .../generations/async_text_to_video_wan26.py | 298 ++++++++++++++++++ .../components/generations/fetch_wan.py | 121 +++++++ .../generations/image_generation_wan26.py | 194 ++++++++++++ .../generations/qwen_image_edit_new.py | 204 ++++-------- 6 files changed, 996 insertions(+), 140 deletions(-) create mode 100644 src/agentscope_bricks/components/generations/async_image_to_video_wan26.py create mode 100644 src/agentscope_bricks/components/generations/async_text_to_video_wan26.py create mode 100644 src/agentscope_bricks/components/generations/fetch_wan.py create mode 100644 src/agentscope_bricks/components/generations/image_generation_wan26.py diff --git a/src/agentscope_bricks/components/__init__.py b/src/agentscope_bricks/components/__init__.py index 57643b2..6345505 100644 --- a/src/agentscope_bricks/components/__init__.py +++ b/src/agentscope_bricks/components/__init__.py @@ -18,9 +18,6 @@ from agentscope_bricks.components.generations.qwen_image_edit import ( QwenImageEdit, ) -from agentscope_bricks.components.generations.qwen_image_edit_new import ( # noqa - QwenImageEditNew, -) from agentscope_bricks.components.generations.qwen_image_generation import ( QwenImageGen, ) @@ -63,6 +60,19 @@ SpeechToVideoSubmit, SpeechToVideoFetch, ) +from agentscope_bricks.components.generations.async_text_to_video_wan26 import ( # noqa + TextToVideoWan26Submit, +) +from agentscope_bricks.components.generations.async_image_to_video_wan26 import ( # noqa + ImageToVideoWan26Submit, +) +from agentscope_bricks.components.generations.image_generation_wan26 import ( # noqa + ImageGenerationWan26, +) +from agentscope_bricks.components.generations.fetch_wan import WanVideoFetch +from agentscope_bricks.components.generations.qwen_image_edit_new import ( + QwenImageEditNew, +) # noqa class McpServerMeta(BaseModel): @@ -123,4 +133,13 @@ class McpServerMeta(BaseModel): instructions="基于通义千问大模型的语音合成服务,支持多种语言语音合成功能", components=[QwenTextToSpeech], ), + "modelstudio_wan26_media": McpServerMeta( + instructions="基于通义万相大模型2.6版本提供的图像和视频生成服务", + components=[ + ImageGenerationWan26, + TextToVideoWan26Submit, + ImageToVideoWan26Submit, + WanVideoFetch, + ], + ), } diff --git a/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py b/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py new file mode 100644 index 0000000..2de8324 --- /dev/null +++ b/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +import os +import uuid +from http import HTTPStatus +from typing import Any, Optional + +from dashscope.aigc.video_synthesis import AioVideoSynthesis +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class ImageToVideoWan26SubmitInput(BaseModel): + """ + Input model for submitting an image-to-video task using wan2.6-i2v. + """ + + image_url: str = Field( + ..., + description="输入图像,支持公网URL、Base64编码或本地文件路径", + ) + prompt: Optional[str] = Field( + default=None, + description="正向提示词,描述希望视频中发生的动作或变化,例如“镜头缓慢推进,风吹动树叶”。", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,用于排除不希望出现的内容,例如“模糊、闪烁、变形、水印”。", + ) + audio_url: Optional[str] = Field( + default=None, + description="自定义音频文件的公网URL。参数优先级:audio_url > audio。", + ) + audio: Optional[bool] = Field( + default=None, + description="是否自动生成配音。仅在 audio_url 未提供时生效。", + ) + template: Optional[str] = Field( + default=None, + description="视频特效模板,如:squish(解压捏捏)、flying(魔法悬浮)、carousel(时光木马)等。", + ) + resolution: Optional[str] = Field( + default=None, + description="视频分辨率,可选值:'720P'、'1080P'。默认为 '1080P'。", + ) + duration: Optional[int] = Field( + default=None, + description="视频时长(秒),可选值:5、10、15。默认为 5。", + ) + prompt_extend: Optional[bool] = Field( + default=None, + description=" Prompt 智能改写。开启后可提升生成效果,并使 shot_type 生效," + "默认值为 true:开启智能改写。false:不开启智能改写。", + ) + shot_type: Optional[str] = Field( + default=None, + description="镜头类型,仅在 prompt_extend=true 时生效。" + "可选值:'single'(单镜头,默认)、'multi'(多镜头切换)。" + "参数优先级高于 prompt 中的描述。", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否在视频中添加水印(如“AI生成”标识)。默认不添加。", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,用于结果复现。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing headers for mcp only, " + "don't generate it", + ) + + +class ImageToVideoWan26SubmitOutput(BaseModel): + """ + Output of the image-to-video task submission. + """ + + task_id: str = Field( + title="Task ID", + description="异步任务的唯一标识符。", + ) + task_status: str = Field( + title="Task Status", + description="视频生成的任务状态,PENDING:任务排队中,RUNNING:任务处理中,SUCCEEDED:任务执行成功," + "FAILED:任务执行失败,CANCELED:任务取消成功,UNKNOWN:任务不存在或状态未知", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="本次请求的唯一ID,可用于日志追踪。", + ) + + +class ImageToVideoWan26Submit( + Component[ImageToVideoWan26SubmitInput, ImageToVideoWan26SubmitOutput], +): + """ + Submit an image-to-video generation task using the wan2.6-i2v model. + """ + + name: str = "modelstudio_image_to_video_wan26_submit_task" + description: str = ( + "[版本: wan2.6] 通义万相图生视频模型(wan2.6-i2v)异步任务提交工具。基于单张首帧图像和文本提示,生成一段流畅的有声视频。\n" # noqa + "支持视频时长:5秒、10秒或15秒;分辨率:720P、1080P;支持自动配音或传入自定义音频,实现音画同步。\n" + "独家支持多镜头叙事:可生成包含多个镜头的视频,并在镜头切换时保持主体一致性。\n" + "提供特效模板(如“魔法悬浮”、“气球膨胀”),适用于创意视频制作、娱乐特效展示等场景。\n" + ) + + @trace(trace_type="AIGC", trace_name="image_to_video_wan26_submit") + async def arun( + self, + args: ImageToVideoWan26SubmitInput, + **kwargs: Any, + ) -> ImageToVideoWan26SubmitOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + model_name = kwargs.get( + "model_name", + os.getenv("IMAGE_TO_VIDEO_MODEL_NAME", "wan2.6-i2v"), + ) + + # 构建 parameters(全部为可选参数) + parameters = {} + if args.audio is not None: + parameters["audio"] = args.audio + if args.resolution: + parameters["resolution"] = args.resolution + if args.duration is not None: + parameters["duration"] = args.duration + if args.prompt_extend is not None: + parameters["prompt_extend"] = args.prompt_extend + if args.shot_type: + parameters["shot_type"] = args.shot_type + if args.watermark is not None: + parameters["watermark"] = args.watermark + if args.seed is not None: + parameters["seed"] = args.seed + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.async_call( + model=model_name, + api_key=api_key, + img_url=args.image_url, + prompt=args.prompt, + negative_prompt=args.negative_prompt, + audio_url=args.audio_url, + template=args.template, + **parameters, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "submit_task": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or response.output.task_status in ["FAILED", "CANCELED"] + ): + raise RuntimeError( + f"Failed to submit image-to-video task: {response}", + ) + + if not request_id: + request_id = ( + response.request_id + if response.request_id + else str(uuid.uuid4()) + ) + + result = ImageToVideoWan26SubmitOutput( + request_id=request_id, + task_id=response.output.task_id, + task_status=response.output.task_status, + ) + return result + + +# ========== Fetch 部分保持不变(仅微调描述) ========== + + +class ImageToVideoWan26FetchInput(BaseModel): # noqa + task_id: str = Field( + title="Task ID", + description="要查询的视频生成任务ID。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing headers for mcp only, " + "don't generate it", + ) + + +class ImageToVideoWan26FetchOutput(BaseModel): + video_url: str = Field( + title="Video URL", + description="生成视频的公网可访问URL(MP4格式)。", + ) + task_id: str = Field( + title="Task ID", + description="任务ID,与输入一致。", + ) + task_status: str = Field( + title="Task Status", + description="任务最终状态,成功时为 SUCCEEDED。", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="请求ID,用于追踪。", + ) + + +class ImageToVideoWan26Fetch( + Component[ImageToVideoWan26FetchInput, ImageToVideoWan26FetchOutput], +): + name: str = "modelstudio_image_to_video_wan26_fetch_result" + description: str = ( + "查询通义万相 wan2.6-i2v 图生视频任务的结果。" + "输入 Task ID,返回生成的视频 URL 及任务状态。" + "请在提交任务后轮询此接口,直到任务状态变为 SUCCEEDED。" + ) + + @trace(trace_type="AIGC", trace_name="image_to_video_wan26_fetch") + async def arun( + self, + args: ImageToVideoWan26FetchInput, + **kwargs: Any, + ) -> ImageToVideoWan26FetchOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError as e: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") from e + + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.fetch( + api_key=api_key, + task=args.task_id, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": response.request_id, + "fetch_result": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or response.output.task_status in ["FAILED", "CANCELED"] + ): + raise RuntimeError( + f"Failed to fetch image-to-video result: {response}", + ) + + request_id = response.request_id or request_id or str(uuid.uuid4()) + + return ImageToVideoWan26FetchOutput( + video_url=response.output.video_url, + task_id=response.output.task_id, + task_status=response.output.task_status, + request_id=request_id, + ) diff --git a/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py b/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py new file mode 100644 index 0000000..a446597 --- /dev/null +++ b/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +import os +import uuid +from http import HTTPStatus +from typing import Any, Optional + +from dashscope.aigc.video_synthesis import AioVideoSynthesis +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class TextToVideoWan26SubmitInput(BaseModel): + """ + Input model for text-to-video generation submission using wan2.6-t2v. + """ + + prompt: str = Field( + ..., + description="正向提示词,描述希望生成的视频内容,例如“一只宇航员猫在火星上跳舞”", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,描述不希望出现在视频中的内容,例如“模糊、水印、文字、变形”", + ) + audio_url: Optional[str] = Field( + default=None, + description="自定义音频文件URL,模型将使用该音频生成视频。" + "参数优先级:audio_url > audio,仅在 audio_url 为空时 audio 生效。", + ) + audio: Optional[bool] = Field( + default=None, + description="是否自动生成音频。" + "参数优先级:audio_url > audio,仅在 audio_url 为空时 audio 生效。", + ) + size: Optional[str] = Field( + default=None, + description="视频分辨率,默认值为 1920*1080(具体支持值请参考文档)", + ) + duration: Optional[int] = Field( + default=None, + description="视频时长(秒),可选值:5、10、15。默认为 5。", + ) + prompt_extend: Optional[bool] = Field( + default=None, + description="是否开启prompt智能改写,开启后使用大模型对输入prompt进行智能优化", + ) + shot_type: Optional[str] = Field( + default=None, + description="镜头类型,仅在 prompt_extend=true 时生效。" + "可选值:'single'(单镜头,默认)、'multi'(多镜头切换)。" + "参数优先级高于 prompt 中的描述。", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否添加水印,默认不设置", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,用于结果复现。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing headers " + "for mcp only, don't generate it", + ) + + +class TextToVideoWan26SubmitOutput(BaseModel): + """ + Output model for text-to-video generation submission. + """ + + task_id: str = Field( + title="Task ID", + description="视频生成的任务ID", + ) + + task_status: str = Field( + title="Task Status", + description="任务状态:PENDING(排队中)、RUNNING(处理中)、SUCCEEDED(成功)、" + "FAILED(失败)、CANCELED(已取消)、UNKNOWN(未知)", + ) + + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="请求ID,用于追踪", + ) + + +class TextToVideoWan26Submit( + Component[TextToVideoWan26SubmitInput, TextToVideoWan26SubmitOutput], +): + """ + Service for submitting text-to-video + generation tasks using Wan 2.6 T2V model. + """ + + name: str = "modelstudio_text_to_video_wan26_submit_task" + description: str = ( + "[版本: wan2.6] 通义万相文生视频模型(wan2.6-t2v)异步任务提交工具。基于纯文本提示生成一段流畅的有声视频。\n" + "支持视频时长:5秒、10秒或15秒;分辨率:720P、1080P;支持自动配音或传入自定义音频,实现音画同步。\n" + "独家支持多镜头叙事:可生成包含多个镜头的视频,并在镜头切换时保持主体一致性。\n" + ) + + @trace(trace_type="AIGC", trace_name="text_to_video_wan26_submit") + async def arun( + self, + args: TextToVideoWan26SubmitInput, + **kwargs: Any, + ) -> TextToVideoWan26SubmitOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + model_name = kwargs.get( + "model_name", + os.getenv("TEXT_TO_VIDEO_MODEL_NAME", "wan2.6-t2v"), + ) + + parameters = {} + if args.audio is not None: + parameters["audio"] = args.audio + if args.size: + parameters["size"] = args.size + if args.duration is not None: + parameters["duration"] = args.duration + if args.prompt_extend is not None: + parameters["prompt_extend"] = args.prompt_extend + if args.watermark is not None: + parameters["watermark"] = args.watermark + if args.shot_type: + parameters["shot_type"] = args.shot_type + if args.seed is not None: + parameters["seed"] = args.seed + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.async_call( + model=model_name, + api_key=api_key, + prompt=args.prompt, + negative_prompt=args.negative_prompt, + audio_url=args.audio_url, + **parameters, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "submit_task": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or response.output.task_status in ["FAILED", "CANCELED"] + ): + raise RuntimeError( + f"Failed to submit text-to-video task: {response}", + ) + + if not request_id: + request_id = response.request_id or str(uuid.uuid4()) + + result = TextToVideoWan26SubmitOutput( + request_id=request_id, + task_id=response.output.task_id, + task_status=response.output.task_status, + ) + return result + + +class TextToVideoWan26FetchInput(BaseModel): + """ + Input model for fetching text-to-video generation results. + """ + + task_id: str = Field( + title="Task ID", + description="视频生成的任务ID", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing headers " + "for mcp only, don't generate it", + ) + + +class TextToVideoWan26FetchOutput(BaseModel): + """ + Output model for fetching text-to-video generation results. + """ + + video_url: str = Field( + title="Video URL", + description="生成的视频公网可访问URL", + ) + + task_id: str = Field( + title="Task ID", + description="视频生成的任务ID", + ) + + task_status: str = Field( + title="Task Status", + description="任务状态:PENDING、RUNNING、SUCCEEDED、FAILED、CANCELED、UNKNOWN", + ) + + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="请求ID", + ) + + +class TextToVideoWan26Fetch( + Component[TextToVideoWan26FetchInput, TextToVideoWan26FetchOutput], +): + """ + Service for fetching text-to-video generation results. + """ + + name: str = "modelstudio_text_to_video_wan26_fetch_result" + description: str = ( + "通义万相-文生视频模型(wan2.6-t2v)的异步任务结果查询工具,根据Task ID查询生成的视频URL。" + ) + + @trace(trace_type="AIGC", trace_name="text_to_video_wan26_fetch") + async def arun( + self, + args: TextToVideoWan26FetchInput, + **kwargs: Any, + ) -> TextToVideoWan26FetchOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError as e: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") from e + + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.fetch( + api_key=api_key, + task=args.task_id, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": response.request_id, + "fetch_result": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or response.output.task_status in ["FAILED", "CANCELED"] + ): + raise RuntimeError( + f"Failed to fetch text-to-video result: {response}", + ) + + if not request_id: + request_id = ( + response.request_id + if response.request_id + else str(uuid.uuid4()) + ) + + result = TextToVideoWan26FetchOutput( + video_url=response.output.video_url, + task_id=response.output.task_id, + task_status=response.output.task_status, + request_id=request_id, + ) + return result diff --git a/src/agentscope_bricks/components/generations/fetch_wan.py b/src/agentscope_bricks/components/generations/fetch_wan.py new file mode 100644 index 0000000..dfa7d49 --- /dev/null +++ b/src/agentscope_bricks/components/generations/fetch_wan.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +import uuid +from http import HTTPStatus +from typing import Any, Optional + +from dashscope.aigc.video_synthesis import AioVideoSynthesis +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class WanVideoFetchInput(BaseModel): + """ + Input for fetching any Tongyi Wanxiang video generation task result. + """ + + task_id: str = Field( + ..., + title="Task ID", + description="通义万相(Wan)视频生成任务返回的任务ID,适用于文生视频、图生视频等所有异步视频任务", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context for MCP internal " + "use only — do not provide manually.", + ) + + +class WanVideoFetchOutput(BaseModel): + """ + Output of the Wan video task fetch result. + """ + + video_url: str = Field( + ..., + title="Video URL", + description="生成的视频公网可访问url", + ) + task_id: str = Field( + ..., + title="Task ID", + description="对应的任务ID", + ) + task_status: str = Field( + ..., + title="Task Status", + description="任务状态:SUCCEEDED(成功)、FAILED(失败)、" + "CANCELED(取消)、PENDING/RUNNING(进行中)", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="本次查询请求的唯一标识", + ) + + +class WanVideoFetch(Component[WanVideoFetchInput, WanVideoFetchOutput]): + """ + Universal fetch tool for all Tongyi Wanxiang (Wan) video generation tasks. + """ + + name: str = "modelstudio_wan_video_fetch_result" + description: str = ( + "通义万相(Wan)异步任务结果查询工具,根据Task ID查询生成的视频URL。适用于文生视频、图生视频等所有异步视频任务" + ) + + @trace(trace_type="AIGC", trace_name="wan_video_fetch") + async def arun( + self, + args: WanVideoFetchInput, + **kwargs: Any, + ) -> WanVideoFetchOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError as e: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") from e + + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.fetch( + api_key=api_key, + task=args.task_id, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": response.request_id, + "fetch_result": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or getattr(response.output, "task_status", None) + in ["FAILED", "CANCELED"] + ): + raise RuntimeError(f"Failed to fetch Wan video result: {response}") + + final_request_id = ( + request_id or response.request_id or str(uuid.uuid4()) + ) + + return WanVideoFetchOutput( + video_url=response.output.video_url, + task_id=response.output.task_id, + task_status=response.output.task_status, + request_id=final_request_id, + ) diff --git a/src/agentscope_bricks/components/generations/image_generation_wan26.py b/src/agentscope_bricks/components/generations/image_generation_wan26.py new file mode 100644 index 0000000..21def3c --- /dev/null +++ b/src/agentscope_bricks/components/generations/image_generation_wan26.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +import uuid +from typing import Any, Optional +from dashscope import AioMultiModalConversation +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace, TraceType +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class ImageGenerationWan26Input(BaseModel): + """ + Input schema for Wanx 2.6 text-to-image generation. + """ + + prompt: str = Field( + ..., + description="正向提示词,描述期望生成的图像内容,建议详细且清晰。超过800字符将被截断。", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,描述不希望出现的内容,如低质量、模糊、文字等。超过500字符将被截断。", + ) + size: Optional[str] = Field( + default=None, + description="输出图像的分辨率。默认值是1280*1280,可不填。", + ) + prompt_extend: Optional[bool] = Field( + default=None, + description="是否开启 Prompt 智能改写。将使用大模型优化正向提示词。true: 开启(默认),false:不开启。", + ) + n: Optional[int] = Field( + default=1, + description="生成图片的数量。取值范围为1~4张 默认1", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,用于结果复现。", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否添加阿里云水印,默认不添加。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context for " + "MCP internal use only, do not generate it.", + ) + + +class ImageGenerationWan26Output(BaseModel): + """ + Output schema for Wanx 2.6 text-to-image generation. + """ + + results: list[str] = Field( + title="Results", + description="生成的图片URL列表。", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="本次请求的唯一标识。", + ) + + +class ImageGenerationWan26( + Component[ImageGenerationWan26Input, ImageGenerationWan26Output], +): + """ + Wanx 2.6 Text-to-Image Generation Tool. + Uses the 'wan2.6-t2i' model from DashScope + to generate high-quality images from text. + """ + + name: str = "modelstudio_wanx26_image_generation" + description: str = ( + "[版本: wan2.6] 通义万相文生图模型(wanx2.6-t2i)。AI绘画服务,根据文本描述生成高质量图像,并返回图片URL。\n" + "新功能包括图像编辑和图文混合输出,满足更多样化的生成与集成需求。\n" + "支持自定义分辨率:图像面积介于 768×768 至 1440×1440 像素之间," + "允许在该范围内自由调整宽高比(例如 768×2700)。\n" + ) + + @trace(trace_type=TraceType.AIGC, trace_name="wanx26_image_generation") + async def arun( + self, + args: ImageGenerationWan26Input, + **kwargs: Any, + ) -> ImageGenerationWan26Output: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + model_name = "wan2.6-t2i" + messages = [ + { + "role": "user", + "content": [{"text": args.prompt}], + }, + ] + + # Normalize watermark + if args.watermark is not None: + if isinstance(args.watermark, str): + args.watermark = args.watermark.strip().lower() in ( + "true", + "1", + ) + else: + args.watermark = bool(args.watermark) + + parameters = {} + if args.negative_prompt: + parameters["negative_prompt"] = args.negative_prompt + if args.size and args.size != "1024*1024": + parameters["size"] = args.size + if args.n is not None and args.n != 1: + parameters["n"] = args.n + if args.seed is not None: + parameters["seed"] = args.seed + if args.watermark is not None: + parameters["watermark"] = args.watermark + if args.prompt_extend is not None: + parameters["prompt_extend"] = args.prompt_extend + + try: + response = await AioMultiModalConversation.call( + api_key=api_key, + model=model_name, + messages=messages, + **parameters, + ) + except Exception as e: + raise RuntimeError( + f"Failed to call Wanx 2.6 image generation API: {str(e)}", + ) from e + + if response.status_code != 200 or not response.output: + raise RuntimeError(f"Wanx 2.6 image generation failed: {response}") + + results = [] + try: + if hasattr(response, "output") and response.output: + choices = getattr(response.output, "choices", []) + if choices: + message = getattr(choices[0], "message", {}) + content = getattr(message, "content", []) + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and "image" in item: + results.append(item["image"]) + elif isinstance(content, str): + results.append(content) + elif isinstance(content, dict) and "image" in content: + results.append(content["image"]) + except Exception as e: + raise RuntimeError( + f"Failed to parse Wanx 2.6 API response: {str(e)}", + ) from e + + if not results: + raise RuntimeError(f"No image URLs found in response: {response}") + + if not request_id: + request_id = getattr(response, "request_id", None) or str( + uuid.uuid4(), + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "wanx26_image_generation_result": { + "status_code": response.status_code, + "results": results, + }, + }, + }, + ) + + return ImageGenerationWan26Output( + results=results, + request_id=request_id, + ) diff --git a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py index 8dbe9ff..a59bbc8 100644 --- a/src/agentscope_bricks/components/generations/qwen_image_edit_new.py +++ b/src/agentscope_bricks/components/generations/qwen_image_edit_new.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import asyncio import os import uuid from typing import Any, Optional @@ -16,22 +15,20 @@ class QwenImageEditNewInput(BaseModel): """ - Qwen Image Edit New Input (Supports multiple images) + Qwen Image Edit New Input (Supports multiple images for fusion) """ image_urls: list[str] = Field( ..., description=( - "输入图像的URL地址列表,每个URL需为公网可访问地址,支持 HTTP 或 " - "HTTPS 协议。格式:JPG、JPEG、PNG、BMP、TIFF、WEBP,分辨率[384, " - "3072],大小不超过10MB。URL不能包含中文字符。" + "输入图像的URL地址列表,每个URL需为公网可访问地址,例如:['http://example.com/image1.jpg', 'http://example.com/image2.jpg']" # noqa ), ) prompt: str = Field( ..., description=( "正向提示词,用来描述生成图像中期望包含的元素和视觉特点," - "超过800个字符自动截断" + "例如:'将两张图融合成一个赛博朋克城市夜景'。超过800个字符自动截断" ), ) negative_prompt: Optional[str] = Field( @@ -43,7 +40,7 @@ class QwenImageEditNewInput(BaseModel): ) watermark: Optional[bool] = Field( default=None, - description="是否添加水印,默认不设置。可设置为true或false。", + description="是否添加水印,默认不设置。可设置为True或False。", ) ctx: Optional[Context] = Field( default=None, @@ -61,7 +58,7 @@ class QwenImageEditNewOutput(BaseModel): results: list[str] = Field( title="Results", - description="输出的编辑后图片URL列表,顺序与输入 image_urls 一致", + description="输出的融合后图片URL列表,仅包含1个URL", ) request_id: Optional[str] = Field( default=None, @@ -74,15 +71,15 @@ class QwenImageEditNew( Component[QwenImageEditNewInput, QwenImageEditNewOutput], ): """ - Qwen Image Edit New Component for AI-powered batch image editing. - Supports multiple input images with the same editing instruction. + Qwen Image Edit New Component for AI-powered multi-image fusion. + Takes multiple input images and fuses them into a single output image + based on the provided prompt. """ - name: str = "modelstudio_qwen_image_edit_new" # ⚠️ 必须唯一! + name: str = "modelstudio_qwen_image_edit_new" description: str = ( - "通义千问-图像编辑模型,支持批量处理多张图像。" - "通义千问-图像编辑模型支持精准的中英双语文字编辑、调色、细节增强、" - "风格迁移、增删物体、改变位置和动作等操作,可实现复杂的图文编辑。" + "通义千问-多图融合模型,基于 qwen-image-edit,支持将多张图像按提示词语义融合为一张新图。" + "可用于风格混合、场景合成、元素组合等复杂图像生成任务。" ) @trace(trace_type="AIGC", trace_name="qwen_image_edit_new") @@ -91,21 +88,24 @@ async def arun( args: QwenImageEditNewInput, **kwargs: Any, ) -> QwenImageEditNewOutput: - """Batch edit multiple images using Qwen Image Edit API. + """Qwen Image Edit using MultiModalConversation API - Each image in image_urls will be edited - independently using the same prompt + This method uses DashScope's MultiModalConversation service to edit + images based on text prompts. The API supports various image editing + operations through natural language instructions. Args: - args: Contains image_urls (list), prompt, negative_prompt,watermark - **kwargs: Includes request_id, trace_event, model_name, api_key. + args: QwenImageEditInput containing image_url, text_prompt, + watermark, and negative_prompt. + **kwargs: Additional keyword arguments including request_id, + trace_event, model_name, api_key. Returns: - QwenImageEditNewOutput with list of edited image URLs. + QwenImageEditOutput containing the edited image URL and request ID. Raises: - ValueError: If DASHSCOPE_API_KEY is missing. - RuntimeError: If any API call fails or response is invalid. + ValueError: If DASHSCOPE_API_KEY is not set or invalid. + RuntimeError: If the API call fails or returns an error. """ trace_event = kwargs.pop("trace_event", None) request_id = TracingUtil.get_request_id() @@ -125,75 +125,53 @@ async def arun( parameters["negative_prompt"] = args.negative_prompt if args.watermark is not None: parameters["watermark"] = args.watermark + content = [{"image": url} for url in args.image_urls] + content.append({"text": args.prompt}) + + messages = [ + { + "role": "user", + "content": content, + }, + ] - async def edit_single_image(image_url: str) -> str: - """Edit one image and return its result URL.""" - messages = [ - { - "role": "user", - "content": [ - {"image": image_url}, - {"text": args.prompt}, - ], - }, - ] - try: - response = await AioMultiModalConversation.call( - api_key=api_key, - model=model_name, - messages=messages, - **parameters, - ) - except Exception as e: - raise RuntimeError( - f"API call failed for image {image_url}: {str(e)}", - ) - - if response.status_code != 200 or not response.output: - raise RuntimeError( - f"Invalid response for {image_url}: {response}", - ) - - # Parse response to extract image URL - try: - choices = getattr(response.output, "choices", []) - if not choices: - raise RuntimeError("No choices in response") - - message = getattr(choices[0], "message", {}) - content = getattr(message, "content", []) - - if isinstance(content, str): - return content - elif isinstance(content, dict) and "image" in content: - return content["image"] - elif isinstance(content, list): - for item in content: - if isinstance(item, dict) and "image" in item: - return item["image"] - raise RuntimeError("No image found in response content") - except Exception as parse_error: - raise RuntimeError( - f"Failed to parse response for {image_url}: {parse_error}", - ) - - # Concurrently process all images try: - tasks = [edit_single_image(url) for url in args.image_urls] - results = await asyncio.gather(*tasks, return_exceptions=True) + response = await AioMultiModalConversation.call( + api_key=api_key, + model=model_name, + messages=messages, + **parameters, + ) except Exception as e: - raise RuntimeError(f"Batch processing failed: {str(e)}") + raise RuntimeError(f"Multi-image fusion API call failed: {str(e)}") - # Handle exceptions in individual results - final_results = [] - for i, res in enumerate(results): - if isinstance(res, Exception): - # You may choose to skip, raise, or use placeholder - raise RuntimeError( - f"Image {i} ({args.image_urls[i]}) failed: {res}", - ) - else: - final_results.append(res) + if response.status_code != 200 or not response.output: + raise RuntimeError(f"Invalid API response: {response}") + try: + choices = getattr(response.output, "choices", []) + if not choices: + raise RuntimeError("No choices in model response") + + message = getattr(choices[0], "message", {}) + content_output = getattr(message, "content", []) + result_url = None + if isinstance(content_output, str): + result_url = content_output + elif ( + isinstance(content_output, dict) and "image" in content_output + ): + result_url = content_output["image"] + elif isinstance(content_output, list): + for item in content_output: + if isinstance(item, dict) and "image" in item: + result_url = item["image"] + break + + if not result_url: + raise RuntimeError("No image URL found in response") + + except Exception as parse_error: + raise RuntimeError(f"Failed to parse fusion result: {parse_error}") if request_id == "": request_id = str(uuid.uuid4()) @@ -207,61 +185,13 @@ async def edit_single_image(image_url: str) -> str: "request_id": request_id, "qwen_image_edit_new_result": { "status": "success", - "result_count": len(final_results), + "result_count": 1, }, }, }, ) return QwenImageEditNewOutput( - results=final_results, + results=[result_url], request_id=request_id, ) - - -if __name__ == "__main__": - editor = QwenImageEditNew() - - async def main() -> None: - # 示例:使用公开可访问的测试图片(请替换为你自己的公开图片) - test_image_urls = [ - ( - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/" - "dog_and_girl.jpeg" - ), - ( - "https://dashscope.oss-cn-beijing.aliyuncs.com/images/" - "dog_and_girl.jpeg" - ), - ] - - # 如果没有可用的公开图片,先注释掉上面并使用单图测试 - if not test_image_urls or "dashscope-result" in test_image_urls[0]: - print( - "⚠️ 警告:示例图片 URL 可能无权限访问,请替换为你的公开图片!", - ) - return - - input_data = QwenImageEditNewInput( - image_urls=test_image_urls, - prompt="给图中的每只狗戴上一顶红色的帽子", - negative_prompt="模糊, 低质量, 失真", - watermark=False, - ) - - try: - start = asyncio.get_event_loop().time() - output = await editor.arun(input_data) - elapsed = asyncio.get_event_loop().time() - start - - print( - f"✅ 成功编辑 {len(output.results)} 张图片,耗时: {elapsed:.2f} 秒", - ) - print(f"🆔 Request ID: {output.request_id}") - for i, url in enumerate(output.results, 1): - print(f"🔗 图片 {i}: {url}") - - except Exception as e: - print(f"❌ 错误: {e}") - - asyncio.run(main()) From 0c65dbb24f7d53d588fd895687d9bb32c0c5b592 Mon Sep 17 00:00:00 2001 From: lether <769451199@qq.com> Date: Tue, 30 Dec 2025 13:35:48 +0800 Subject: [PATCH 11/12] new_server_and_tool --- src/agentscope_bricks/components/__init__.py | 46 ++- .../generations/async_image_out_painting.py | 332 ++++++++++++++++++ .../async_image_to_video_fl_wan22.py | 291 +++++++++++++++ .../generations/async_image_to_video_wan26.py | 15 +- .../generations/async_text_to_video_wan26.py | 11 +- .../generations/image_edit_wan26.py | 211 +++++++++++ .../generations/image_generation_zimage.py | 162 +++++++++ .../generations/image_out_painting.py | 271 ++++++++++++++ .../image_text_interleave_generation_wan26.py | 198 +++++++++++ 9 files changed, 1512 insertions(+), 25 deletions(-) create mode 100644 src/agentscope_bricks/components/generations/async_image_out_painting.py create mode 100644 src/agentscope_bricks/components/generations/async_image_to_video_fl_wan22.py create mode 100644 src/agentscope_bricks/components/generations/image_edit_wan26.py create mode 100644 src/agentscope_bricks/components/generations/image_generation_zimage.py create mode 100644 src/agentscope_bricks/components/generations/image_out_painting.py create mode 100644 src/agentscope_bricks/components/generations/image_text_interleave_generation_wan26.py diff --git a/src/agentscope_bricks/components/__init__.py b/src/agentscope_bricks/components/__init__.py index 333ba7e..23fe223 100644 --- a/src/agentscope_bricks/components/__init__.py +++ b/src/agentscope_bricks/components/__init__.py @@ -15,9 +15,6 @@ from agentscope_bricks.components.generations.image_edit_wan25 import ( ImageEditWan25, ) -from agentscope_bricks.components.generations.multichannel_speech_to_text import ( # noqa - MultichannelSpeechToText, -) from agentscope_bricks.components.generations.qwen_image_edit import ( QwenImageEdit, ) @@ -75,7 +72,26 @@ from agentscope_bricks.components.generations.fetch_wan import WanVideoFetch from agentscope_bricks.components.generations.qwen_image_edit_new import ( QwenImageEditNew, -) # noqa +) +from agentscope_bricks.components.generations.image_edit_wan26 import ( + ImageEditWan26, +) +from agentscope_bricks.components.generations.image_generation_zimage import ( + ZImageGeneration, +) +from agentscope_bricks.components.generations.async_image_out_painting import ( + ImageOutPaintingSubmit, + ImageOutPaintingFetch, +) +from agentscope_bricks.components.generations.async_image_to_video_fl_wan22 import ( # noqa + ImageToVideoByFirstAndLastFrameWan22Submit, +) +from agentscope_bricks.components.generations.image_out_painting import ( + ImageOutPaintingAuto, +) +from agentscope_bricks.components.generations.image_text_interleave_generation_wan26 import ( # noqa + WanImageInterleaveGeneration, +) class McpServerMeta(BaseModel): @@ -92,7 +108,14 @@ class McpServerMeta(BaseModel): mcp_server_metas: Dict[str, McpServerMeta] = { "modelstudio_wan_image": McpServerMeta( instructions="基于通义万相大模型的智能图像生成服务,提供高质量的图像处理和编辑功能", - components=[ImageGeneration, ImageEdit, ImageStyleRepaint], + components=[ + ImageGeneration, + ImageEdit, + ImageStyleRepaint, + ImageOutPaintingSubmit, + ImageOutPaintingFetch, + ImageOutPaintingAuto, + ], ), "modelstudio_wan_video": McpServerMeta( instructions="基于通义万相大模型提供AI视频生成服务,支持文本到视频、图像到视频和语音到视频的多模态生成功能", @@ -103,6 +126,8 @@ class McpServerMeta(BaseModel): ImageToVideoFetch, SpeechToVideoSubmit, SpeechToVideoFetch, + ImageToVideoByFirstAndLastFrameWan22Submit, + WanVideoFetch, ], ), "modelstudio_wan25_media": McpServerMeta( @@ -130,7 +155,7 @@ class McpServerMeta(BaseModel): ), "modelstudio_speech_to_text": McpServerMeta( instructions="录音文件的语音识别服务,支持多种音频格式的语音转文字功能", - components=[SpeechToText, MultichannelSpeechToText], + components=[SpeechToText], ), "modelstudio_qwen_text_to_speech": McpServerMeta( instructions="基于通义千问大模型的语音合成服务,支持多种语言语音合成功能", @@ -143,6 +168,15 @@ class McpServerMeta(BaseModel): TextToVideoWan26Submit, ImageToVideoWan26Submit, WanVideoFetch, + ImageEditWan26, + WanImageInterleaveGeneration, + ], + ), + "modelstudio_Z_image": McpServerMeta( + instructions="基于通义Z-Image大模型的智能图像生成服务,是一款轻量级文生图模型," + "可快速生成图像,支持中英文字渲染,并灵活适配多种分辨率与宽高比例。", + components=[ + ZImageGeneration, ], ), } diff --git a/src/agentscope_bricks/components/generations/async_image_out_painting.py b/src/agentscope_bricks/components/generations/async_image_out_painting.py new file mode 100644 index 0000000..a60b371 --- /dev/null +++ b/src/agentscope_bricks/components/generations/async_image_out_painting.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +import os +import uuid +from http import HTTPStatus +from typing import Any, Optional, Dict + +import aiohttp +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +DASHSCOPE_API_BASE = "https://dashscope.aliyuncs.com/api/v1" + + +class ImageOutPaintingSubmitInput(BaseModel): + """ + Input model for submitting an image out-painting (expansion) task. + """ + + image_url: str = Field( + ..., + description="输入图像的公网可访问 URL。", + ) + angle: Optional[float] = Field( + default=None, + description="逆时针旋转角度,取值范围 [0, 359]。默认为 0(不旋转)。", + ) + output_ratio: Optional[str] = Field( + default=None, + description='目标宽高比,可选值:["", "1:1", "3:4", "4:3", "9:16", "16:9"]。' + '默认值为"",表示不设置输出图像的宽高比。', + ) + x_scale: Optional[float] = Field( + default=None, + description="水平方向扩展比例(居中扩展),默认 1.0。可以与 y_scale 搭配使用。取值范围 [1.0, 3.0]。" + "例如:输入图像分辨率为1000×1000(宽×高),x_scale=2.0,扩展后的图像分辨率为2000×1000(宽×高)。" + "保持高度不变,左右各添加500个像素。", + ) + y_scale: Optional[float] = Field( + default=None, + description="垂直方向扩展比例(居中扩展),默认 1.0。可以选择与 x_scale 搭配使用。取值范围 [1.0, 3.0]。" + "例如:输入图像分辨率为1000×1000(宽×高),y_scale=2.0,扩展后的图像分辨率为1000×2000(宽×高)。" + "保持宽度不变,上下各添加500个像素。", + ) + top_offset: Optional[float] = Field( + default=None, + description="在图像上方添加的像素数。默认值为0," + "需满足 top_offset + bottom_offset < 3 × 原图高度。" + "输入图像分辨率为1000×1000(宽×高),top_offset=500,扩展后的图像分辨率为1000×1500(宽×高)。" + "保持宽度不变,只在图像上方添加500个像素。", + ) + bottom_offset: Optional[float] = Field( + default=None, + description="在图像下方添加的像素数。默认值为0," + "需满足 top_offset + bottom_offset < 3 × 原图高度。" + "例如:输入图像分辨率为1000×1000(宽×高),bottom_offset=500,扩展后的图像分辨率为1000×1500(宽×高)。" + "保持宽度不变,只在图像下方添加500个像素。", + ) + left_offset: Optional[float] = Field( + default=None, + description="在图像左侧添加的像素数。默认值为0," + "需满足 left_offset + right_offset < 3 × 原图宽度。" + "例如:输入图像分辨率为1000×1000(宽×高),left_offset=500,扩展后的图像分辨率为1500×1000(宽×高)。" + "保持高度不变,只在图像左侧添加500个像素。", + ) + right_offset: Optional[float] = Field( + default=None, + description="在图像右侧添加的像素数。默认值为0," + "需满足 left_offset + right_offset < 3 × 原图宽度。" + "例如:输入图像分辨率为1000×1000(宽×高),right_offset=500,扩展后的图像分辨率为1500×1000(宽×高)。" + "保持高度不变,只在图像右侧添加500个像素。", + ) + best_quality: Optional[bool] = Field( + default=None, + description="是否启用最佳质量模式。默认 false(速度优先),设为 true 可提升细节但耗时增加。", + ) + limit_image_size: Optional[bool] = Field( + default=None, + description="是否限制输出图像大小(≤5MB)。默认 true,建议保持开启。" + "模型生成的图像需要经过一层安全过滤后才能输出,当前不支持大于10M的图像处理。", + ) + add_watermark: Optional[bool] = Field( + default=None, + description="是否添加水印,True:默认值,添加水印,False:不添加水印。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing " + "headers for mcp only, don't generate it", + ) + + +class ImageOutPaintingSubmitOutput(BaseModel): + task_id: str = Field( + title="Task ID", + description="异步任务的唯一标识符,有效期 24 小时。", + ) + task_status: str = Field( + title="Task Status", + description="任务状态:PENDING(排队中)、RUNNING(处理中)、" + "SUCCEEDED(成功)、FAILED(失败)等。", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="请求唯一 ID,用于日志追踪。", + ) + + +class ImageOutPaintingSubmit( + Component[ImageOutPaintingSubmitInput, ImageOutPaintingSubmitOutput], +): + name: str = "modelstudio_image_out_painting_submit" + description: str = ( + "图像画面扩展(扩图)异步任务提交工具,基于image-out-painting 模型。\n" + "支持三种扩图方式(按优先级):\n" + "1. 按宽高比(output_ratio)\n" + "2. 按比例缩放(x_scale / y_scale)\n" + "3. 指定方向像素填充(top/bottom/left/right_offset)\n" + "可选旋转(angle),先旋转后扩图。" + ) + + @trace(trace_type="AIGC", trace_name="image_out_painting_submit") + async def arun( + self, + args: ImageOutPaintingSubmitInput, + **kwargs: Any, + ) -> ImageOutPaintingSubmitOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + # 构建 parameters 字典(只包含非 None 值) + parameters: Dict[str, Any] = {} + if args.angle is not None: + parameters["angle"] = args.angle + if args.output_ratio is not None: + parameters["output_ratio"] = args.output_ratio + if args.x_scale is not None: + parameters["x_scale"] = args.x_scale + if args.y_scale is not None: + parameters["y_scale"] = args.y_scale + if args.top_offset is not None: + parameters["top_offset"] = args.top_offset + if args.bottom_offset is not None: + parameters["bottom_offset"] = args.bottom_offset + if args.left_offset is not None: + parameters["left_offset"] = args.left_offset + if args.right_offset is not None: + parameters["right_offset"] = args.right_offset + if args.best_quality is not None: + parameters["best_quality"] = args.best_quality + if args.limit_image_size is not None: + parameters["limit_image_size"] = args.limit_image_size + if args.add_watermark is not None: + parameters["add_watermark"] = args.add_watermark + + headers = { + "Authorization": f"Bearer {api_key}", + "X-DashScope-Async": "enable", + "Content-Type": "application/json", + } + + payload = { + "model": "image-out-painting", + "input": {"image_url": args.image_url}, + "parameters": parameters, + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{DASHSCOPE_API_BASE}/services/aigc/image2image/out-painting", + headers=headers, + json=payload, + ) as resp: + status_code = resp.status + response_json = await resp.json() + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "submit_response", + "payload": { + "request_id": request_id, + "response": response_json, + "status_code": status_code, + }, + }, + ) + + if status_code != HTTPStatus.OK or "output" not in response_json: + error_msg = response_json.get("message", "Unknown error") + raise RuntimeError( + f"Failed to submit out-painting task: {error_msg} (code: {status_code})", # noqa + ) + + output = response_json["output"] + task_id = output["task_id"] + task_status = output["task_status"] + actual_request_id = ( + response_json.get("request_id") or request_id or str(uuid.uuid4()) + ) + + return ImageOutPaintingSubmitOutput( + task_id=task_id, + task_status=task_status, + request_id=actual_request_id, + ) + + +# ==================== Fetch Result ==================== + + +class ImageOutPaintingFetchInput(BaseModel): + task_id: str = Field( + ..., + description="要查询的扩图任务 ID。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing " + "headers for mcp only, don't generate it", + ) + + +class ImageOutPaintingFetchOutput(BaseModel): + output_image_url: str = Field( + ..., + description="扩图后生成的图像公网 URL(PNG/JPG 等格式)。", + ) + task_id: str = Field( + ..., + description="任务 ID,与输入一致。", + ) + task_status: str = Field( + ..., + description="任务最终状态,成功时为 SUCCEEDED。", + ) + request_id: Optional[str] = Field( + default=None, + description="请求 ID,用于追踪。", + ) + + +class ImageOutPaintingFetch( + Component[ImageOutPaintingFetchInput, ImageOutPaintingFetchOutput], +): + name: str = "modelstudio_image_out_painting_fetch" + description: str = ( + "查询图像画面扩展(扩图)任务的结果。\n" + "输入 Task ID,返回扩图后的图像 URL 和任务状态。\n" + "请在提交任务后轮询此接口,直到状态变为 SUCCEEDED。" + ) + + @trace(trace_type="AIGC", trace_name="image_out_painting_fetch") + async def arun( + self, + args: ImageOutPaintingFetchInput, + **kwargs: Any, + ) -> ImageOutPaintingFetchOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError as e: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") from e + + headers = { + "Authorization": f"Bearer {api_key}", + } + + async with aiohttp.ClientSession() as session: + async with session.get( + f"{DASHSCOPE_API_BASE}/tasks/{args.task_id}", + headers=headers, + ) as resp: + status_code = resp.status + response_json = await resp.json() + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "fetch_response", + "payload": { + "request_id": request_id, + "response": response_json, + "status_code": status_code, + }, + }, + ) + + if status_code != HTTPStatus.OK or "output" not in response_json: + error_msg = response_json.get("message", "Unknown error") + raise RuntimeError( + f"Failed to fetch out-painting result: {error_msg} (code: {status_code})", # noqa + ) + + output = response_json["output"] + task_status = output["task_status"] + + if task_status in ["FAILED", "CANCELED"]: + error_msg = output.get("message", "Task failed") + raise RuntimeError(f"Out-painting task failed: {error_msg}") + + if task_status != "SUCCEEDED": + raise RuntimeError( + f"Task not completed yet. Current status: {task_status}", + ) + + output_image_url = output["output_image_url"] + actual_request_id = ( + response_json.get("request_id") or request_id or str(uuid.uuid4()) + ) + + return ImageOutPaintingFetchOutput( + output_image_url=output_image_url, + task_id=output["task_id"], + task_status=task_status, + request_id=actual_request_id, + ) diff --git a/src/agentscope_bricks/components/generations/async_image_to_video_fl_wan22.py b/src/agentscope_bricks/components/generations/async_image_to_video_fl_wan22.py new file mode 100644 index 0000000..12ddda3 --- /dev/null +++ b/src/agentscope_bricks/components/generations/async_image_to_video_fl_wan22.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +import os +import uuid +from http import HTTPStatus +from typing import Any, Optional + +from dashscope.aigc.video_synthesis import AioVideoSynthesis +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class ImageToVideoByFirstAndLastFrameWan22SubmitInput(BaseModel): + """ + Input model for submitting a + keyframe-to-video task using wan2.2-kf2v-flash. + """ + + first_frame_url: str = Field( + ..., + description="首帧图像,支持公网URL、Base64编码。", + ) + last_frame_url: str = Field( + ..., + description="尾帧图像,支持公网URL、Base64编码。", + ) + prompt: Optional[str] = Field( + default=None, + description="正向提示词,描述希望视频中发生的动作或变化,例如“镜头缓慢推进,风吹动树叶”。", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,用于排除不希望出现的内容,例如“模糊、闪烁、变形、水印”。", + ) + resolution: Optional[str] = Field( + default=None, + description="视频分辨率,可选值:'480P'、'720P'、'1080P'。默认为 '720P'。", + ) + template: Optional[str] = Field( + default=None, + description="不同模型支持不同的特效模板。调用前请查阅视频特效列表,以免调用失败。", + ) + prompt_extend: Optional[bool] = Field( + default=None, + description="Prompt 智能改写。开启后可提升生成效果。默认值为 true。", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否添加水印。false(默认):不添加;true:添加。", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,取值范围 [0, 2147483647]。用于提升结果可复现性,但不保证完全一致。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing " + "headers for mcp only, don't generate it", + ) + + +class ImageToVideoByFirstAndLastFrameWan22SubmitOutput(BaseModel): + """ + Output of the keyframe-to-video task submission. + """ + + task_id: str = Field( + title="Task ID", + description="异步任务的唯一标识符。", + ) + task_status: str = Field( + title="Task Status", + description="视频生成的任务状态,PENDING:任务排队中,RUNNING:任务处理中,SUCCEEDED:任务执行成功," + "FAILED:任务执行失败,CANCELED:任务取消成功,UNKNOWN:任务不存在或状态未知", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="本次请求的唯一ID,可用于日志追踪。", + ) + + +class ImageToVideoByFirstAndLastFrameWan22Submit( + Component[ + ImageToVideoByFirstAndLastFrameWan22SubmitInput, + ImageToVideoByFirstAndLastFrameWan22SubmitOutput, + ], +): + """ + Submit a keyframe-to-video generation + task using the wan2.2-kf2v-flash model. + """ + + name: str = ( + "modelstudio_image_to_video_by_first_and_last_frame_wan22_submit_task" + ) + description: str = ( + "[版本: wan2.2] 通义万相首尾帧生视频模型(wan2.2-kf2v-flash)异步任务提交工具。\n" + "基于首帧与尾帧图像及文本提示,生成一段流畅的无声视频(当前不支持音频输出)。\n" + ) + + @trace( + trace_type="AIGC", + trace_name="image_to_video_by_first_and_last_frame_wan22_submit", + ) + async def arun( + self, + args: ImageToVideoByFirstAndLastFrameWan22SubmitInput, + **kwargs: Any, + ) -> ImageToVideoByFirstAndLastFrameWan22SubmitOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + model_name = kwargs.get( + "model_name", + os.getenv("IMAGE_TO_VIDEO_KF2V_MODEL_NAME", "wan2.2-kf2v-flash"), + ) + + # 构建 parameters(全部为可选参数) + parameters = {} + if args.resolution: + parameters["resolution"] = args.resolution + if args.prompt_extend is not None: + parameters["prompt_extend"] = args.prompt_extend + if args.watermark is not None: + parameters["watermark"] = args.watermark + if args.seed is not None: + parameters["seed"] = args.seed + if args.template: + parameters["template"] = args.template + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.async_call( + model=model_name, + api_key=api_key, + first_frame_url=args.first_frame_url, + last_frame_url=args.last_frame_url, + prompt=args.prompt, + negative_prompt=args.negative_prompt, + **parameters, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "submit_task": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or response.output.task_status in ["FAILED", "CANCELED"] + ): + raise RuntimeError( + f"Failed to submit keyframe-to-video task: {response}", + ) + + if not request_id: + request_id = ( + response.request_id + if response.request_id + else str(uuid.uuid4()) + ) + + result = ImageToVideoByFirstAndLastFrameWan22SubmitOutput( + request_id=request_id, + task_id=response.output.task_id, + task_status=response.output.task_status, + ) + return result + + +# ========== Fetch 部分 ========== + + +class ImageToVideoByFirstAndLastFrameWan22FetchInput(BaseModel): + task_id: str = Field( + title="Task ID", + description="要查询的视频生成任务ID。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing " + "headers for mcp only, don't generate it", + ) + + +class ImageToVideoByFirstAndLastFrameWan22FetchOutput(BaseModel): + video_url: str = Field( + title="Video URL", + description="生成视频的公网可访问URL(MP4格式,无声)。有效期24小时,请及时下载。", + ) + task_id: str = Field( + title="Task ID", + description="任务ID,与输入一致。", + ) + task_status: str = Field( + title="Task Status", + description="任务最终状态,成功时为 SUCCEEDED。", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="请求ID,用于追踪。", + ) + + +class ImageToVideoByFirstAndLastFrameWan22Fetch( + Component[ + ImageToVideoByFirstAndLastFrameWan22FetchInput, + ImageToVideoByFirstAndLastFrameWan22FetchOutput, + ], +): + name: str = ( + "modelstudio_image_to_video_by_first_and_last_frame_wan22_fetch_result" + ) + description: str = ( + "查询通义万相 wan2.2-kf2v-flash 首尾帧生视频任务的结果。\n" + "输入 Task ID,返回生成的视频 URL 及任务状态。\n" + "请在提交任务后轮询此接口,直到任务状态变为 SUCCEEDED。\n" + "注意:video_url 有效期为 24 小时。" + ) + + @trace( + trace_type="AIGC", + trace_name="image_to_video_by_first_and_last_frame_wan22_fetch", + ) + async def arun( + self, + args: ImageToVideoByFirstAndLastFrameWan22FetchInput, + **kwargs: Any, + ) -> ImageToVideoByFirstAndLastFrameWan22FetchOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError as e: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") from e + + aio_video_synthesis = AioVideoSynthesis() + + response = await aio_video_synthesis.fetch( + api_key=api_key, + task=args.task_id, + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": response.request_id, + "fetch_result": response, + }, + }, + ) + + if ( + response.status_code != HTTPStatus.OK + or not response.output + or response.output.task_status in ["FAILED", "CANCELED"] + ): + raise RuntimeError( + f"Failed to fetch keyframe-to-video result: {response}", + ) + + request_id = response.request_id or request_id or str(uuid.uuid4()) + + return ImageToVideoByFirstAndLastFrameWan22FetchOutput( + video_url=response.output.video_url, + task_id=response.output.task_id, + task_status=response.output.task_status, + request_id=request_id, + ) diff --git a/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py b/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py index 2de8324..d1c2bdf 100644 --- a/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py +++ b/src/agentscope_bricks/components/generations/async_image_to_video_wan26.py @@ -21,7 +21,7 @@ class ImageToVideoWan26SubmitInput(BaseModel): image_url: str = Field( ..., - description="输入图像,支持公网URL、Base64编码或本地文件路径", + description="输入图像,支持公网URL、Base64编码", ) prompt: Optional[str] = Field( default=None, @@ -33,15 +33,12 @@ class ImageToVideoWan26SubmitInput(BaseModel): ) audio_url: Optional[str] = Field( default=None, - description="自定义音频文件的公网URL。参数优先级:audio_url > audio。", - ) - audio: Optional[bool] = Field( - default=None, - description="是否自动生成配音。仅在 audio_url 未提供时生效。", + description="自定义音频文件的公网URL。参数优先级:audio_url > audio。" + "若不提供audio_url ,模型将根据视频内容自动生成匹配的背景音乐或音效。", ) template: Optional[str] = Field( default=None, - description="视频特效模板,如:squish(解压捏捏)、flying(魔法悬浮)、carousel(时光木马)等。", + description="视频特效模板,如:flying,表示使用“魔法悬浮”特效等。", ) resolution: Optional[str] = Field( default=None, @@ -64,7 +61,7 @@ class ImageToVideoWan26SubmitInput(BaseModel): ) watermark: Optional[bool] = Field( default=None, - description="是否在视频中添加水印(如“AI生成”标识)。默认不添加。", + description="是否添加水印,false:默认值,不添加水印,true:添加水印。", ) seed: Optional[int] = Field( default=None, @@ -134,8 +131,6 @@ async def arun( # 构建 parameters(全部为可选参数) parameters = {} - if args.audio is not None: - parameters["audio"] = args.audio if args.resolution: parameters["resolution"] = args.resolution if args.duration is not None: diff --git a/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py b/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py index a446597..8b8bca0 100644 --- a/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py +++ b/src/agentscope_bricks/components/generations/async_text_to_video_wan26.py @@ -30,12 +30,7 @@ class TextToVideoWan26SubmitInput(BaseModel): audio_url: Optional[str] = Field( default=None, description="自定义音频文件URL,模型将使用该音频生成视频。" - "参数优先级:audio_url > audio,仅在 audio_url 为空时 audio 生效。", - ) - audio: Optional[bool] = Field( - default=None, - description="是否自动生成音频。" - "参数优先级:audio_url > audio,仅在 audio_url 为空时 audio 生效。", + "若不提供audio_url ,模型将根据视频内容自动生成匹配的背景音乐或音效。", ) size: Optional[str] = Field( default=None, @@ -57,7 +52,7 @@ class TextToVideoWan26SubmitInput(BaseModel): ) watermark: Optional[bool] = Field( default=None, - description="是否添加水印,默认不设置", + description="是否在视频中添加水印,false:默认值,不添加水印,true:添加水印。", ) seed: Optional[int] = Field( default=None, @@ -128,8 +123,6 @@ async def arun( ) parameters = {} - if args.audio is not None: - parameters["audio"] = args.audio if args.size: parameters["size"] = args.size if args.duration is not None: diff --git a/src/agentscope_bricks/components/generations/image_edit_wan26.py b/src/agentscope_bricks/components/generations/image_edit_wan26.py new file mode 100644 index 0000000..97224b8 --- /dev/null +++ b/src/agentscope_bricks/components/generations/image_edit_wan26.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +import uuid +from typing import Any, Optional +from dashscope import AioMultiModalConversation +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace, TraceType +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class ImageGenInput(BaseModel): + """ + Input schema for Wanx 2.6 image editing generation. + """ + + prompt: str = Field( + ..., + description="正向提示词,描述期望生成的图像内容", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,描述不希望出现的内容,如低质量、模糊、文字等。", + ) + size: Optional[str] = Field( + default=None, + description="输出图像的分辨率。默认值是1280*1280,可不填。", + ) + prompt_extend: Optional[bool] = Field( + default=None, + description="是否开启 Prompt 智能改写。将使用大模型优化正向提示词。true: 开启(默认),false:不开启。", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,用于结果复现。", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否添加水印,false:默认值,不添加水印,true:添加水印。", + ) + n: Optional[int] = Field( + default=4, + description="生成图片的数量。取值范围为1~4张 默认4", + ) + images: list[str] = Field( + ..., + description=( + "参考图像URL列表,用于图像编辑。\n" "必须提供至少1张参考图像。" + ), + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context for " + "MCP internal use only, do not generate it.", + ) + + +class ImageGenOutput(BaseModel): + """ + Output schema for Wanx 2.6 image generation. + """ + + results: list[str] = Field( + title="Results", + description="生成的图片URL列表。", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="本次请求的唯一标识。", + ) + + +class ImageEditWan26( + Component[ImageGenInput, ImageGenOutput], +): + """ + Wanx 2.6 Image Editing Generation Tool. + Supports: + - Image editing mode (with 1-3 reference images) + Uses the 'wan2.6-image' model from DashScope. + """ + + name: str = "modelstudio_image_edit_wan26" + description: str = ( + "[版本: wan2.6] 通义万相文生图模型(wan2.6-image)。\n" + "图像编辑,基于1~4张输入图像进行编辑、风格迁移或主体一致性生成。返回编辑后的图片URL列表。" + ) + + @trace(trace_type=TraceType.AIGC, trace_name="wanx26_image_generation") + async def arun( + self, + args: ImageGenInput, + **kwargs: Any, + ) -> ImageGenOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + model_name = "wan2.6-image" + + # 构造多模态 content:文本 + 可选图像 + content = [{"text": args.prompt}] + images = args.images or [] # 安全处理 None + for img_url in images: + content.append({"image": img_url}) + + messages = [ + { + "role": "user", + "content": content, + }, + ] + parameters = {} + if args.negative_prompt: + parameters["negative_prompt"] = args.negative_prompt + if args.size and args.size != "1280*1280": + parameters["size"] = args.size + if args.seed is not None: + parameters["seed"] = args.seed + if args.watermark is not None: + parameters["watermark"] = args.watermark + if args.prompt_extend is not None: + parameters["prompt_extend"] = args.prompt_extend + if args.n is not None and args.n != 4: + parameters["n"] = args.n + try: + response = await AioMultiModalConversation.call( + api_key=api_key, + model=model_name, + messages=messages, + enable_interleave=False, + **parameters, + ) + except Exception as e: + raise RuntimeError( + f"Failed to call Wanx 2.6 image generation API: {str(e)}", + ) from e + + if response.status_code != 200 or not response.output: + raise RuntimeError(f"Wanx 2.6 image generation failed: {response}") + + results = [] + + try: + if hasattr(response, "output") and response.output: + choices = getattr(response.output, "choices", []) + if choices: + for choice in choices: + message = getattr(choice, "message", {}) + msg_content = getattr(message, "content", []) + if isinstance(msg_content, list): + # 遍历当前 choice 的 content + for item in msg_content: + if isinstance(item, dict) and "image" in item: + results.append(item["image"]) + elif isinstance(item, str) and item.startswith( + ("http://", "https://"), + ): + results.append(item) + elif isinstance( + msg_content, + str, + ) and msg_content.startswith( + ("http://", "https://"), + ): + results.append(msg_content) + elif ( + isinstance(msg_content, dict) + and "image" in msg_content + ): + results.append(msg_content["image"]) + # --- 修改结束 --- + except Exception as e: + raise RuntimeError( + f"Failed to parse Wanx 2.6 API response: {str(e)}", + ) from e + + if not results: + raise RuntimeError(f"No image found in response: {response}") + + if not request_id: + request_id = getattr(response, "request_id", None) or str( + uuid.uuid4(), + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "wanx26_image_generation_result": { + "status_code": response.status_code, + "results": results, + }, + }, + }, + ) + + return ImageGenOutput( + results=results, + request_id=request_id, + ) diff --git a/src/agentscope_bricks/components/generations/image_generation_zimage.py b/src/agentscope_bricks/components/generations/image_generation_zimage.py new file mode 100644 index 0000000..1cbb2ef --- /dev/null +++ b/src/agentscope_bricks/components/generations/image_generation_zimage.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +import uuid +from typing import Any, Optional +from dashscope import AioMultiModalConversation +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace, TraceType +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +class ZImageGenerationInput(BaseModel): + """ + Input schema for Z-Image text-to-image generation. + """ + + prompt: str = Field( + ..., + description="正向提示词,描述期望生成的图像内容,建议详细且清晰。超过800字符将被截断。", + ) + size: Optional[str] = Field( + default="1024*1536", + description="输出图像的分辨率。默认 1024*1536", + ) + prompt_extend: Optional[bool] = Field( + default=None, + description="是否开启 Prompt 智能改写。将使用大模型优化正向提示词。true: 开启,false:不开启(默认)。", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,用于结果复现。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context for MCP " + "internal use only, do not generate it.", + ) + + +class ZImageGenerationOutput(BaseModel): + """ + Output schema for Z-Image text-to-image generation. + """ + + results: list[str] = Field( + title="Results", + description="生成的图片URL列表。", + ) + request_id: Optional[str] = Field( + default=None, + title="Request ID", + description="本次请求的唯一标识。", + ) + + +class ZImageGeneration( + Component[ZImageGenerationInput, ZImageGenerationOutput], +): + """ + Z-Image Text-to-Image Generation Tool (based on z-image-turbo). + Uses the 'z-image-turbo' model from DashScope to + generate high-quality images from text prompts. + Supports custom resolution, negative prompts, batch generation, and more. + """ + + name: str = "modelstudio_z_image_generation" + description: str = ( + " 基于通义Z-Image大模型的智能图像生成服务,是一款轻量级文生图模型," + "可快速生成图像,支持中英文字渲染,并灵活适配多种分辨率与宽高比例。" + ) + + @trace(trace_type=TraceType.AIGC, trace_name="z_image_generation") + async def arun( + self, + args: ZImageGenerationInput, + **kwargs: Any, + ) -> ZImageGenerationOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + model_name = "z-image-turbo" + messages = [ + { + "role": "user", + "content": [{"text": args.prompt}], + }, + ] + parameters = {} + if args.size and args.size != "1024*1536": + parameters["size"] = args.size + if args.seed is not None: + parameters["seed"] = args.seed + if args.prompt_extend is not None: + parameters["prompt_extend"] = args.prompt_extend + + try: + response = await AioMultiModalConversation.call( + api_key=api_key, + model=model_name, + messages=messages, + **parameters, + ) + except Exception as e: + raise RuntimeError( + f"Failed to call Z-Image (z-image-turbo) API: {str(e)}", + ) from e + + if response.status_code != 200 or not response.output: + raise RuntimeError(f"Z-Image generation failed: {response}") + results = [] + try: + choices = getattr(response.output, "choices", []) + if choices: + message = getattr(choices[0], "message", {}) + content = getattr(message, "content", []) + if isinstance(content, list): + for item in content: + if isinstance(item, dict) and "image" in item: + results.append(item["image"]) + elif isinstance(content, str): + results.append(content) + elif isinstance(content, dict) and "image" in content: + results.append(content["image"]) + except Exception as e: + raise RuntimeError( + f"Failed to parse Z-Image API response: {str(e)}", + ) from e + + if not results: + raise RuntimeError( + f"No image URLs found in Z-Image response: {response}", + ) + + if not request_id: + request_id = getattr(response, "request_id", None) or str( + uuid.uuid4(), + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "results", + "payload": { + "request_id": request_id, + "z_image_generation_result": { + "status_code": response.status_code, + "results": results, + }, + }, + }, + ) + + return ZImageGenerationOutput( + results=results, + request_id=request_id, + ) diff --git a/src/agentscope_bricks/components/generations/image_out_painting.py b/src/agentscope_bricks/components/generations/image_out_painting.py new file mode 100644 index 0000000..bc5578b --- /dev/null +++ b/src/agentscope_bricks/components/generations/image_out_painting.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +import os +import uuid +import asyncio +from http import HTTPStatus +from typing import Any, Optional, Dict + +import aiohttp +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +DASHSCOPE_API_BASE = "https://dashscope.aliyuncs.com/api/v1" + + +class ImageOutPaintingAutoInput(BaseModel): + """ + Input for auto-submit-and-fetch image out-painting task. + """ + + image_url: str = Field( + ..., + description="输入图像的公网可访问 URL。", + ) + angle: Optional[float] = Field( + default=None, + description="逆时针旋转角度,取值范围 [0, 359]。默认为 0(不旋转)。", + ) + output_ratio: Optional[str] = Field( + default=None, + description='目标宽高比,可选值:["", "1:1", "3:4", "4:3", "9:16", "16:9"]。' + '默认值为"",表示不设置输出图像的宽高比。', + ) + x_scale: Optional[float] = Field( + default=None, + description="水平方向扩展比例(居中扩展),默认 1.0。可以与 y_scale 搭配使用。取值范围 [1.0, 3.0]。" + "例如:输入图像分辨率为1000×1000(宽×高),x_scale=2.0,扩展后的图像分辨率为2000×1000(宽×高)。" + "保持高度不变,左右各添加500个像素。", + ) + y_scale: Optional[float] = Field( + default=None, + description="垂直方向扩展比例(居中扩展),默认 1.0。可以选择与 x_scale 搭配使用。取值范围 [1.0, 3.0]。" + "例如:输入图像分辨率为1000×1000(宽×高),y_scale=2.0,扩展后的图像分辨率为1000×2000(宽×高)。" + "保持宽度不变,上下各添加500个像素。", + ) + top_offset: Optional[float] = Field( + default=None, + description="在图像上方添加的像素数。默认值为0," + "需满足 top_offset + bottom_offset < 3 × 原图高度。" + "输入图像分辨率为1000×1000(宽×高),top_offset=500,扩展后的图像分辨率为1000×1500(宽×高)。" + "保持宽度不变,只在图像上方添加500个像素。", + ) + bottom_offset: Optional[float] = Field( + default=None, + description="在图像下方添加的像素数。默认值为0," + "需满足 top_offset + bottom_offset < 3 × 原图高度。" + "例如:输入图像分辨率为1000×1000(宽×高),bottom_offset=500,扩展后的图像分辨率为1000×1500(宽×高)。" + "保持宽度不变,只在图像下方添加500个像素。", + ) + left_offset: Optional[float] = Field( + default=None, + description="在图像左侧添加的像素数。默认值为0," + "需满足 left_offset + right_offset < 3 × 原图宽度。" + "例如:输入图像分辨率为1000×1000(宽×高),left_offset=500,扩展后的图像分辨率为1500×1000(宽×高)。" + "保持高度不变,只在图像左侧添加500个像素。", + ) + right_offset: Optional[float] = Field( + default=None, + description="在图像右侧添加的像素数。默认值为0," + "需满足 left_offset + right_offset < 3 × 原图宽度。" + "例如:输入图像分辨率为1000×1000(宽×高),right_offset=500,扩展后的图像分辨率为1500×1000(宽×高)。" + "保持高度不变,只在图像右侧添加500个像素。", + ) + best_quality: Optional[bool] = Field( + default=None, + description="是否启用最佳质量模式。默认 false(速度优先),设为 true 可提升细节但耗时增加。", + ) + limit_image_size: Optional[bool] = Field( + default=None, + description="是否限制输出图像大小(≤5MB)。默认 true,建议保持开启。" + "模型生成的图像需要经过一层安全过滤后才能输出,当前不支持大于10M的图像处理。", + ) + add_watermark: Optional[bool] = Field( + default=None, + description="是否添加水印,True:默认值,添加水印,False:不添加水印。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing " + "headers for mcp only, don't generate it", + ) + + +class ImageOutPaintingAutoOutput(BaseModel): + output_image_url: str = Field( + ..., + description="扩图后生成的图像公网 URL(PNG/JPG 等格式),有效期 24 小时。", + ) + task_id: str = Field( + ..., + description="异步任务的唯一标识符。", + ) + request_id: Optional[str] = Field( + default=None, + description="请求 ID,用于日志追踪。", + ) + + +class ImageOutPaintingAuto( + Component[ImageOutPaintingAutoInput, ImageOutPaintingAutoOutput], +): + name: str = "modelstudio_image_out_painting_auto" + description: str = ( + "图像画面扩展(扩图)同步自动执行工具。\n" + "提交扩图任务并内部轮询结果,直接返回扩图后的图像 URL。\n" + "无需手动查询任务状态,适合需要端到端结果的场景。" + ) + + @trace(trace_type="AIGC", trace_name="image_out_painting_auto") + async def arun( + self, + args: ImageOutPaintingAutoInput, + **kwargs: Any, + ) -> ImageOutPaintingAutoOutput: + trace_event = kwargs.pop("trace_event", None) + request_id = TracingUtil.get_request_id() + + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + # 构建 parameters(仅非 None 值) + parameters: Dict[str, Any] = {} + for field in [ + "angle", + "output_ratio", + "x_scale", + "y_scale", + "top_offset", + "bottom_offset", + "left_offset", + "right_offset", + "best_quality", + "limit_image_size", + "add_watermark", + ]: + value = getattr(args, field) + if value is not None: + parameters[field] = value + + headers = { + "Authorization": f"Bearer {api_key}", + "X-DashScope-Async": "enable", + "Content-Type": "application/json", + } + + payload = { + "model": "image-out-painting", + "input": {"image_url": args.image_url}, + "parameters": parameters, + } + + # Step 1: Submit task + async with aiohttp.ClientSession() as session: + async with session.post( + f"{DASHSCOPE_API_BASE}/services/aigc/image2image/out-painting", + headers=headers, + json=payload, + ) as resp: + status_code = resp.status + response_json = await resp.json() + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "submit", + "payload": { + "request_id": request_id, + "response": response_json, + "status_code": status_code, + }, + }, + ) + + if status_code != HTTPStatus.OK or "output" not in response_json: + error_msg = response_json.get("message", "Unknown error") + raise RuntimeError( + f"Failed to submit out-painting task: {error_msg} (code: {status_code})", # noqa + ) + + task_id = response_json["output"]["task_id"] + request_id = ( + response_json.get("request_id") or request_id or str(uuid.uuid4()) + ) + + # Step 2: Poll until completion + max_retries = 60 # 最多等待 2 分钟(60 * 2s) + retry_interval = 2 # 每 2 秒查询一次 + + fetch_headers = {"Authorization": f"Bearer {api_key}"} + + for attempt in range(max_retries): + await asyncio.sleep(retry_interval) + + async with aiohttp.ClientSession() as session: + async with session.get( + f"{DASHSCOPE_API_BASE}/tasks/{task_id}", + headers=fetch_headers, + ) as resp: + fetch_status = resp.status + fetch_response = await resp.json() + + if fetch_status != HTTPStatus.OK or "output" not in fetch_response: + error_msg = fetch_response.get( + "message", + "Unknown fetch error", + ) + raise RuntimeError( + f"Failed to poll task: {error_msg} (code: {fetch_status})", + ) + + output = fetch_response["output"] + task_status = output["task_status"] + + if task_status == "SUCCEEDED": + output_image_url = output["output_image_url"] + final_request_id = ( + fetch_response.get("request_id") or request_id + ) + + if trace_event: + trace_event.on_log( + "", + **{ + "step_suffix": "success", + "payload": { + "output_image_url": output_image_url, + "request_id": final_request_id, + }, + }, + ) + + return ImageOutPaintingAutoOutput( + output_image_url=output_image_url, + task_id=task_id, + request_id=final_request_id, + ) + + elif task_status in ("FAILED", "CANCELED"): + error_msg = output.get( + "message", + "Task failed without details", + ) + raise RuntimeError( + f"Out-painting task failed: {error_msg} (task_id: {task_id})", # noqa + ) + + # else: PENDING / RUNNING → continue polling + + # Timeout + raise TimeoutError( + f"Out-painting task did not complete within {max_retries * retry_interval} seconds " # noqa + f"(task_id: {task_id}). Current status may still be PENDING/RUNNING.", # noqa + ) diff --git a/src/agentscope_bricks/components/generations/image_text_interleave_generation_wan26.py b/src/agentscope_bricks/components/generations/image_text_interleave_generation_wan26.py new file mode 100644 index 0000000..e2e9d0b --- /dev/null +++ b/src/agentscope_bricks/components/generations/image_text_interleave_generation_wan26.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +import uuid +import json +from http import HTTPStatus +from typing import Any, Optional, Dict, AsyncGenerator +import aiohttp +from mcp.server.fastmcp import Context +from pydantic import BaseModel, Field + +from agentscope_bricks.base.component import Component +from agentscope_bricks.utils.tracing_utils.wrapper import trace +from agentscope_bricks.utils.api_key_util import ApiNames, get_api_key +from agentscope_bricks.utils.tracing_utils import TracingUtil + + +DASHSCOPE_API_BASE = "https://dashscope.aliyuncs.com/api/v1" + + +class WanImageInterleaveGenerationInput(BaseModel): + """ + Input model for Alibaba Cloud + Wan 2.6 Image Interleaved (Text + Image) Generation. + """ + + prompt: str = Field( + ..., + description="用户输入的文本指令,例如 '给我一个3张图辣椒炒肉教程'。", + ) + negative_prompt: Optional[str] = Field( + default=None, + description="反向提示词,描述不希望出现的内容,如低质量、模糊、文字等。", + ) + image: Optional[str] = Field( + default=None, + description="可选的参考图像 URL,图片和prompt要有关系,否则会被忽略。", + ) + max_images: Optional[int] = Field( + default=5, + description="期望生成的最大图像数量取值范围:1~5,默认值为 5,该参数仅代表“数量上限”。" + "实际生成的图像数量由模型推理决定,可能会少于设定值。", + ) + size: Optional[str] = Field( + default="1280*1280", + description="输出图像的分辨率。默认值是1280*1280,可不填。", + ) + watermark: Optional[bool] = Field( + default=None, + description="是否添加水印,false:默认值,不添加水印,true:添加水印。", + ) + seed: Optional[int] = Field( + default=None, + description="随机种子,用于结果可复现。", + ) + ctx: Optional[Context] = Field( + default=None, + description="HTTP request context containing " + "headers for mcp only, don't generate it", + ) + + +class WanImageInterleaveGenerationOutput(BaseModel): + full_text: str = Field( + ..., + description="模型生成的完整文本内容(不含图片占位符)。", + ) + image_urls: list[str] = Field( + ..., + description="按顺序生成的图像公网 URL 列表。", + ) + request_id: Optional[str] = Field( + default=None, + description="请求唯一 ID,用于日志追踪。", + ) + + +class WanImageInterleaveGeneration( + Component[ + WanImageInterleaveGenerationInput, + WanImageInterleaveGenerationOutput, + ], +): + name: str = "modelstudio_wan_text_image_interleave_generation" + description: str = ( + "[版本: wan2.6] 通义万相图文混排生成工具(wan2.6-image),支持文本+图像混合生成。\n" + "支持传入最多1张参考图用于风格/背景引导。" + ) + + @trace( + trace_type="AIGC", + trace_name="wan_image_interleave_generation_stream", + ) + async def astream( + self, + args: WanImageInterleaveGenerationInput, + **kwargs: Any, + ) -> AsyncGenerator[Dict[str, Any], None]: + try: + api_key = get_api_key(ApiNames.dashscope_api_key, **kwargs) + except AssertionError: + raise ValueError("Please set valid DASHSCOPE_API_KEY!") + + content: list[Dict[str, str]] = [{"text": args.prompt}] + if args.image: + content.append({"image": args.image}) + parameters = { + "enable_interleave": True, # 必须为 true + "stream": True, # 启用流式 + "max_images": args.max_images, + "size": args.size, + "watermark": args.watermark, + } + + # 可选参数:仅当非 None 时传入 + if args.negative_prompt is not None: + parameters["negative_prompt"] = args.negative_prompt + if args.seed is not None: + parameters["seed"] = args.seed + + payload = { + "model": "wan2.6-image", + "input": { + "messages": [ + { + "role": "user", + "content": content, + }, + ], + }, + "parameters": parameters, + } + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-DashScope-Sse": "enable", + } + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{DASHSCOPE_API_BASE}/services/aigc/multimodal-generation/generation", # noqa + headers=headers, + json=payload, + ) as resp: + if resp.status != HTTPStatus.OK: + error_text = await resp.text() + raise RuntimeError(f"SSE request failed: {error_text}") + + async for line_bytes in resp.content: + line = line_bytes.decode("utf-8").strip() + if not line or not line.startswith("data:"): + continue + + data_str = line[5:].strip() + if data_str == "[DONE]": + break + + try: + chunk = json.loads(data_str) + contents = chunk["output"]["choices"][0]["message"][ + "content" + ] + for item in contents: + if item.get("type") == "text": + yield {"type": "text", "value": item["text"]} + elif item.get("type") == "image": + img_url = item.get("image") + if isinstance(img_url, str): + yield {"type": "image", "value": img_url} + except ( + KeyError, + IndexError, + TypeError, + json.JSONDecodeError, + ): + continue + + @trace(trace_type="AIGC", trace_name="wan_image_interleave_generation") + async def arun( + self, + args: WanImageInterleaveGenerationInput, + **kwargs: Any, + ) -> WanImageInterleaveGenerationOutput: + full_text = "" + image_urls: list[str] = [] + request_id = TracingUtil.get_request_id() or str(uuid.uuid4()) + + # 复用 astream 逻辑来聚合结果(避免重复代码) + async for chunk in self.astream(args, **kwargs): + if chunk["type"] == "text": + full_text += chunk["value"] + elif chunk["type"] == "image": + image_urls.append(chunk["value"]) + + return WanImageInterleaveGenerationOutput( + full_text=full_text, + image_urls=image_urls, + request_id=request_id, + ) From 5dc1654be5c95f5e0110145fae4211e820a5951d Mon Sep 17 00:00:00 2001 From: lether <769451199@qq.com> Date: Tue, 30 Dec 2025 13:38:49 +0800 Subject: [PATCH 12/12] update --- .../components/generations/image_generation_wan26.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agentscope_bricks/components/generations/image_generation_wan26.py b/src/agentscope_bricks/components/generations/image_generation_wan26.py index 21def3c..e68f767 100644 --- a/src/agentscope_bricks/components/generations/image_generation_wan26.py +++ b/src/agentscope_bricks/components/generations/image_generation_wan26.py @@ -42,7 +42,7 @@ class ImageGenerationWan26Input(BaseModel): ) watermark: Optional[bool] = Field( default=None, - description="是否添加阿里云水印,默认不添加。", + description="是否添加水印,false:默认值,不添加水印,true:添加水印。", ) ctx: Optional[Context] = Field( default=None,