Browse Source

full commit

Y 1 month ago
parent
commit
d0ea2c4b79
100 changed files with 7163 additions and 0 deletions
  1. 249 0
      any_script.py
  2. 0 0
      api_modules/__init__.py
  3. BIN
      api_modules/__pycache__/__init__.cpython-310.pyc
  4. BIN
      api_modules/__pycache__/ark_client.cpython-310.pyc
  5. BIN
      api_modules/__pycache__/ark_client_async.cpython-310.pyc
  6. BIN
      api_modules/__pycache__/ark_client_async.cpython-312.pyc
  7. BIN
      api_modules/__pycache__/ark_image_client.cpython-310.pyc
  8. BIN
      api_modules/__pycache__/ark_image_client_async.cpython-310.pyc
  9. BIN
      api_modules/__pycache__/ark_video_client.cpython-310.pyc
  10. BIN
      api_modules/__pycache__/ark_video_client_async.cpython-310.pyc
  11. BIN
      api_modules/__pycache__/base_client.cpython-310.pyc
  12. BIN
      api_modules/__pycache__/base_client_async.cpython-310.pyc
  13. BIN
      api_modules/__pycache__/base_client_async.cpython-312.pyc
  14. BIN
      api_modules/__pycache__/example.cpython-310.pyc
  15. 368 0
      api_modules/ark_client.py
  16. 238 0
      api_modules/ark_client_async.py
  17. 263 0
      api_modules/ark_image_client.py
  18. 512 0
      api_modules/ark_image_client_async.py
  19. 701 0
      api_modules/ark_video_client.py
  20. 512 0
      api_modules/ark_video_client_async.py
  21. 268 0
      api_modules/base_client.py
  22. 321 0
      api_modules/base_client_async.py
  23. 294 0
      api_modules/example.py
  24. 4 0
      config/refer_images.json
  25. 38 0
      config/taskflow_config.example.json
  26. 3 0
      data.toon
  27. BIN
      data/image/006.jpg
  28. BIN
      data/image/010.jpg
  29. BIN
      data/image/cloth.jpg
  30. BIN
      data/image/cloth1.jpg
  31. BIN
      data/image/cloth2.jpg
  32. BIN
      data/image/cloth3.jpg
  33. BIN
      data/image/cloth4.jpg
  34. BIN
      data/image/face.jpg
  35. BIN
      data/image/yi1.png
  36. BIN
      data/image/yi2.png
  37. BIN
      data/image/yi3.png
  38. 10 0
      data/input/sample_text.txt
  39. BIN
      data/video/video1.mp4
  40. BIN
      data/video/video2.mp4
  41. BIN
      data/video/video3.mp4
  42. 0 0
      examples/__init__.py
  43. BIN
      examples/__pycache__/__init__.cpython-310.pyc
  44. BIN
      examples/__pycache__/__init__.cpython-312.pyc
  45. 0 0
      examples/refer_video_create/__init__.py
  46. BIN
      examples/refer_video_create/__pycache__/__init__.cpython-310.pyc
  47. BIN
      examples/refer_video_create/__pycache__/__init__.cpython-312.pyc
  48. BIN
      examples/refer_video_create/__pycache__/main.cpython-310.pyc
  49. 290 0
      examples/refer_video_create/main.py
  50. BIN
      examples/refer_video_create/mcps/__pycache__/script_check.cpython-310.pyc
  51. BIN
      examples/refer_video_create/mcps/__pycache__/script_check.cpython-312.pyc
  52. BIN
      examples/refer_video_create/mcps/__pycache__/script_create.cpython-310.pyc
  53. BIN
      examples/refer_video_create/mcps/__pycache__/script_optimate.cpython-310.pyc
  54. 152 0
      examples/refer_video_create/mcps/script_check.py
  55. 81 0
      examples/refer_video_create/mcps/script_create.py
  56. 157 0
      examples/refer_video_create/mcps/script_optimate.py
  57. 3 0
      examples/refer_video_create/pipeline/__init__.py
  58. BIN
      examples/refer_video_create/pipeline/__pycache__/__init__.cpython-310.pyc
  59. BIN
      examples/refer_video_create/pipeline/__pycache__/refer_video_create_pipeline.cpython-310.pyc
  60. 413 0
      examples/refer_video_create/pipeline/refer_video_create_pipeline.py
  61. 5 0
      examples/text_analysis/__init__.py
  62. BIN
      examples/text_analysis/__pycache__/__init__.cpython-310.pyc
  63. BIN
      examples/text_analysis/__pycache__/main.cpython-310.pyc
  64. BIN
      examples/text_analysis/__pycache__/processors.cpython-310.pyc
  65. BIN
      examples/text_analysis/__pycache__/steps.cpython-310.pyc
  66. 137 0
      examples/text_analysis/main.py
  67. 141 0
      examples/text_analysis/processors.py
  68. 118 0
      examples/text_analysis/steps.py
  69. 0 0
      examples/video_create/__init__.py
  70. BIN
      examples/video_create/__pycache__/__init__.cpython-310.pyc
  71. BIN
      examples/video_create/__pycache__/__init__.cpython-312.pyc
  72. BIN
      examples/video_create/__pycache__/main.cpython-310.pyc
  73. BIN
      examples/video_create/__pycache__/main.cpython-312.pyc
  74. 378 0
      examples/video_create/main.py
  75. 3 0
      examples/video_create/mcps/__init__.py
  76. BIN
      examples/video_create/mcps/__pycache__/__init__.cpython-310.pyc
  77. BIN
      examples/video_create/mcps/__pycache__/__init__.cpython-312.pyc
  78. BIN
      examples/video_create/mcps/__pycache__/camera_tree.cpython-310.pyc
  79. BIN
      examples/video_create/mcps/__pycache__/character_extract.cpython-310.pyc
  80. BIN
      examples/video_create/mcps/__pycache__/character_portrait.cpython-310.pyc
  81. BIN
      examples/video_create/mcps/__pycache__/concat_clip.cpython-310.pyc
  82. BIN
      examples/video_create/mcps/__pycache__/refer_image.cpython-310.pyc
  83. BIN
      examples/video_create/mcps/__pycache__/story_create.cpython-310.pyc
  84. BIN
      examples/video_create/mcps/__pycache__/story_create.cpython-312.pyc
  85. BIN
      examples/video_create/mcps/__pycache__/storyboard_create.cpython-310.pyc
  86. 131 0
      examples/video_create/mcps/camera_tree.py
  87. 94 0
      examples/video_create/mcps/character_extract.py
  88. 69 0
      examples/video_create/mcps/character_portrait.py
  89. 40 0
      examples/video_create/mcps/concat_clip.py
  90. 119 0
      examples/video_create/mcps/refer_image.py
  91. 184 0
      examples/video_create/mcps/story_create.py
  92. 391 0
      examples/video_create/mcps/storyboard_create.py
  93. 3 0
      examples/video_create/pipeline/__init__.py
  94. BIN
      examples/video_create/pipeline/__pycache__/__init__.cpython-310.pyc
  95. BIN
      examples/video_create/pipeline/__pycache__/__init__.cpython-312.pyc
  96. BIN
      examples/video_create/pipeline/__pycache__/idea2video_pipeline.cpython-310.pyc
  97. BIN
      examples/video_create/pipeline/__pycache__/idea2video_pipeline.cpython-312.pyc
  98. 473 0
      examples/video_create/pipeline/idea2video_pipeline.py
  99. 0 0
      examples/video_create/utils/__init__.py
  100. BIN
      examples/video_create/utils/__pycache__/__init__.cpython-310.pyc

+ 249 - 0
any_script.py

@@ -0,0 +1,249 @@
+import os
+import time
+import threading
+from pathlib import Path
+from typing import Optional, List, Dict, Any, Callable, Union
+from tqdm import tqdm
+from dataclasses import dataclass, field
+
+from modules.media_understanding.media_captioner import media_captioner
+from modules.media_process.media_processor import media_processor
+from modules.media_generate.media_generator import (
+    video_create,
+    handle_video_result
+)
+from utils.tools import (
+    read_json_file,
+    save_json_file,
+)
+
+from utils.logger_config import setup_logger
+
+logger = setup_logger(__name__)
+
+@dataclass
+class VideoGenerationConfig:
+    """视频生成配置类"""
+    # 路径配置
+    output_base_dir: str = "./output"
+    
+    # 视频生成参数
+    video_resolution: str = "1080p"
+    video_ratio: str = "16:9"
+    watermark_enabled: bool = False
+    crop_frame: bool = False
+    
+    # 超时配置
+    video_generation_timeout: int = 3600  # 秒
+    polling_interval: int = 5  # 秒
+    
+    # 脚本生成参数
+    script_prompt_type: str = "script"
+    script_scenario: str = "video"
+    prompt_optimization_prefix: str = "待打磨优化的提示词:"
+
+class VideoClipGenerator:
+    """视频片段生成器"""
+    
+    def __init__(self, config: VideoGenerationConfig):
+        self.config = config
+        self._completed_tasks = 0
+        self._lock = threading.Lock()
+    
+    def generate(self, video_script_data: dict) -> bool:
+        """
+        生成所有视频片段
+        
+        Args:
+            script_path: 脚本文件路径
+            
+        Returns:
+            是否全部成功完成
+            
+        Raises:
+            FileNotFoundError: 当脚本文件不存在时
+            ValueError: 当脚本数据无效时
+        """
+        # if not os.path.exists(script_path):
+        #     raise FileNotFoundError(f"脚本文件不存在: {script_path}")
+        
+        # # 读取脚本
+        # video_script_data = read_json_file(script_path)
+        # if not video_script_data:
+        #     raise ValueError("无法读取脚本文件或文件为空")
+        
+        lens_details = []
+        storyboards = video_script_data.get("storyboards", [])
+        for storyboard in storyboards:
+            item_info = storyboard.get("storyboard", [])
+            lens_details.append(item_info)
+
+        lens_details = [item for sublist in lens_details for item in sublist]
+
+        if not storyboards:
+            raise ValueError("脚本中未找到分镜详情")
+        
+        total_tasks = len(lens_details)
+        self._completed_tasks = 0
+        
+        logger.info(f"开始生成 {total_tasks} 个视频片段")
+        
+        # 创建所有视频生成任务
+        for idx, lens_item in enumerate(tqdm(lens_details, desc="提交视频任务")):
+            self._create_video_task(lens_item, idx, total_tasks)
+        
+        # 保存脚本(包含任务ID)
+        # save_json_file(video_script_data, script_path)
+        
+        # 等待所有任务完成
+        return video_script_data, self._wait_for_completion(total_tasks)
+    
+    def _create_video_task(
+        self,
+        lens_item: Dict[str, Any],
+        task_index: int,
+        total_tasks: int
+    ) -> None:
+        """
+        创建单个视频生成任务
+        
+        Args:
+            lens_item: 分镜详情字典
+            script_path: 脚本文件路径
+            task_index: 任务索引
+            total_tasks: 总任务数
+        """
+        lens_id = lens_item.get("idx")
+        motion_prompt = lens_item.get("motion_desc")
+        image_url = lens_item.get("ff_path")
+        lens_duration = 4
+
+        if not all([motion_prompt, image_url, lens_duration]):
+            logger.warning(f"分镜 {lens_id} 缺少必要信息,跳过")
+            return
+        
+        try:
+            # 构建生成参数
+            gen_params = self._build_gen_params(lens_duration)
+            video_filename = os.path.basename(lens_item["ff_path"]).replace(".png", ".mp4")
+            logger.info(f"正在生成视频片段 {lens_id}: {video_filename}")
+
+            # 创建完成事件
+            completion_event = threading.Event()
+            
+            # 包装回调函数
+            wrapped_callback = self._create_callback_wrapper(
+                handle_video_result,
+                completion_event,
+                task_index,
+                total_tasks
+            )
+            
+            # 提交异步任务
+            task_id = video_create.create_video_task_async(
+                prompt=motion_prompt,
+                image_url=image_url,
+                gen_params=gen_params,
+                filename=video_filename,
+                callback=wrapped_callback
+            )
+            
+            if task_id:
+                lens_item["clip_path"] = f"./output/{video_filename}"
+                lens_item["task_id"] = task_id
+                logger.info(f"视频任务 {task_index + 1}/{total_tasks} 已提交: {task_id}")
+            else:
+                logger.error(f"视频任务 {task_index + 1}/{total_tasks} 提交失败")
+                
+        except Exception as e:
+            logger.error(f"创建视频任务时出错: {e}")
+    
+    def _build_gen_params(self, duration: float) -> str:
+        """
+        构建视频生成参数字符串
+        
+        Args:
+            duration: 视频时长
+            
+        Returns:
+            参数字符串
+        """
+        return (
+            f"--rs {self.config.video_resolution} "
+            f"--rt {self.config.video_ratio} "
+            f"--dur {duration} "
+            f"--wm {'true' if self.config.watermark_enabled else 'false'} "
+            f"--cf {'true' if self.config.crop_frame else 'false'}"
+        )
+    
+    def _create_callback_wrapper(
+        self,
+        original_callback: Callable,
+        event: threading.Event,
+        task_index: int,
+        total_tasks: int
+    ) -> Callable:
+        """
+        创建回调函数包装器
+        
+        Args:
+            original_callback: 原始回调函数
+            event: 完成事件
+            task_index: 任务索引
+            total_tasks: 总任务数
+            
+        Returns:
+            包装后的回调函数
+        """
+        def wrapper(*args, **kwargs):
+            try:
+                # 调用原始回调
+                if original_callback:
+                    original_callback(*args, **kwargs)
+            except Exception as e:
+                logger.error(f"回调函数执行出错: {e}")
+            finally:
+                # 标记任务完成
+                event.set()
+                with self._lock:
+                    self._completed_tasks += 1
+                    logger.info(f"视频任务 {task_index + 1}/{total_tasks} 完成 "
+                              f"({self._completed_tasks}/{total_tasks})")
+        
+        return wrapper
+    
+    def _wait_for_completion(self, total_tasks: int) -> bool:
+        """
+        等待所有任务完成
+        
+        Args:
+            total_tasks: 总任务数
+            
+        Returns:
+            是否全部成功完成
+        """
+        logger.info(f"等待 {total_tasks} 个视频任务完成...")
+        
+        start_time = time.time()
+        timeout = self.config.video_generation_timeout
+        
+        while self._completed_tasks < total_tasks:
+            elapsed = time.time() - start_time
+            
+            if elapsed > timeout:
+                logger.error(f"视频生成超时({timeout}秒),"
+                           f"已完成 {self._completed_tasks}/{total_tasks} 个任务")
+                return False
+            
+            remaining = total_tasks - self._completed_tasks
+            if remaining > 0:
+                logger.info(f"等待中... 剩余任务: {remaining}, "
+                          f"已耗时: {int(elapsed)}秒")
+            
+            time.sleep(self.config.polling_interval)
+        
+        logger.info("所有视频生成任务已完成")
+        return True
+
+
+video_generator = VideoClipGenerator(VideoGenerationConfig())

+ 0 - 0
api_modules/__init__.py


BIN
api_modules/__pycache__/__init__.cpython-310.pyc


BIN
api_modules/__pycache__/ark_client.cpython-310.pyc


BIN
api_modules/__pycache__/ark_client_async.cpython-310.pyc


BIN
api_modules/__pycache__/ark_client_async.cpython-312.pyc


BIN
api_modules/__pycache__/ark_image_client.cpython-310.pyc


BIN
api_modules/__pycache__/ark_image_client_async.cpython-310.pyc


BIN
api_modules/__pycache__/ark_video_client.cpython-310.pyc


BIN
api_modules/__pycache__/ark_video_client_async.cpython-310.pyc


BIN
api_modules/__pycache__/base_client.cpython-310.pyc


BIN
api_modules/__pycache__/base_client_async.cpython-310.pyc


BIN
api_modules/__pycache__/base_client_async.cpython-312.pyc


BIN
api_modules/__pycache__/example.cpython-310.pyc


+ 368 - 0
api_modules/ark_client.py

@@ -0,0 +1,368 @@
+"""
+火山引擎ARK API客户端
+封装ARK API的调用,提供类型安全的接口
+"""
+
+import io
+import os
+import base64
+import asyncio
+from PIL import Image
+from typing import List, Optional, Dict, Any, Union
+from dataclasses import dataclass, asdict
+from enum import Enum
+
+from .base_client import APIClient, APIError
+from taskflow.logger import get_logger
+from taskflow.config import get_config
+
+logger = get_logger("api_modules.ark_client")
+
+
+class ContentType(str, Enum):
+    """内容类型枚举"""
+    INPUT_TEXT = "input_text"
+    INPUT_IMAGE = "input_image"
+    INPUT_VIDEO = "input_video"
+    OUTPUT_TEXT = "output_text"
+    OUTPUT_IMAGE = "output_image"
+
+
+@dataclass
+class ArkTextContent:
+    """ARK文本内容"""
+    type: str = ContentType.INPUT_TEXT
+    text: str = ""
+    
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {"type": self.type, "text": self.text}
+
+
+@dataclass
+class ArkImageContent:
+    """ARK图片内容"""
+    type: str = ContentType.INPUT_IMAGE
+    image_url: str = ""
+    
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {"type": self.type, "image_url": self.image_url}
+
+@dataclass
+class ArkVideoContent:
+    """ARK视频内容"""
+    type: str = ContentType.INPUT_VIDEO
+    video_url: str = ""
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return {"type": self.type, "video_url": self.video_url}
+
+
+
+@dataclass
+class ArkMessage:
+    """ARK消息"""
+    role: str = "user"
+    content: List[Union[ArkTextContent, ArkImageContent, Dict[str, Any]]] = None
+    
+    def __post_init__(self):
+        """初始化后处理"""
+        if self.content is None:
+            self.content = []
+    
+    def add_text(self, text: str):
+        """添加文本内容"""
+        self.content.append(ArkTextContent(text=text))
+    
+    def add_image(self, image_url: str):
+        """添加图片内容"""
+        if "http" not in image_url:
+            image_base64 = self._encode_image(image_url)
+            image_url = f"data:image/jpeg;base64,{image_base64}"
+        self.content.append(ArkImageContent(image_url=image_url))
+
+    def add_video(self, video_url: str):
+        """添加视频内容"""
+        if "http" not in video_url:
+            video_base64 = self._encode_video(video_url)
+            video_url = f"data:video/mp4;base64,{video_base64}"
+        self.content.append(ArkVideoContent(video_url=video_url))
+    
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        content_list = []
+        for item in self.content:
+            if isinstance(item, (ArkTextContent, ArkImageContent, ArkVideoContent)):
+                content_list.append(item.to_dict())
+            else:
+                content_list.append(item)
+        
+        return {
+            "role": self.role,
+            "content": content_list
+        }
+
+    def _encode_video(self, video_path: str) -> str:
+        """
+        将视频文件转换为base64编码
+        
+        Args:
+            video_path: 视频文件路径
+            
+        Returns:
+            str: base64编码的视频数据
+            
+        Raises:
+            FileNotFoundError: 视频文件不存在
+            IOError: 读取文件失败
+        """
+        if not os.path.exists(video_path):
+            raise FileNotFoundError(f"Video file not found: {video_path}")
+            
+        with open(video_path, "rb") as f:
+            return base64.b64encode(f.read()).decode("utf-8")
+
+    def _encode_image(self, image_path: str) -> str:
+        """
+        将图片文件转换为base64编码
+        
+        Args:
+            image_path: 图片文件路径
+            
+        Returns:
+            str: base64编码的图片数据
+            
+        Raises:
+            FileNotFoundError: 图片文件不存在
+            IOError: 读取或处理图片失败
+        """
+        if not os.path.exists(image_path):
+            raise FileNotFoundError(f"Image file not found: {image_path}")
+            
+        with Image.open(image_path) as img:
+            buffered = io.BytesIO()
+            img.save(buffered, format="JPEG")
+            return base64.b64encode(buffered.getvalue()).decode("utf-8")
+
+
+class ArkClient(APIClient):
+    """
+    火山引擎ARK API客户端
+    
+    封装ARK API的调用,提供便捷的接口
+    """
+    
+    DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
+    DEFAULT_ENDPOINT = "/api/v3/responses"
+    DEFAULT_MODEL = "doubao-seed-1-6-251015"
+    
+    def __init__(
+        self,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        model: Optional[str] = None,
+        timeout: int = 300,
+        **kwargs
+    ):
+        """
+        初始化ARK API客户端
+        
+        Args:
+            api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
+            base_url: API基础URL(默认使用官方URL)
+            model: 模型名称(如果为None,会尝试从配置中获取)
+            timeout: 请求超时时间(秒,默认300秒,因为AI模型可能需要较长时间)
+            **kwargs: 传递给APIClient的其他参数
+        """
+        # 获取API密钥(优先级:参数 > 环境变量 > 配置)
+        if api_key is None:
+            api_key = os.getenv("ARK_API_KEY")
+            if api_key is None:
+                config = get_config()
+                api_key = config.get("api.ark.api_key")
+        
+        if not api_key:
+            raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
+        
+        # 获取base_url(优先级:参数 > 配置 > 默认值)
+        if base_url is None:
+            config = get_config()
+            base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
+        
+        # 获取model(优先级:参数 > 配置 > 默认值)
+        if model is None:
+            config = get_config()
+            model = config.get("api.ark.model", self.DEFAULT_MODEL)
+        
+        super().__init__(
+            base_url=base_url,
+            api_key=api_key,
+            timeout=timeout,
+            **kwargs
+        )
+        
+        # 保存模型名称
+        self.model = model
+        
+        logger.info(f"ARK API客户端初始化完成,模型: {self.model}")
+    
+    def chat(
+        self,
+        model: Optional[str] = None,
+        messages: List[Union[ArkMessage, Dict[str, Any]]] = None,
+        system_prompt: Optional[Union[str, ArkMessage, Dict[str, Any]]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        发送聊天请求
+        
+        Args:
+            model: 模型名称(如果为None,使用初始化时设置的模型或DEFAULT_MODEL)
+            messages: 消息列表(ArkMessage对象或字典)
+            system_prompt: 系统提示词,可以是字符串、ArkMessage对象或字典。
+                          如果提供,会自动作为第一条消息(role="system")添加到消息列表前。
+                          如果messages中已存在role="system"的消息,则不会重复添加。
+            **kwargs: 其他请求参数
+        
+        Returns:
+            API响应数据
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        # 如果没有提供model参数,使用实例变量中的model
+        if model is None:
+            model = getattr(self, 'model', self.DEFAULT_MODEL)
+        
+        # 转换消息格式
+        input_messages = []
+        
+        # 检查是否已有system角色的消息
+        has_system_message = False
+        for msg in messages:
+            if isinstance(msg, ArkMessage):
+                if msg.role == "system":
+                    has_system_message = True
+            elif isinstance(msg, dict):
+                if msg.get("role") == "system":
+                    has_system_message = True
+        
+        # 处理系统提示词
+        if system_prompt is not None and not has_system_message:
+            if isinstance(system_prompt, str):
+                # 字符串格式,转换为ArkMessage
+                system_msg = ArkMessage(role="system")
+                system_msg.add_text(system_prompt)
+                input_messages.append(system_msg.to_dict())
+            elif isinstance(system_prompt, ArkMessage):
+                # 确保role为system
+                system_prompt.role = "system"
+                input_messages.append(system_prompt.to_dict())
+            elif isinstance(system_prompt, dict):
+                # 字典格式,确保role为system
+                system_prompt = system_prompt.copy()
+                system_prompt["role"] = "system"
+                input_messages.append(system_prompt)
+            else:
+                raise ValueError(f"不支持的系统提示词类型: {type(system_prompt)}")
+        
+        # 添加其他消息
+        for msg in messages:
+            if isinstance(msg, ArkMessage):
+                input_messages.append(msg.to_dict())
+            else:
+                input_messages.append(msg)
+        
+        # 构建请求体
+        request_data = {
+            "model": model,
+            "input": input_messages,
+            **kwargs
+        }
+        
+        logger.info(f"发送聊天请求,模型: {model}, 消息数: {len(input_messages)}")
+        
+        try:
+            response = self.post(
+                endpoint=self.DEFAULT_ENDPOINT,
+                json=request_data
+            )
+            
+            logger.info("聊天请求成功")
+            return response
+            
+        except APIError as e:
+            logger.error(f"聊天请求失败: {e}")
+            raise
+    
+    def chat_simple(
+        self,
+        model: Optional[str] = None,
+        text: str = None,
+        image_url: Optional[str] = None,
+        system_prompt: Optional[Union[str, ArkMessage, Dict[str, Any]]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        简化的聊天接口(仅文本或文本+图片)
+        
+        Args:
+            model: 模型名称(如果为None,使用初始化时设置的模型或DEFAULT_MODEL)
+            text: 文本内容(必填)
+            image_url: 可选的图片URL
+            system_prompt: 系统提示词,可以是字符串、ArkMessage对象或字典。
+                          如果提供,会自动作为第一条消息(role="system")添加到消息列表前。
+            **kwargs: 其他请求参数
+        
+        Returns:
+            API响应数据
+        """
+        # 如果没有提供model参数,使用实例变量中的model
+        if model is None:
+            model = getattr(self, 'model', self.DEFAULT_MODEL)
+        
+        if text is None:
+            raise ValueError("文本内容不能为空")
+        
+        message = ArkMessage(role="user")
+        
+        if image_url:
+            message.add_image(image_url)
+        message.add_text(text)
+        
+        return self.chat(model=model, messages=[message], system_prompt=system_prompt, **kwargs)
+    
+    def get_response_text(self, response: Dict[str, Any]) -> Optional[str]:
+        """
+        从响应中提取文本内容
+        
+        Args:
+            response: API响应数据
+        
+        Returns:
+            提取的文本内容,如果不存在则返回None
+        """
+        try:
+            # 根据ARK API的实际响应结构提取文本
+            # 这里需要根据实际API响应格式调整
+            if "output" in response and isinstance(response["output"], list):
+                for item in response["output"]:
+                    if isinstance(item, dict) and "content" in item:
+                        for content in item.get("content", []):
+                            if content.get("type") == ContentType.OUTPUT_TEXT:
+                                return content.get("text")
+            
+            # 备用提取方式
+            if "choices" in response:
+                for choice in response["choices"]:
+                    if "message" in choice and "content" in choice["message"]:
+                        return choice["message"]["content"]
+            
+            return None
+            
+        except Exception as e:
+            logger.warning(f"提取响应文本失败: {e}")
+            return None
+

+ 238 - 0
api_modules/ark_client_async.py

@@ -0,0 +1,238 @@
+"""
+火山引擎ARK API异步客户端
+封装ARK API的异步调用,提供类型安全的接口
+"""
+
+import os
+import asyncio
+from typing import List, Optional, Dict, Any, Union
+from dataclasses import dataclass, asdict
+from enum import Enum
+
+from .base_client_async import AsyncAPIClient, APIError
+from .ark_client import ArkMessage, ContentType  # 复用同步版本的消息类
+from taskflow.logger import get_logger
+from taskflow.config import get_config
+
+logger = get_logger("api_modules.ark_client_async")
+
+
+class AsyncArkClient(AsyncAPIClient):
+    """
+    火山引擎ARK API异步客户端
+    
+    封装ARK API的异步调用,提供便捷的接口
+    """
+    
+    DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
+    DEFAULT_ENDPOINT = "/api/v3/responses"
+    DEFAULT_MODEL = "doubao-seed-1-6-251015"
+    
+    def __init__(
+        self,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        model: Optional[str] = None,
+        timeout: int = 300,
+        **kwargs
+    ):
+        """
+        初始化ARK API异步客户端
+        
+        Args:
+            api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
+            base_url: API基础URL(默认使用官方URL)
+            model: 模型名称(如果为None,会尝试从配置中获取)
+            timeout: 请求超时时间(秒,默认300秒,因为AI模型可能需要较长时间)
+            **kwargs: 传递给AsyncAPIClient的其他参数
+        """
+        # 获取API密钥(优先级:参数 > 环境变量 > 配置)
+        if api_key is None:
+            api_key = os.getenv("ARK_API_KEY")
+            if api_key is None:
+                config = get_config()
+                api_key = config.get("api.ark.api_key")
+        
+        if not api_key:
+            raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
+        
+        # 获取base_url(优先级:参数 > 配置 > 默认值)
+        if base_url is None:
+            config = get_config()
+            base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
+        
+        # 获取model(优先级:参数 > 配置 > 默认值)
+        if model is None:
+            config = get_config()
+            model = config.get("api.ark.model", self.DEFAULT_MODEL)
+        
+        super().__init__(
+            base_url=base_url,
+            api_key=api_key,
+            timeout=timeout,
+            **kwargs
+        )
+        
+        # 保存模型名称
+        self.model = model
+        
+        logger.info(f"ARK API异步客户端初始化完成,模型: {self.model}")
+    
+    async def chat(
+        self,
+        model: Optional[str] = None,
+        messages: List[Union[ArkMessage, Dict[str, Any]]] = None,
+        system_prompt: Optional[Union[str, ArkMessage, Dict[str, Any]]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        发送异步聊天请求
+        
+        Args:
+            model: 模型名称(如果为None,使用初始化时设置的模型或DEFAULT_MODEL)
+            messages: 消息列表(ArkMessage对象或字典)
+            system_prompt: 系统提示词,可以是字符串、ArkMessage对象或字典。
+                          如果提供,会自动作为第一条消息(role="system")添加到消息列表前。
+                          如果messages中已存在role="system"的消息,则不会重复添加。
+            **kwargs: 其他请求参数
+        
+        Returns:
+            API响应数据
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        # 如果没有提供model参数,使用实例变量中的model
+        if model is None:
+            model = getattr(self, 'model', self.DEFAULT_MODEL)
+        
+        # 转换消息格式
+        input_messages = []
+        
+        # 检查是否已有system角色的消息
+        has_system_message = False
+        for msg in messages:
+            if isinstance(msg, ArkMessage):
+                if msg.role == "system":
+                    has_system_message = True
+            elif isinstance(msg, dict):
+                if msg.get("role") == "system":
+                    has_system_message = True
+        
+        # 处理系统提示词
+        if system_prompt is not None and not has_system_message:
+            if isinstance(system_prompt, str):
+                # 字符串格式,转换为ArkMessage
+                system_msg = ArkMessage(role="system")
+                system_msg.add_text(system_prompt)
+                input_messages.append(system_msg.to_dict())
+            elif isinstance(system_prompt, ArkMessage):
+                # 确保role为system
+                system_prompt.role = "system"
+                input_messages.append(system_prompt.to_dict())
+            elif isinstance(system_prompt, dict):
+                # 字典格式,确保role为system
+                system_prompt = system_prompt.copy()
+                system_prompt["role"] = "system"
+                input_messages.append(system_prompt)
+            else:
+                raise ValueError(f"不支持的系统提示词类型: {type(system_prompt)}")
+        
+        # 添加其他消息
+        for msg in messages:
+            if isinstance(msg, ArkMessage):
+                input_messages.append(msg.to_dict())
+            else:
+                input_messages.append(msg)
+        
+        # 构建请求体
+        request_data = {
+            "model": model,
+            "input": input_messages,
+            **kwargs
+        }
+        
+        logger.info(f"发送异步聊天请求,模型: {model}, 消息数: {len(input_messages)}")
+
+        try:
+            response = await self.post(
+                endpoint=self.DEFAULT_ENDPOINT,
+                json=request_data
+            )
+            
+            logger.info("异步聊天请求成功")
+            return response
+            
+        except APIError as e:
+            logger.error(f"异步聊天请求失败: {e}")
+            raise
+    
+    async def chat_simple(
+        self,
+        model: Optional[str] = None,
+        text: str = None,
+        image_url: Optional[str] = None,
+        system_prompt: Optional[Union[str, ArkMessage, Dict[str, Any]]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        简化的异步聊天接口(仅文本或文本+图片)
+        
+        Args:
+            model: 模型名称(如果为None,使用初始化时设置的模型或DEFAULT_MODEL)
+            text: 文本内容(必填)
+            image_url: 可选的图片URL
+            system_prompt: 系统提示词,可以是字符串、ArkMessage对象或字典。
+                          如果提供,会自动作为第一条消息(role="system")添加到消息列表前。
+            **kwargs: 其他请求参数
+        
+        Returns:
+            API响应数据
+        """
+        # 如果没有提供model参数,使用实例变量中的model
+        if model is None:
+            model = getattr(self, 'model', self.DEFAULT_MODEL)
+        
+        if text is None:
+            raise ValueError("文本内容不能为空")
+        
+        message = ArkMessage(role="user")
+        
+        if image_url:
+            message.add_image(image_url)
+        message.add_text(text)
+        
+        return await self.chat(model=model, messages=[message], system_prompt=system_prompt, **kwargs)
+    
+    def get_response_text(self, response: Dict[str, Any]) -> Optional[str]:
+        """
+        从响应中提取文本内容
+        
+        Args:
+            response: API响应数据
+        
+        Returns:
+            提取的文本内容,如果不存在则返回None
+        """
+        try:
+            # 根据ARK API的实际响应结构提取文本
+            # 这里需要根据实际API响应格式调整
+            if "output" in response and isinstance(response["output"], list):
+                for item in response["output"]:
+                    if isinstance(item, dict) and "content" in item:
+                        for content in item.get("content", []):
+                            if content.get("type") == ContentType.OUTPUT_TEXT:
+                                return content.get("text")
+            
+            # 备用提取方式
+            if "choices" in response:
+                for choice in response["choices"]:
+                    if "message" in choice and "content" in choice["message"]:
+                        return choice["message"]["content"]
+            
+            return None
+            
+        except Exception as e:
+            logger.warning(f"提取响应文本失败: {e}")
+            return None
+

+ 263 - 0
api_modules/ark_image_client.py

@@ -0,0 +1,263 @@
+"""
+火山引擎ARK图片生成API客户端
+封装ARK图片生成API的调用,提供类型安全的接口
+"""
+
+import os
+import base64
+from typing import Optional, Dict, Any, List
+from pathlib import Path
+
+from .base_client import APIClient, APIError
+from taskflow.logger import get_logger
+from taskflow.config import get_config
+
+logger = get_logger("api_modules.ark_image_client")
+
+
+def encode_image_to_base64(image_path: str) -> str:
+    """
+    将本地图片文件编码为base64格式
+    
+    Args:
+        image_path: 图片文件路径
+    
+    Returns:
+        base64编码的图片字符串(包含data:image/...;base64,前缀)
+    """
+    try:
+        with open(image_path, 'rb') as image_file:
+            image_data = image_file.read()
+            image_base64 = base64.b64encode(image_data).decode('utf-8')
+            
+            # 根据文件扩展名确定MIME类型
+            ext = Path(image_path).suffix.lower()
+            mime_types = {
+                '.jpg': 'image/jpeg',
+                '.jpeg': 'image/jpeg',
+                '.png': 'image/png',
+                '.gif': 'image/gif',
+                '.webp': 'image/webp'
+            }
+            mime_type = mime_types.get(ext, 'image/jpeg')
+            
+            return f"data:{mime_type};base64,{image_base64}"
+    except Exception as e:
+        logger.error(f"编码图片失败: {e}")
+        raise ValueError(f"无法读取或编码图片文件: {image_path}") from e
+
+
+class ArkImageClient(APIClient):
+    """
+    火山引擎ARK图片生成API客户端
+    
+    封装ARK图片生成API的调用,提供便捷的接口
+    """
+    
+    DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
+    DEFAULT_ENDPOINT = "/api/v3/images/generations"
+    DEFAULT_MODEL = "doubao-seedream-4-0-250828"
+    
+    def __init__(
+        self,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        model: Optional[str] = None,
+        timeout: int = 120,
+        sequential_generation: str = "disabled",
+        response_format: str = "url",
+        stream: bool = False,
+        watermark: bool = False,
+        **kwargs
+    ):
+        """
+        初始化ARK图片生成API客户端
+        
+        Args:
+            api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
+            base_url: API基础URL(默认使用官方URL)
+            model: 模型名称(如果为None,会尝试从配置中获取)
+            timeout: 请求超时时间(秒,默认120秒)
+            sequential_generation: 序列生成开关(默认"disabled")
+            response_format: 响应格式(默认"url")
+            stream: 流式响应开关(默认False)
+            watermark: 水印开关(默认False)
+            **kwargs: 传递给APIClient的其他参数
+        """
+        # 获取API密钥(优先级:参数 > 环境变量 > 配置)
+        if api_key is None:
+            api_key = os.getenv("ARK_API_KEY")
+            if api_key is None:
+                config = get_config()
+                api_key = config.get("api.ark.api_key")
+        
+        if not api_key:
+            raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
+        
+        # 获取base_url(优先级:参数 > 配置 > 默认值)
+        if base_url is None:
+            config = get_config()
+            base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
+        
+        # 获取model(优先级:参数 > 配置 > 默认值)
+        if model is None:
+            config = get_config()
+            model = config.get("api.ark.image_model", self.DEFAULT_MODEL)
+        
+        super().__init__(
+            base_url=base_url,
+            api_key=api_key,
+            timeout=timeout,
+            **kwargs
+        )
+        
+        # 保存图片生成相关配置
+        self.model = model
+        self.sequential_generation = sequential_generation
+        self.response_format = response_format
+        self.stream = stream
+        self.watermark = watermark
+        
+        logger.info(f"ARK图片生成API客户端初始化完成,模型: {self.model}")
+    
+    def generate_image(
+        self,
+        prompt: str,
+        size: str = "1440x2560",
+        reference_image: Optional[List[str]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        生成图片
+        
+        Args:
+            prompt: 图片生成提示词(必填)
+            size: 图片尺寸,格式为"宽x高"(默认"1440x2560")
+            reference_image: 参考图片列表,可以是:
+                - 本地文件路径列表(会自动编码为base64)
+                - HTTP/HTTPS URL列表
+                - base64编码的字符串列表(包含data:image/...;base64,前缀)
+                如果为None,则生成无参考图片列表
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            API响应数据,包含生成的图片信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        if not prompt:
+            raise ValueError("prompt不能为空")
+        
+        # 构建请求体
+        request_data = {
+            "model": kwargs.get("model", self.model),
+            "prompt": prompt,
+            "size": size,
+            "sequential_image_generation": kwargs.get("sequential_generation", self.sequential_generation),
+            "response_format": kwargs.get("response_format", self.response_format),
+            "stream": kwargs.get("stream", self.stream),
+            "watermark": kwargs.get("watermark", self.watermark),
+        }
+        
+        # 如果有参考图片,添加到请求中
+        if reference_image:
+            # 判断是本地文件路径还是URL
+            if reference_image[0].startswith(("http://", "https://")):
+                # URL格式,直接使用
+                request_data["image"] = reference_image
+            elif reference_image[0].startswith("data:image"):
+                # 已经是base64格式,直接使用
+                request_data["image"] = reference_image
+            else:
+                # 本地文件路径,编码为base64
+                request_data["image"] = [encode_image_to_base64(image) for image in reference_image]
+        
+        logger.info(f"发送图片生成请求,模型: {request_data['model']}, 尺寸: {size}")
+        if reference_image:
+            logger.info(f"使用参考图片: {reference_image[:50]}...")
+        
+        try:
+            response = self.post(
+                endpoint=self.DEFAULT_ENDPOINT,
+                json=request_data
+            )
+            
+            logger.info("图片生成请求成功")
+            return response
+            
+        except APIError as e:
+            logger.error(f"图片生成请求失败: {e}")
+            raise
+    
+    def get_image_url(self, response: Dict[str, Any]) -> Optional[str]:
+        """
+        从响应中提取图片URL
+        
+        Args:
+            response: API响应数据
+        
+        Returns:
+            图片URL,如果不存在则返回None
+        """
+        try:
+            if "data" in response and isinstance(response["data"], list):
+                if len(response["data"]) > 0:
+                    image_data = response["data"][0]
+                    if isinstance(image_data, dict):
+                        # 根据response_format返回相应字段
+                        if self.response_format == "url":
+                            return image_data.get("url")
+                        elif self.response_format == "b64_json":
+                            return image_data.get("b64_json")
+            
+            return None
+            
+        except (KeyError, TypeError, IndexError) as e:
+            logger.warning(f"提取图片URL失败: {e}")
+            return None
+    
+    def get_image_urls(self, response: Dict[str, Any]) -> List[str]:
+        """
+        从响应中提取所有图片URL
+        
+        Args:
+            response: API响应数据
+        
+        Returns:
+            图片URL列表
+        """
+        urls = []
+        try:
+            if "data" in response and isinstance(response["data"], list):
+                for image_data in response["data"]:
+                    if isinstance(image_data, dict):
+                        if self.response_format == "url":
+                            url = image_data.get("url")
+                        elif self.response_format == "b64_json":
+                            url = image_data.get("b64_json")
+                        else:
+                            url = image_data.get("url") or image_data.get("b64_json")
+                        
+                        if url:
+                            urls.append(url)
+            
+            return urls
+            
+        except (KeyError, TypeError, IndexError) as e:
+            logger.warning(f"提取图片URL列表失败: {e}")
+            return []
+
+
+if __name__ == "__main__":
+    client = ArkImageClient()
+
+    response = client.generate_image(
+        prompt = "图1中的女生穿着图2中的衣服在街道上散步",
+        reference_image = ["./data/image/face.jpg", "./data/image/cloth.jpg"],
+        size = "1440x2560"
+    )
+
+    image_url = client.get_image_url(response)
+    print(image_url)

+ 512 - 0
api_modules/ark_image_client_async.py

@@ -0,0 +1,512 @@
+"""
+火山引擎ARK图片生成API异步客户端
+封装ARK图片生成API的异步调用,提供类型安全的接口
+"""
+
+import os
+import base64
+import asyncio
+import aiohttp
+from typing import Optional, Dict, Any, List, Callable
+from pathlib import Path
+
+from .base_client_async import AsyncAPIClient, APIError, RetryConfig
+from .ark_image_client import encode_image_to_base64  # 复用同步版本的编码函数
+from taskflow.logger import get_logger
+from taskflow.config import get_config
+
+logger = get_logger("api_modules.ark_image_client_async")
+
+
+def handle_image_result(
+    task_id: str,
+    output_path: str,
+    result: Optional[Dict],
+    error: Optional[str]
+) -> None:
+    """处理图片生成结果的回调函数"""
+    if error:
+        logger.info(f"\n任务 {task_id} 处理失败:{error}")
+    else:
+        from examples.video_create.utils.tools import download_image
+        image_url = result.get("data", [{}])[0].get("url") if result.get("data") else None
+        if image_url:
+            download_image(image_url, output_path)
+            logger.info(f"生成图片已下载:{output_path}")
+        else:
+            logger.warning(f"任务 {task_id} 完成但未获取到图片URL")
+
+
+class AsyncArkImageClient(AsyncAPIClient):
+    """
+    火山引擎ARK图片生成API异步客户端
+    
+    封装ARK图片生成API的异步调用,提供便捷的接口
+    """
+    
+    DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
+    DEFAULT_ENDPOINT = "/api/v3/images/generations"
+    DEFAULT_MODEL = "doubao-seedream-4-0-250828"
+    
+    def __init__(
+        self,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        model: Optional[str] = None,
+        timeout: int = 120,
+        sequential_generation: str = "disabled",
+        response_format: str = "url",
+        stream: bool = False,
+        watermark: bool = False,
+        **kwargs
+    ):
+        """
+        初始化ARK图片生成API异步客户端
+        
+        Args:
+            api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
+            base_url: API基础URL(默认使用官方URL)
+            model: 模型名称(如果为None,会尝试从配置中获取)
+            timeout: 请求超时时间(秒,默认120秒)
+            sequential_generation: 序列生成开关(默认"disabled")
+            response_format: 响应格式(默认"url")
+            stream: 流式响应开关(默认False)
+            watermark: 水印开关(默认False)
+            **kwargs: 传递给AsyncAPIClient的其他参数
+        """
+        # 获取API密钥(优先级:参数 > 环境变量 > 配置)
+        if api_key is None:
+            api_key = os.getenv("ARK_API_KEY")
+            if api_key is None:
+                config = get_config()
+                api_key = config.get("api.ark.api_key")
+        
+        if not api_key:
+            raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
+        
+        # 获取base_url(优先级:参数 > 配置 > 默认值)
+        if base_url is None:
+            config = get_config()
+            base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
+        
+        # 获取model(优先级:参数 > 配置 > 默认值)
+        if model is None:
+            config = get_config()
+            model = config.get("api.ark.image_model", self.DEFAULT_MODEL)
+
+        # 创建自定义重试配置
+        retry_config = RetryConfig(
+            max_retries=3,
+            backoff_factor=3.0,
+            retry_on_status=(500, 502, 429, 503, 504),
+            retry_on_exception=(aiohttp.ClientError, asyncio.TimeoutError)
+        )
+
+        super().__init__(
+            base_url=base_url,
+            api_key=api_key,
+            timeout=timeout,
+            retry_config=retry_config,
+            **kwargs
+        )
+        
+        # 保存图片生成相关配置
+        self.model = model
+        self.sequential_generation = sequential_generation
+        self.response_format = response_format
+        self.stream = stream
+        self.watermark = watermark
+        
+        logger.info(f"ARK图片生成API异步客户端初始化完成,模型: {self.model}")
+    
+    async def create_image_task(
+        self,
+        prompt: str,
+        size: str = "1440x2560",
+        reference_image: Optional[List[str]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        异步创建图片生成任务
+        
+        Args:
+            prompt: 图片生成提示词(必填)
+            size: 图片尺寸,格式为"宽x高"(默认"1440x2560")
+            reference_image: 参考图片列表,可以是:
+                - 本地文件路径列表(会自动编码为base64)
+                - HTTP/HTTPS URL列表
+                - base64编码的字符串列表(包含data:image/...;base64,前缀)
+                如果为None,则生成无参考图片列表
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            API响应数据,包含生成的图片信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        if not prompt or not prompt.strip():
+            raise ValueError("prompt不能为空")
+        
+        # 构建请求体
+        request_data = {
+            "model": kwargs.get("model", self.model),
+            "prompt": prompt,
+            "size": size,
+            "sequential_image_generation": kwargs.get("sequential_generation", self.sequential_generation),
+            "response_format": kwargs.get("response_format", self.response_format),
+            "stream": kwargs.get("stream", self.stream),
+            "watermark": kwargs.get("watermark", self.watermark),
+        }
+        
+        # 如果有参考图片,添加到请求中
+        if reference_image and len(reference_image) > 0:
+            # 判断是本地文件路径还是URL
+            if reference_image[0].startswith(("http://", "https://")):
+                # URL格式,直接使用
+                request_data["image"] = reference_image
+            elif reference_image[0].startswith("data:image"):
+                # 已经是base64格式,直接使用
+                request_data["image"] = reference_image
+            else:
+                # 本地文件路径,编码为base64(使用线程池避免阻塞事件循环)
+                loop = asyncio.get_event_loop()
+                request_data["image"] = [await loop.run_in_executor(None, encode_image_to_base64, image) for image in reference_image]
+        
+        logger.info(f"创建异步图片生成任务,模型: {request_data['model']}, 尺寸: {size}")
+        if reference_image and len(reference_image) > 0:
+            logger.info(f"使用参考图片: {reference_image[:50]}...")
+        else:
+            logger.info("未使用参考图片")
+        
+        # 记录请求数据(用于调试)
+        logger.debug(f"请求数据: {request_data}")
+        
+        try:
+            response = await self.post(
+                endpoint=self.DEFAULT_ENDPOINT,
+                json=request_data
+            )
+            
+            logger.info("图片生成任务创建成功")
+            return response
+            
+        except APIError as e:
+            logger.error(f"创建图片生成任务失败: {e}")
+            logger.error(f"请求数据: {request_data}")
+            if e.response:
+                logger.error(f"API错误响应: {e.response}")
+            raise
+    
+    async def query_image_task(self, task_id: str) -> Dict[str, Any]:
+        """
+        查询图片生成任务状态
+        
+        注意:图片生成API通常是同步的,此方法主要用于接口一致性。
+        如果任务已完成,直接返回结果;否则返回待处理状态。
+        
+        Args:
+            task_id: 任务ID(对于图片生成,这通常是响应中的某个标识符)
+        
+        Returns:
+            任务状态详情,包含图片URL等信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        # 图片生成API通常是同步的,不需要查询
+        # 此方法主要用于接口一致性
+        logger.warning("图片生成API是同步的,query_image_task方法可能不适用")
+        raise NotImplementedError("图片生成API是同步的,不需要查询任务状态")
+    
+    async def wait_for_task(
+        self,
+        task_id: str,
+        callback: Optional[Callable[[str, Dict[str, Any], Optional[str]], None]] = None
+    ) -> Dict[str, Any]:
+        """
+        等待任务完成
+        
+        注意:图片生成API通常是同步的,此方法主要用于接口一致性。
+        对于图片生成,任务通常在create_image_task时就已经完成。
+        
+        Args:
+            task_id: 任务ID
+            callback: 可选的回调函数,参数为 (task_id, result, error)
+        
+        Returns:
+            任务完成后的结果
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        # 图片生成API通常是同步的,不需要等待
+        # 此方法主要用于接口一致性
+        logger.warning("图片生成API是同步的,wait_for_task方法可能不适用")
+        raise NotImplementedError("图片生成API是同步的,不需要等待任务完成")
+    
+    async def create_image_task_async(
+        self,
+        prompt: str,
+        size: str = "1440x2560",
+        reference_image: Optional[List[str]] = None,
+        callback: Optional[Callable] = handle_image_result,
+        output_path: Optional[str] = None,
+        **kwargs
+    ) -> Optional[str]:
+        """
+        创建图片生成任务并在后台任务中处理(不阻塞主流程)
+        
+        任务会在后台异步任务中执行,完成后调用回调函数。
+        
+        Args:
+            prompt: 图片生成提示词(必填)
+            size: 图片尺寸,格式为"宽x高"(默认"1440x2560")
+            reference_image: 参考图片列表(可选)
+            callback: 可选的回调函数,可以是以下两种签名之一:
+                     1. (task_id, result, error) -> None
+                     2. (task_id, output_path, result, error) -> None
+            output_path: 图片输出路径(可选,会传递给回调函数)
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            任务ID(task_id),如果创建失败则返回None
+        
+        Raises:
+            APIError: 如果创建任务失败
+        """
+        # 生成一个简单的任务ID(基于时间戳)
+        import time
+        task_id = f"img_{int(time.time() * 1000)}"
+        
+        async def _background_task():
+            """后台任务:执行图片生成并调用回调"""
+            try:
+                # 创建图片生成任务
+                result = await self.create_image_task(
+                    prompt=prompt,
+                    size=size,
+                    reference_image=reference_image,
+                    **kwargs
+                )
+                
+                # 调用回调函数
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        # 4参数版本:(task_id, output_path, result, error)
+                        callback(task_id, output_path or "", result, None)
+                    else:
+                        # 3参数版本:(task_id, result, error)
+                        callback(task_id, result, None)
+                
+            except Exception as e:
+                error_msg = str(e)
+                logger.error(f"后台图片生成任务失败: {error_msg}")
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        callback(task_id, output_path or "", {}, error_msg)
+                    else:
+                        callback(task_id, {}, error_msg)
+        
+        # 启动后台任务
+        asyncio.create_task(_background_task())
+        logger.info(f"图片生成任务已提交,task_id: {task_id},后台处理中...")
+        
+        return task_id
+    
+    async def create_and_wait(
+        self,
+        prompt: str,
+        size: str = "1440x2560",
+        reference_image: Optional[List[str]] = None,
+        callback: Optional[Callable[[str, Dict[str, Any], Optional[str]], None]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        创建图片生成任务并等待完成(便捷方法)
+        
+        Args:
+            prompt: 图片生成提示词(必填)
+            size: 图片尺寸,格式为"宽x高"(默认"1440x2560")
+            reference_image: 参考图片列表(可选)
+            callback: 可选的回调函数,参数为 (task_id, result, error)
+            **kwargs: 其他请求参数
+        
+        Returns:
+            任务完成后的结果
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        # 创建任务(图片生成是同步的,所以直接返回结果)
+        result = await self.create_image_task(
+            prompt=prompt,
+            size=size,
+            reference_image=reference_image,
+            **kwargs
+        )
+        
+        # 生成一个简单的任务ID
+        import time
+        task_id = f"img_{int(time.time() * 1000)}"
+        
+        logger.info(f"图片生成任务完成,任务ID: {task_id}")
+        
+        # 调用回调函数(如果提供)
+        if callback:
+            if asyncio.iscoroutinefunction(callback):
+                await callback(task_id, result, None)
+            else:
+                callback(task_id, result, None)
+        
+        return result
+    
+    def get_image_url(self, response: Dict[str, Any]) -> Optional[str]:
+        """
+        从响应中提取图片URL
+        
+        Args:
+            response: API响应数据(从create_image_task或create_and_wait返回)
+        
+        Returns:
+            图片URL,如果不存在则返回None
+        """
+        try:
+            if "data" in response and isinstance(response["data"], list):
+                if len(response["data"]) > 0:
+                    image_data = response["data"][0]
+                    if isinstance(image_data, dict):
+                        # 根据response_format返回相应字段
+                        if self.response_format == "url":
+                            return image_data.get("url")
+                        elif self.response_format == "b64_json":
+                            return image_data.get("b64_json")
+            
+            return None
+            
+        except (KeyError, TypeError, IndexError) as e:
+            logger.warning(f"提取图片URL失败: {e}")
+            return None
+    
+    def get_image_urls(self, response: Dict[str, Any]) -> List[str]:
+        """
+        从响应中提取所有图片URL
+        
+        Args:
+            response: API响应数据
+        
+        Returns:
+            图片URL列表
+        """
+        urls = []
+        try:
+            if "data" in response and isinstance(response["data"], list):
+                for image_data in response["data"]:
+                    if isinstance(image_data, dict):
+                        if self.response_format == "url":
+                            url = image_data.get("url")
+                        elif self.response_format == "b64_json":
+                            url = image_data.get("b64_json")
+                        else:
+                            url = image_data.get("url") or image_data.get("b64_json")
+                        
+                        if url:
+                            urls.append(url)
+            
+            return urls
+            
+        except (KeyError, TypeError, IndexError) as e:
+            logger.warning(f"提取图片URL列表失败: {e}")
+            return []
+    
+    def get_task_status(self, result: Dict[str, Any]) -> Optional[str]:
+        """
+        从任务结果中提取任务状态
+        
+        Args:
+            result: 任务结果(从create_image_task或create_and_wait返回)
+        
+        Returns:
+            任务状态字符串,如果不存在则返回None
+        """
+        try:
+            # 图片生成API通常是同步的,如果返回了数据,则认为成功
+            if result.get("data"):
+                return "succeeded"
+            return None
+        except (KeyError, TypeError, AttributeError):
+            return None
+    
+    # 保持向后兼容:generate_image作为便捷方法
+    async def generate_image(
+        self,
+        prompt: str,
+        size: str = "1440x2560",
+        reference_image: Optional[List[str]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        异步生成图片(便捷方法,等同于create_and_wait)
+        
+        Args:
+            prompt: 图片生成提示词(必填)
+            size: 图片尺寸,格式为"宽x高"(默认"1440x2560")
+            reference_image: 参考图片列表(可选)
+            **kwargs: 其他请求参数
+        
+        Returns:
+            API响应数据,包含生成的图片信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        return await self.create_and_wait(
+            prompt=prompt,
+            size=size,
+            reference_image=reference_image,
+            **kwargs
+        )
+
+
+async def main():
+    # 示例用法
+    async with AsyncArkImageClient() as client:
+        # 方式1:创建任务并等待完成(推荐)
+        try:
+            result = await client.create_and_wait(
+                prompt="图1中的女生穿着图2中的衣服在街道上散步,目视前方,手牵着一只小狗",
+                reference_image=["./data/image/face.jpg", "./data/image/cloth.jpg"],
+                size="1440x2560"
+            )
+            image_url = client.get_image_url(result)
+            print(f"图片生成成功,URL: {image_url}")
+        except Exception as e:
+            print(f"图片生成失败: {e}")
+        
+        # 方式2:使用便捷方法generate_image
+        # response = await client.generate_image(
+        #     prompt="一个美丽的风景",
+        #     size="1440x2560"
+        # )
+        # image_url = client.get_image_url(response)
+        # print(f"图片URL: {image_url}")
+        
+        # 方式3:异步创建任务(后台处理)
+        # task_id = await client.create_image_task_async(
+        #     prompt="一个美丽的风景",
+        #     callback=handle_image_result,
+        #     output_path="./output/image.jpg"
+        # )
+        # print(f"任务已提交,task_id: {task_id}")
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 701 - 0
api_modules/ark_video_client.py

@@ -0,0 +1,701 @@
+"""
+火山引擎ARK视频生成API客户端
+封装ARK视频生成API的调用,提供类型安全的接口
+"""
+
+import os
+import time
+import threading
+from typing import Optional, Dict, Any, Callable
+from enum import Enum
+
+from examples.video_create.utils.tools import download_video
+from .base_client import APIClient, APIError
+from taskflow.logger import get_logger
+from taskflow.config import get_config
+
+logger = get_logger("api_modules.ark_video_client")
+
+
+class TaskStatus(str, Enum):
+    """任务状态枚举"""
+    PENDING = "pending"
+    PROCESSING = "processing"
+    SUCCEEDED = "succeeded"
+    FAILED = "failed"
+
+def handle_video_result(
+    task_id: str,
+    output_path: str,
+    result: Optional[Dict],
+    error: Optional[str]
+) -> None:
+    if error:
+        logger.info(f"\n任务 {task_id} 处理失败:{error}")
+    else:
+        video_url = result.get("content", {}).get("video_url")
+        download_video(video_url, output_path) 
+        logger.info(f"生成视频已下载:{output_path}")
+
+class ArkVideoClient(APIClient):
+    """
+    火山引擎ARK视频生成API客户端
+    
+    封装ARK视频生成API的调用,提供便捷的接口
+    """
+    
+    DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
+    DEFAULT_ENDPOINT = "/api/v3/contents/generations/tasks"
+    DEFAULT_MODEL = "doubao-seedance-1-0-pro-250528"
+    
+    def __init__(
+        self,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        model: Optional[str] = None,
+        timeout: int = 60,
+        poll_interval: int = 5,
+        max_poll_time: int = 500,
+        **kwargs
+    ):
+        """
+        初始化ARK视频生成API客户端
+        
+        Args:
+            api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
+            base_url: API基础URL(默认使用官方URL)
+            model: 模型名称(如果为None,会尝试从配置中获取)
+            timeout: 请求超时时间(秒,默认60秒)
+            poll_interval: 轮询间隔(秒,默认5秒)
+            max_poll_time: 最大轮询总时间(秒,默认500秒)
+            **kwargs: 传递给APIClient的其他参数
+        """
+        # 获取API密钥(优先级:参数 > 环境变量 > 配置)
+        if api_key is None:
+            api_key = os.getenv("ARK_API_KEY")
+            if api_key is None:
+                config = get_config()
+                api_key = config.get("api.ark.api_key")
+        
+        if not api_key:
+            raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
+        
+        # 获取base_url(优先级:参数 > 配置 > 默认值)
+        if base_url is None:
+            config = get_config()
+            base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
+        
+        # 获取model(优先级:参数 > 配置 > 默认值)
+        if model is None:
+            config = get_config()
+            model = config.get("api.ark.video_model", self.DEFAULT_MODEL)
+        
+        super().__init__(
+            base_url=base_url,
+            api_key=api_key,
+            timeout=timeout,
+            **kwargs
+        )
+        
+        # 保存视频生成相关配置
+        self.model = model
+        self.poll_interval = poll_interval
+        self.max_poll_time = max_poll_time
+        
+        logger.info(f"ARK视频生成API客户端初始化完成,模型: {self.model}")
+    
+
+    def _build_gen_params(
+        self,
+        duration: Optional[int] = None,
+        ratio: Optional[str] = None,
+        resolution: Optional[str] = None,
+        watermark: Optional[str] = None,
+        camerafixed: Optional[str] = None,
+        **kwargs
+    ) -> str:
+        """
+        构建生成参数字符串
+        
+        如果参数为None,则使用默认值或从kwargs中获取
+
+        Args:
+            duration: 视频时长(秒,默认4秒)
+            ratio: 视频比例(默认"16:9")
+            resolution: 视频分辨率(默认"1080p")
+            watermark: 水印开关(默认"false")
+            camerafixed: 相机固定开关(默认"false")
+            **kwargs: 其他参数,会优先从kwargs中获取
+
+        Returns:
+            生成参数字符串,格式如:"--dur 4 --rt 16:9 --rs 1080p --wm false --cf false"
+        """
+        # 默认值
+        defaults = {
+            "duration": 4,
+            "ratio": "16:9",
+            "resolution": "1080p",
+            "watermark": "false",
+            "camerafixed": "false"
+        }
+        
+        # 从kwargs中获取参数,如果没有则使用传入的参数,再没有则使用默认值
+        # 优先级:kwargs > 显式参数 > 默认值
+        def get_param(key: str, param_value: Any) -> Any:
+            if key in kwargs:
+                return kwargs[key]
+            return param_value if param_value is not None else defaults[key]
+        
+        duration = get_param("duration", duration)
+        ratio = get_param("ratio", ratio)
+        resolution = get_param("resolution", resolution)
+        watermark = get_param("watermark", watermark)
+        camerafixed = get_param("camerafixed", camerafixed)
+        
+        # 构建参数字符串
+        params = [
+            f"--dur {duration}",
+            f"--rt {ratio}",
+            f"--rs {resolution}",
+            f"--wm {watermark}",
+            f"--cf {camerafixed}"
+        ]
+        
+        return " ".join(params) + " "
+        
+    def create_video_task(
+        self,
+        prompt: str,
+        image_url: str,
+        gen_params: Optional[str] = None,
+        duration: Optional[int] = None,
+        ratio: Optional[str] = None,
+        resolution: Optional[str] = None,
+        watermark: Optional[str] = None,
+        camerafixed: Optional[str] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        创建视频生成任务
+        
+        Args:
+            prompt: 视频生成提示词(必填)
+            image_url: 参考图片URL(必填,必须是可访问的HTTP/HTTPS URL)
+            gen_params: 自定义生成参数字符串(可选,如果提供则忽略其他生成参数)
+            duration: 视频时长(秒,默认4秒)
+            ratio: 视频比例(默认"16:9")
+            resolution: 视频分辨率(默认"1080p")
+            watermark: 水印开关(默认"false")
+            camerafixed: 相机固定开关(默认"false")
+            **kwargs: 其他请求参数(会覆盖默认配置,包括生成参数)
+        
+        Returns:
+            API响应数据,包含任务ID等信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        
+        示例:
+            >>> client.create_video_task(
+            ...     prompt="一个美丽的风景",
+            ...     image_url="https://example.com/image.jpg",
+            ...     duration=5,
+            ...     ratio="9:16",
+            ...     resolution="720p"
+            ... )
+        """
+        if not prompt or not prompt.strip():
+            raise ValueError("prompt不能为空")
+        
+        if not image_url or not image_url.strip():
+            raise ValueError("image_url不能为空")
+        
+        # 验证image_url是否为URL格式
+        if not image_url.startswith(("http://", "https://")):
+            raise ValueError(
+                f"image_url必须是HTTP/HTTPS URL格式,当前值: {image_url}。"
+                "如果是本地文件路径,请先上传到云存储获取URL。"
+            )
+
+        # 构建生成参数:如果提供了gen_params字符串,直接使用;否则根据参数构建
+        if gen_params is None:
+            # 合并kwargs和显式参数
+            gen_params_kwargs = {
+                "duration": duration,
+                "ratio": ratio,
+                "resolution": resolution,
+                "watermark": watermark,
+                "camerafixed": camerafixed,
+                **kwargs
+            }
+            gen_params = self._build_gen_params(**gen_params_kwargs)
+        else:
+            # 如果提供了gen_params字符串,确保以空格结尾(如果没有)
+            gen_params = gen_params.strip()
+            if gen_params and not gen_params.endswith(" "):
+                gen_params += " "
+
+        # 构建请求体
+        request_data = {
+            "model": kwargs.get("model", self.model),
+            "content": [
+                {
+                    "type": "text",
+                    "text": prompt + gen_params
+                },
+                {
+                    "type": "image_url",
+                    "image_url": {
+                        "url": image_url
+                    }
+                }
+            ],
+            **{k: v for k, v in kwargs.items() if k != "model"}
+        }
+        
+        logger.info(f"创建视频生成任务,模型: {request_data['model']}, 提示词: {prompt[:50]}...")
+        logger.info(f"参考图片: {image_url}")
+        
+        try:
+            response = self.post(
+                endpoint=self.DEFAULT_ENDPOINT,
+                json=request_data
+            )
+            
+            logger.info(f"视频生成任务创建成功,任务ID: {response.get('id', 'unknown')}")
+            return response
+            
+        except APIError as e:
+            logger.error(f"创建视频生成任务失败: {e}")
+            raise
+    
+    def query_video_task(self, task_id: str) -> Dict[str, Any]:
+        """
+        查询视频生成任务状态
+        
+        Args:
+            task_id: 任务ID(从create_video_task响应中获取)
+        
+        Returns:
+            任务状态详情,包含状态、视频URL等信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        if not task_id or not task_id.strip():
+            raise ValueError("task_id不能为空")
+        
+        query_endpoint = f"{self.DEFAULT_ENDPOINT}/{task_id}"
+        
+        logger.debug(f"查询视频生成任务状态,任务ID: {task_id}")
+        
+        try:
+            response = self.get(endpoint=query_endpoint)
+            
+            status = response.get("status", "").lower()
+            logger.debug(f"任务 {task_id} 状态: {status}")
+            
+            return response
+            
+        except APIError as e:
+            logger.error(f"查询视频生成任务状态失败: {e}")
+            raise
+    
+    def wait_for_task(
+        self,
+        task_id: str,
+        callback: Optional[Callable] = None,
+        callback_kwargs: Optional[Dict[str, Any]] = None
+    ) -> Dict[str, Any]:
+        """
+        等待任务完成(同步轮询)
+        
+        Args:
+            task_id: 任务ID
+            callback: 可选的回调函数,可以是以下两种签名之一:
+                     1. (task_id, result, error) -> None
+                     2. (task_id, output_path, result, error) -> None
+            callback_kwargs: 传递给回调函数的额外关键字参数(如 output_path)
+        
+        Returns:
+            任务完成后的结果
+        
+        Raises:
+            APIError: 如果请求失败
+            TimeoutError: 如果任务超时
+        """
+        start_time = time.time()
+        callback_kwargs = callback_kwargs or {}
+        
+        while True:
+            elapsed = time.time() - start_time
+            
+            if elapsed > self.max_poll_time:
+                error_msg = f"任务超时(超过 {self.max_poll_time} 秒)"
+                logger.error(f"任务 {task_id} {error_msg}")
+                if callback:
+                    # 检查回调函数签名,支持两种格式
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        # 4参数版本:(task_id, output_path, result, error)
+                        callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
+                    else:
+                        # 3参数版本:(task_id, result, error)
+                        callback(task_id, {}, error_msg)
+                raise TimeoutError(error_msg)
+            
+            # 查询任务状态
+            result = self.query_video_task(task_id)
+            
+            if not result:
+                logger.warning(f"任务 {task_id} 查询结果为空,继续等待...")
+                time.sleep(self.poll_interval)
+                continue
+            
+            # 解析状态
+            status = result.get("status", "").lower()
+            
+            if status == TaskStatus.SUCCEEDED:
+                logger.info(f"任务 {task_id} 完成,耗时: {int(elapsed)}秒")
+                if callback:
+                    # 检查回调函数签名,支持两种格式
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        # 4参数版本:(task_id, output_path, result, error)
+                        callback(task_id, callback_kwargs.get("output_path", ""), result, None)
+                    else:
+                        # 3参数版本:(task_id, result, error)
+                        callback(task_id, result, None)
+                return result
+            
+            elif status == TaskStatus.FAILED:
+                error_msg = result.get("error", {}).get("message", "未知错误")
+                logger.error(f"任务 {task_id} 失败: {error_msg}")
+                if callback:
+                    # 检查回调函数签名,支持两种格式
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        # 4参数版本:(task_id, output_path, result, error)
+                        callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
+                    else:
+                        # 3参数版本:(task_id, result, error)
+                        callback(task_id, {}, error_msg)
+                raise APIError(f"任务失败: {error_msg}")
+            
+            elif status in [TaskStatus.PENDING, TaskStatus.PROCESSING]:
+                logger.info(f"任务 {task_id} 处理中({int(elapsed)}秒),状态: {status}")
+                time.sleep(self.poll_interval)
+            
+            else:
+                logger.warning(f"任务 {task_id} 未知状态: {status},继续等待...")
+                time.sleep(self.poll_interval)
+    
+    def _background_poll(
+        self,
+        task_id: str,
+        callback: Optional[Callable] = None,
+        callback_kwargs: Optional[Dict[str, Any]] = None
+    ):
+        """
+        后台轮询任务状态的线程函数
+        
+        Args:
+            task_id: 任务ID
+            callback: 回调函数
+            callback_kwargs: 传递给回调函数的额外参数
+        """
+        callback_kwargs = callback_kwargs or {}
+        start_time = time.time()
+        
+        while True:
+            elapsed = time.time() - start_time
+            
+            if elapsed > self.max_poll_time:
+                error_msg = f"任务超时(超过 {self.max_poll_time} 秒)"
+                logger.error(f"任务 {task_id} {error_msg}")
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
+                    else:
+                        callback(task_id, {}, error_msg)
+                return
+            
+            # 查询任务状态
+            try:
+                result = self.query_video_task(task_id)
+            except Exception as e:
+                logger.error(f"查询任务 {task_id} 状态失败: {e}")
+                time.sleep(self.poll_interval)
+                continue
+            
+            if not result:
+                logger.warning(f"任务 {task_id} 查询结果为空,继续等待...")
+                time.sleep(self.poll_interval)
+                continue
+            
+            # 解析状态
+            status = result.get("status", "").lower()
+            
+            if status == TaskStatus.SUCCEEDED:
+                logger.info(f"任务 {task_id} 完成,耗时: {int(elapsed)}秒")
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        callback(task_id, callback_kwargs.get("output_path", ""), result, None)
+                    else:
+                        callback(task_id, result, None)
+                return
+            
+            elif status == TaskStatus.FAILED:
+                error_msg = result.get("error", {}).get("message", "未知错误")
+                logger.error(f"任务 {task_id} 失败: {error_msg}")
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    if param_count == 4:
+                        callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
+                    else:
+                        callback(task_id, {}, error_msg)
+                return
+            
+            elif status in [TaskStatus.PENDING, TaskStatus.PROCESSING]:
+                logger.info(f"任务 {task_id} 处理中({int(elapsed)}秒),状态: {status}")
+                time.sleep(self.poll_interval)
+            
+            else:
+                logger.warning(f"任务 {task_id} 未知状态: {status},继续等待...")
+                time.sleep(self.poll_interval)
+    
+    def create_video_task_async(
+        self,
+        prompt: str,
+        image_url: str,
+        gen_params: Optional[str] = None,
+        callback: Optional[Callable] = handle_video_result,
+        output_path: Optional[str] = None,
+        duration: Optional[int] = None,
+        ratio: Optional[str] = None,
+        resolution: Optional[str] = None,
+        watermark: Optional[str] = None,
+        camerafixed: Optional[str] = None,
+        **kwargs
+    ) -> Optional[str]:
+        """
+        创建视频生成任务并立即返回task_id(不阻塞主流程)
+        
+        任务会在后台线程中轮询,完成后调用回调函数。
+        
+        Args:
+            prompt: 视频生成提示词(必填)
+            image_url: 参考图片URL(必填)
+            gen_params: 自定义生成参数字符串(可选,如果提供则忽略其他生成参数)
+            callback: 可选的回调函数,可以是以下两种签名之一:
+                     1. (task_id, result, error) -> None
+                     2. (task_id, output_path, result, error) -> None
+            output_path: 视频输出路径(可选,会传递给回调函数)
+            duration: 视频时长(秒,默认4秒)
+            ratio: 视频比例(默认"16:9")
+            resolution: 视频分辨率(默认"1080p")
+            watermark: 水印开关(默认"false")
+            camerafixed: 相机固定开关(默认"false")
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            任务ID(task_id),如果创建失败则返回None
+        
+        Raises:
+            APIError: 如果创建任务失败
+        """
+        # 创建任务
+        task_response = self.create_video_task(
+            prompt=prompt,
+            image_url=image_url,
+            gen_params=gen_params,
+            duration=duration,
+            ratio=ratio,
+            resolution=resolution,
+            watermark=watermark,
+            camerafixed=camerafixed,
+            **kwargs
+        )
+        
+        task_id = task_response.get("id")
+        if not task_id:
+            logger.error("任务提交失败,无法启动后台轮询")
+            return None
+        
+        logger.info(f"任务提交成功,task_id: {task_id},启动后台轮询...")
+        
+        # 准备回调参数
+        callback_kwargs = {}
+        if output_path:
+            callback_kwargs["output_path"] = output_path
+        
+        # 启动后台线程轮询结果
+        poll_thread = threading.Thread(
+            target=self._background_poll,
+            args=(task_id, callback),
+            kwargs={"callback_kwargs": callback_kwargs},
+            daemon=True  # 守护线程:主程序退出时自动结束
+        )
+        poll_thread.start()
+        
+        return task_id
+    
+    def create_and_wait(
+        self,
+        prompt: str,
+        image_url: str,
+        gen_params: Optional[str] = None,
+        callback: Optional[Callable] = None,
+        output_path: Optional[str] = None,
+        duration: Optional[int] = None,
+        ratio: Optional[str] = None,
+        resolution: Optional[str] = None,
+        watermark: Optional[str] = None,
+        camerafixed: Optional[str] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        创建视频生成任务并等待完成(同步方法,会阻塞)
+        
+        Args:
+            prompt: 视频生成提示词(必填)
+            image_url: 参考图片URL(必填)
+            gen_params: 自定义生成参数字符串(可选,如果提供则忽略其他生成参数)
+            callback: 可选的回调函数,可以是以下两种签名之一:
+                     1. (task_id, result, error) -> None
+                     2. (task_id, output_path, result, error) -> None
+            output_path: 视频输出路径(可选,会传递给回调函数)
+            duration: 视频时长(秒,默认4秒)
+            ratio: 视频比例(默认"16:9")
+            resolution: 视频分辨率(默认"1080p")
+            watermark: 水印开关(默认"false")
+            camerafixed: 相机固定开关(默认"false")
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            任务完成后的结果
+        
+        Raises:
+            APIError: 如果请求失败
+            TimeoutError: 如果任务超时
+        """
+        # 创建任务
+        task_response = self.create_video_task(
+            prompt=prompt,
+            image_url=image_url,
+            gen_params=gen_params,
+            duration=duration,
+            ratio=ratio,
+            resolution=resolution,
+            watermark=watermark,
+            camerafixed=camerafixed,
+            **kwargs
+        )
+        
+        task_id = task_response.get("id")
+        if not task_id:
+            raise APIError("创建任务成功但未返回任务ID")
+        
+        logger.info(f"任务已创建,任务ID: {task_id},开始等待完成...")
+        
+        # 等待任务完成
+        callback_kwargs = {}
+        if output_path:
+            callback_kwargs["output_path"] = output_path
+        
+        return self.wait_for_task(task_id, callback=callback, callback_kwargs=callback_kwargs)
+    
+    def get_video_url(self, result: Dict[str, Any]) -> Optional[str]:
+        """
+        从任务结果中提取视频URL
+        
+        Args:
+            result: 任务结果(从query_video_task或wait_for_task返回)
+        
+        Returns:
+            视频URL,如果不存在则返回None
+        """
+        try:
+            content = result.get("content", {})
+            if isinstance(content, dict):
+                return content.get("video_url")
+            return None
+        except (KeyError, TypeError, AttributeError) as e:
+            logger.warning(f"提取视频URL失败: {e}")
+            return None
+    
+    def get_task_status(self, result: Dict[str, Any]) -> Optional[str]:
+        """
+        从任务结果中提取任务状态
+        
+        Args:
+            result: 任务结果
+        
+        Returns:
+            任务状态字符串,如果不存在则返回None
+        """
+        try:
+            return result.get("status", "").lower()
+        except (KeyError, TypeError, AttributeError):
+            return None
+
+
+if __name__ == "__main__":
+    # 示例用法
+    client = ArkVideoClient()
+    
+    # 方式1:异步创建任务(立即返回task_id,不阻塞主流程)推荐
+    # 使用默认生成参数
+    task_id = client.create_video_task_async(
+        prompt="图中的女生在街道上散步",
+        image_url="https://ark-content-generation-v2-cn-beijing.tos-cn-beijing.volces.com/doubao-seedream-4-5/021766049633300c2c2346a8f7f450117997084af59f03e11d642_0.jpeg?X-Tos-Algorithm=TOS4-HMAC-SHA256&X-Tos-Credential=AKLTYWJkZTExNjA1ZDUyNDc3YzhjNTM5OGIyNjBhNDcyOTQ%2F20251218%2Fcn-beijing%2Ftos%2Frequest&X-Tos-Date=20251218T092054Z&X-Tos-Expires=86400&X-Tos-Signature=a83d797cceaa38226c6489f27892ab9e6651dc7fe84addb37c95fec18358706c&X-Tos-SignedHeaders=host",
+        callback=handle_video_result,
+        output_path="./output/video3.mp4"
+    )
+    
+    # 方式1b:使用自定义生成参数(推荐方式)
+    # task_id = client.create_video_task_async(
+    #     prompt="图中的女生在街道上散步",
+    #     image_url="https://example.com/image.jpg",
+    #     duration=5,
+    #     ratio="9:16",
+    #     resolution="720p",
+    #     callback=handle_video_result,
+    #     output_path="./output/video2.mp4"
+    # )
+    
+    # 方式1c:使用自定义生成参数字符串
+    # task_id = client.create_video_task_async(
+    #     prompt="图中的女生在街道上散步",
+    #     image_url="https://example.com/image.jpg",
+    #     gen_params="--dur 5 --rt 9:16 --rs 720p --wm false --cf false",
+    #     callback=handle_video_result,
+    #     output_path="./output/video2.mp4"
+    # )
+    
+    print(f"任务已提交,task_id: {task_id},主流程继续执行...")
+
+    # 等待视频下载完成(可选)
+    while True:
+        if os.path.exists("./output/video3.mp4"):
+            print(f"视频下载完成,退出循环...")
+            break
+        time.sleep(10)
+        print(f"等待10秒...")
+

+ 512 - 0
api_modules/ark_video_client_async.py

@@ -0,0 +1,512 @@
+"""
+火山引擎ARK视频生成API异步客户端
+封装ARK视频生成API的异步调用,提供类型安全的接口
+"""
+
+import os
+import time
+import asyncio
+from typing import Optional, Dict, Any, Callable, Tuple
+
+from .base_client_async import AsyncAPIClient, APIError
+from .ark_video_client import TaskStatus  # 复用同步版本的枚举
+from taskflow.logger import get_logger
+from taskflow.config import get_config
+from examples.video_create.utils.tools import upload_file_to_tos, download_video
+
+logger = get_logger("api_modules.ark_video_client_async")
+
+
+async def handle_video_result(
+    task_id: str,
+    output_path: str,
+    result: Optional[Dict],
+    error: Optional[str]
+) -> None:
+    """
+    处理视频生成结果的异步回调函数
+    
+    Args:
+        task_id: 任务ID
+        output_path: 视频输出路径
+        result: 任务结果(如果成功)
+        error: 错误信息(如果失败)
+    """
+    if error:
+        logger.info(f"\n任务 {task_id} 处理失败:{error}")
+    else:
+        video_url = result.get("content", {}).get("video_url")
+        if video_url:
+            # 使用 asyncio.to_thread 在后台线程中执行同步的下载函数
+            await asyncio.to_thread(download_video, video_url, output_path)
+            logger.info(f"生成视频已下载:{output_path}")
+        else:
+            logger.warning(f"任务 {task_id} 完成但未获取到视频URL")
+
+
+class AsyncArkVideoClient(AsyncAPIClient):
+    """
+    火山引擎ARK视频生成API异步客户端
+    
+    封装ARK视频生成API的异步调用,提供便捷的接口
+    """
+    
+    DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
+    DEFAULT_ENDPOINT = "/api/v3/contents/generations/tasks"
+    DEFAULT_MODEL = "doubao-seedance-1-0-pro-250528"
+    
+    def __init__(
+        self,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        model: Optional[str] = None,
+        timeout: int = 60,
+        poll_interval: int = 5,
+        max_poll_time: int = 500,
+        **kwargs
+    ):
+        """
+        初始化ARK视频生成API异步客户端
+        
+        Args:
+            api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
+            base_url: API基础URL(默认使用官方URL)
+            model: 模型名称(如果为None,会尝试从配置中获取)
+            timeout: 请求超时时间(秒,默认60秒)
+            poll_interval: 轮询间隔(秒,默认5秒)
+            max_poll_time: 最大轮询总时间(秒,默认500秒)
+            **kwargs: 传递给AsyncAPIClient的其他参数
+        """
+        # 获取API密钥(优先级:参数 > 环境变量 > 配置)
+        if api_key is None:
+            api_key = os.getenv("ARK_API_KEY")
+            if api_key is None:
+                config = get_config()
+                api_key = config.get("api.ark.api_key")
+        
+        if not api_key:
+            raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
+        
+        # 获取base_url(优先级:参数 > 配置 > 默认值)
+        if base_url is None:
+            config = get_config()
+            base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
+        
+        # 获取model(优先级:参数 > 配置 > 默认值)
+        if model is None:
+            config = get_config()
+            model = config.get("api.ark.video_model", self.DEFAULT_MODEL)
+        
+        super().__init__(
+            base_url=base_url,
+            api_key=api_key,
+            timeout=timeout,
+            **kwargs
+        )
+        
+        # 保存视频生成相关配置
+        self.model = model
+        self.poll_interval = poll_interval
+        self.max_poll_time = max_poll_time
+        
+        logger.info(f"ARK视频生成API异步客户端初始化完成,模型: {self.model}")
+    
+    async def create_video_task(
+        self,
+        prompt: str,
+        image_url: str,
+        gen_params: str = "",
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        异步创建视频生成任务
+        
+        Args:
+            prompt: 视频生成提示词(必填)
+            image_url: 参考图片URL(必填,必须是可访问的HTTP/HTTPS URL)
+            gen_params: 额外的生成参数(可选,会追加到prompt后面)
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            API响应数据,包含任务ID等信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        if not prompt or not prompt.strip():
+            raise ValueError("prompt不能为空")
+        
+        if not image_url or not image_url.strip():
+            raise ValueError("image_url不能为空")
+        
+        # # 验证image_url是否为URL格式
+        # if not image_url.startswith(("http://", "https://")):
+        #     raise ValueError(
+        #         f"image_url必须是HTTP/HTTPS URL格式,当前值: {image_url}。"
+        #         "如果是本地文件路径,请先上传到云存储获取URL。"
+        #     )
+
+        image_url = upload_file_to_tos(image_url) if "http" not in image_url else image_url
+        
+        # 构建请求体
+        request_data = {
+            "model": kwargs.get("model", self.model),
+            "content": [
+                {
+                    "type": "text",
+                    "text": prompt + gen_params
+                },
+                {
+                    "type": "image_url",
+                    "image_url": {
+                        "url": image_url
+                    }
+                }
+            ],
+            **{k: v for k, v in kwargs.items() if k != "model"}
+        }
+        
+        logger.info(f"创建异步视频生成任务,模型: {request_data['model']}, 提示词: {prompt[:50]}...")
+        logger.info(f"参考图片: {image_url}")
+        
+        try:
+            response = await self.post(
+                endpoint=self.DEFAULT_ENDPOINT,
+                json=request_data
+            )
+            
+            logger.info(f"视频生成任务创建成功,任务ID: {response.get('id', 'unknown')}")
+            return response
+            
+        except APIError as e:
+            logger.error(f"创建视频生成任务失败: {e}")
+            raise
+    
+    async def query_video_task(self, task_id: str) -> Dict[str, Any]:
+        """
+        异步查询视频生成任务状态
+        
+        Args:
+            task_id: 任务ID(从create_video_task响应中获取)
+        
+        Returns:
+            任务状态详情,包含状态、视频URL等信息
+        
+        Raises:
+            APIError: 如果请求失败
+            ValueError: 如果参数无效
+        """
+        if not task_id or not task_id.strip():
+            raise ValueError("task_id不能为空")
+        
+        query_endpoint = f"{self.DEFAULT_ENDPOINT}/{task_id}"
+        
+        logger.debug(f"查询异步视频生成任务状态,任务ID: {task_id}")
+        
+        try:
+            response = await self.get(endpoint=query_endpoint)
+            
+            status = response.get("status", "").lower()
+            logger.debug(f"任务 {task_id} 状态: {status}")
+            
+            return response
+            
+        except APIError as e:
+            logger.error(f"查询视频生成任务状态失败: {e}")
+            raise
+    
+    async def wait_for_task(
+        self,
+        task_id: str,
+        callback: Optional[Callable[[str, Dict[str, Any], Optional[str]], None]] = None
+    ) -> Dict[str, Any]:
+        """
+        异步等待任务完成(异步轮询)
+        
+        Args:
+            task_id: 任务ID
+            callback: 可选的回调函数,参数为 (task_id, result, error)
+                     注意:回调函数如果是异步的,需要使用asyncio.create_task调用
+        
+        Returns:
+            任务完成后的结果
+        
+        Raises:
+            APIError: 如果请求失败
+            TimeoutError: 如果任务超时
+        """
+        start_time = time.time()
+        
+        while True:
+            elapsed = time.time() - start_time
+            
+            if elapsed > self.max_poll_time:
+                error_msg = f"任务超时(超过 {self.max_poll_time} 秒)"
+                logger.error(f"任务 {task_id} {error_msg}")
+                if callback:
+                    # 如果回调是协程函数,需要特殊处理
+                    if asyncio.iscoroutinefunction(callback):
+                        await callback(task_id, {}, error_msg)
+                    else:
+                        callback(task_id, {}, error_msg)
+                raise TimeoutError(error_msg)
+            
+            # 查询任务状态
+            result = await self.query_video_task(task_id)
+            
+            if not result:
+                logger.warning(f"任务 {task_id} 查询结果为空,继续等待...")
+                await asyncio.sleep(self.poll_interval)
+                continue
+            
+            # 解析状态
+            status = result.get("status", "").lower()
+            
+            if status == TaskStatus.SUCCEEDED:
+                logger.info(f"任务 {task_id} 完成,耗时: {int(elapsed)}秒")
+                if callback:
+                    if asyncio.iscoroutinefunction(callback):
+                        await callback(task_id, result, None)
+                    else:
+                        callback(task_id, result, None)
+                return result
+            
+            elif status == TaskStatus.FAILED:
+                error_msg = result.get("error", {}).get("message", "未知错误")
+                logger.error(f"任务 {task_id} 失败: {error_msg}")
+                if callback:
+                    if asyncio.iscoroutinefunction(callback):
+                        await callback(task_id, {}, error_msg)
+                    else:
+                        callback(task_id, {}, error_msg)
+                raise APIError(f"任务失败: {error_msg}")
+            
+            elif status in [TaskStatus.PENDING, TaskStatus.PROCESSING]:
+                logger.info(f"任务 {task_id} 处理中({int(elapsed)}秒),状态: {status}")
+                await asyncio.sleep(self.poll_interval)
+            
+            else:
+                logger.warning(f"任务 {task_id} 未知状态: {status},继续等待...")
+                await asyncio.sleep(self.poll_interval)
+    
+    async def create_and_wait(
+        self,
+        prompt: str,
+        image_url: str,
+        gen_params: str = "",
+        callback: Optional[Callable[[str, Dict[str, Any], Optional[str]], None]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        异步创建视频生成任务并等待完成(便捷方法)
+        
+        Args:
+            prompt: 视频生成提示词(必填)
+            image_url: 参考图片URL(必填)
+            gen_params: 额外的生成参数(可选)
+            callback: 可选的回调函数,参数为 (task_id, result, error)
+            **kwargs: 其他请求参数
+        
+        Returns:
+            任务完成后的结果
+        
+        Raises:
+            APIError: 如果请求失败
+            TimeoutError: 如果任务超时
+        """
+        # 创建任务
+        task_response = await self.create_video_task(
+            prompt=prompt,
+            image_url=image_url,
+            gen_params=gen_params,
+            **kwargs
+        )
+        
+        task_id = task_response.get("id")
+        if not task_id:
+            raise APIError("创建任务成功但未返回任务ID")
+        
+        logger.info(f"任务已创建,任务ID: {task_id},开始等待完成...")
+        
+        # 等待任务完成
+        return await self.wait_for_task(task_id, callback=callback)
+    
+    async def create_video_task_async(
+        self,
+        prompt: str,
+        image_url: str,
+        gen_params: str = "",
+        callback: Optional[Callable] = handle_video_result,
+        output_path: Optional[str] = None,
+        **kwargs
+    ) -> Tuple[Optional[str], Optional[asyncio.Task]]:
+        """
+        创建视频生成任务并立即返回task_id和后台任务对象(不阻塞主流程)
+        
+        任务会在后台异步任务中轮询,完成后调用回调函数。
+        调用者可以通过返回的任务对象等待任务完成。
+        
+        Args:
+            prompt: 视频生成提示词(必填)
+            image_url: 参考图片URL(必填)
+            gen_params: 额外的生成参数(可选,会追加到prompt后面)
+            callback: 可选的回调函数,可以是以下两种签名之一:
+                     1. (task_id, result, error) -> None
+                     2. (task_id, output_path, result, error) -> None
+                     注意:如果是异步函数,需要使用 async def 定义
+            output_path: 视频输出路径(可选,会传递给回调函数)
+            **kwargs: 其他请求参数(会覆盖默认配置)
+        
+        Returns:
+            元组 (task_id, background_task):
+            - task_id: 任务ID,如果创建失败则返回None
+            - background_task: 后台异步任务对象,可以用于等待任务完成
+                              如果创建失败则返回None
+        
+        Raises:
+            APIError: 如果创建任务失败
+        """
+        # 创建任务
+        task_response = await self.create_video_task(
+            prompt=prompt,
+            image_url=image_url,
+            gen_params=gen_params,
+            **kwargs
+        )
+        
+        task_id = task_response.get("id")
+        if not task_id:
+            logger.error("任务提交失败,无法启动后台轮询")
+            return None, None
+        
+        logger.info(f"任务提交成功,task_id: {task_id},启动后台异步轮询...")
+        
+        # 定义后台异步任务包装函数
+        async def _background_wait():
+            """后台异步任务:等待任务完成并调用回调"""
+            try:
+                # 等待任务完成
+                result = await self.wait_for_task(task_id)
+                
+                # 调用回调函数
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    
+                    if asyncio.iscoroutinefunction(callback):
+                        # 异步回调函数
+                        if param_count == 4:
+                            await callback(task_id, output_path or "", result, None)
+                        else:
+                            await callback(task_id, result, None)
+                    else:
+                        # 同步回调函数
+                        if param_count == 4:
+                            # 4参数版本:(task_id, output_path, result, error)
+                            callback(task_id, output_path or "", result, None)
+                        else:
+                            # 3参数版本:(task_id, result, error)
+                            callback(task_id, result, None)
+            except Exception as e:
+                error_msg = str(e)
+                logger.error(f"后台异步任务处理失败: {error_msg}")
+                if callback:
+                    import inspect
+                    sig = inspect.signature(callback)
+                    param_count = len(sig.parameters)
+                    
+                    if asyncio.iscoroutinefunction(callback):
+                        if param_count == 4:
+                            await callback(task_id, output_path or "", {}, error_msg)
+                        else:
+                            await callback(task_id, {}, error_msg)
+                    else:
+                        if param_count == 4:
+                            callback(task_id, output_path or "", {}, error_msg)
+                        else:
+                            callback(task_id, {}, error_msg)
+        
+        # 启动后台异步任务并返回任务对象,以便调用者可以等待
+        background_task = asyncio.create_task(_background_wait())
+        
+        # 返回任务ID和任务对象(使用元组)
+        return task_id, background_task
+    
+    def get_video_url(self, result: Dict[str, Any]) -> Optional[str]:
+        """
+        从任务结果中提取视频URL
+        
+        Args:
+            result: 任务结果(从query_video_task或wait_for_task返回)
+        
+        Returns:
+            视频URL,如果不存在则返回None
+        """
+        try:
+            content = result.get("content", {})
+            if isinstance(content, dict):
+                return content.get("video_url")
+            return None
+        except (KeyError, TypeError, AttributeError) as e:
+            logger.warning(f"提取视频URL失败: {e}")
+            return None
+    
+    def get_task_status(self, result: Dict[str, Any]) -> Optional[str]:
+        """
+        从任务结果中提取任务状态
+        
+        Args:
+            result: 任务结果
+        
+        Returns:
+            任务状态字符串,如果不存在则返回None
+        """
+        try:
+            return result.get("status", "").lower()
+        except (KeyError, TypeError, AttributeError):
+            return None
+
+
+async def main():
+    # 示例用法
+    async with AsyncArkVideoClient() as client:
+        # 方式1:创建任务并等待完成(阻塞)
+        try:
+            result = await client.create_and_wait(
+                prompt="图中的女生在街道上散步",
+                image_url="https://example.com/image.jpg",  # 必须是可访问的URL
+                gen_params=" --dur 4"
+            )
+            video_url = client.get_video_url(result)
+            print(f"视频生成成功,URL: {video_url}")
+        except Exception as e:
+            print(f"视频生成失败: {e}")
+        
+        # 方式2:异步创建任务(不阻塞,后台处理)
+        # task_id = await client.create_video_task_async(
+        #     prompt="图中的女生在街道上散步",
+        #     image_url="https://example.com/image.jpg",
+        #     gen_params=" --dur 4",
+        #     callback=handle_video_result,
+        #     output_path="./output/video.mp4"
+        # )
+        # print(f"任务已提交,task_id: {task_id},主流程继续执行...")
+        # # 主流程可以继续执行其他操作
+        # await asyncio.sleep(10)  # 等待一段时间
+        
+        # 方式3:创建任务后手动查询
+        # task_response = await client.create_video_task(
+        #     prompt="图中的女生在街道上散步",
+        #     image_url="https://example.com/image.jpg",
+        #     gen_params=" --dur 4"
+        # )
+        # task_id = task_response.get("id")
+        # result = await client.wait_for_task(task_id)
+        # video_url = client.get_video_url(result)
+        # print(f"视频URL: {video_url}")
+
+if __name__ == "__main__":
+    asyncio.run(main())
+

+ 268 - 0
api_modules/base_client.py

@@ -0,0 +1,268 @@
+"""
+API客户端基类
+提供通用API调用功能,包括错误处理、重试机制、日志记录等
+"""
+
+import time
+import logging
+from typing import Any, Dict, Optional, Callable
+from dataclasses import dataclass
+import requests
+from requests.adapters import HTTPAdapter
+from urllib3.util import Retry
+
+from taskflow.logger import get_logger
+
+logger = get_logger("api_modules.base_client")
+
+@dataclass 
+class RetryConfig:
+    """
+    重试配置类
+
+    该类用于配置 API 请求的重试机制,包括最大重试次数、退避因子、需要重试的 HTTP 状态码及需要重试的异常类型。
+
+    属性:
+        max_retries (int): 最大重试次数,默认为 3。当请求失败达到该次数后不再重试。
+        backoff_factor (float): 退避因子,控制每次重试间的等待时长。默认为 1.0。
+        retry_on_status (tuple): 需要进行重试的 HTTP 状态码,默认为 (500, 502, 503, 504)。
+        retry_on_exception (tuple): 需要重试的异常类型,例如连接超时或连接错误,默认为 (requests.exceptions.ConnectionError, requests.exceptions.Timeout)。
+    """
+    max_retries: int = 3
+    backoff_factor: float = 1.0
+    retry_on_status: tuple = (500, 502, 503, 504)
+    retry_on_exception: tuple = (requests.exceptions.ConnectionError, requests.exceptions.Timeout)
+
+class APIError(Exception):
+    """API调用异常"""
+    
+    def __init__(self, message: str, status_code: Optional[int] = None, response: Optional[Dict] = None):
+        """
+        初始化API错误
+        
+        Args:
+            message: 错误消息
+            status_code: HTTP状态码
+            response: 响应内容
+        """
+        super().__init__(message)
+        self.message = message
+        self.status_code = status_code
+        self.response = response
+    
+    def __str__(self):
+        if self.status_code:
+            return f"{self.message} (Status: {self.status_code})"
+        return self.message
+
+
+class APIClient:
+    """
+    API客户端基类
+    
+    提供通用的API调用功能:
+    - 统一的请求接口
+    - 自动重试机制
+    - 错误处理
+    - 日志记录
+    - 超时控制
+    
+    使用示例:
+        >>> client = APIClient(base_url="https://api.example.com", api_key="your_key")
+        >>> response = client.post("/endpoint", json={"data": "value"})
+    """
+
+    def __init__(
+        self,
+        base_url: str,
+        api_key: Optional[str] = None,
+        timeout: int = 300,
+        retry_config: Optional[RetryConfig] = None,
+        headers: Optional[Dict[str, str]] = None
+    ):
+        """
+        初始化API客户端
+        
+        Args:
+            base_url: API基础URL
+            api_key: API密钥(可选,也可以通过headers传入)
+            timeout: 请求超时时间(秒)
+            retry_config: 重试配置
+            headers: 默认请求头
+        """
+        self.base_url = base_url.rstrip('/')
+        self.api_key = api_key
+        self.timeout = timeout
+        self.retry_config = retry_config or RetryConfig()
+        
+        # 设置默认请求头
+        self.default_headers = {
+            "Content-Type": "application/json",
+            **({} if headers is None else headers)
+        }
+        
+        if api_key:
+            self.default_headers["Authorization"] = f"Bearer {api_key}"
+        
+        # 创建session并配置重试
+        self.session = requests.Session()
+        self._setup_retry()
+        
+        logger.info(f"初始化API客户端: {self.base_url}")
+
+    def _setup_retry(self):
+        """配置重试机制"""
+        retry = Retry(
+            total=self.retry_config.max_retries,
+            backoff_factor=self.retry_config.backoff_factor,
+            status_forcelist=self.retry_config.retry_on_status,
+            raise_on_status=False,
+        )
+        adapter = HTTPAdapter(max_retries=retry)
+        self.session.mount('https://', adapter)
+        self.session.mount('http://', adapter)
+
+    def _build_url(self, endpoint: str) -> str:
+        """
+        构建完整的URL
+        
+        Args:
+            endpoint: API端点路径
+        
+        Returns:
+            完整的URL
+        """
+        endpoint = endpoint.lstrip('/')
+        return f"{self.base_url}/{endpoint}"
+
+    def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
+        """
+        处理API响应
+        
+        Args:
+            response: requests响应对象
+        
+        Returns:
+            解析后的响应数据
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        try:
+            response.raise_for_status()
+        except requests.exceptions.HTTPError as e:
+            # 尝试解析错误响应
+            error_detail = None
+            try:
+                error_detail = response.json()
+            except:
+                error_detail = response.text
+
+            raise APIError(
+                message=f"API请求失败:{str(e)}",
+                status_code=response.status_code,
+                response=error_detail
+            )
+        
+        # 解析响应数据
+        try:
+            return response.json()
+        except ValueError:
+            return {"content": response.text}
+
+    def _log_request(self, method: str, url: str, **kwargs):
+        """记录请求日志"""
+        logger.debug(f"{method} {url}")
+        if "json" in kwargs:
+            logger.debug(f"请求体: {kwargs['json']}")
+
+    def _log_response(self, response: requests.Response):
+        """记录响应日志"""
+        logger.debug(f"响应状态: {response.status_code}")
+        try:
+            logger.debug(f"响应体: {response.json()}")
+        except:
+            logger.debug(f"响应体: {response.text[:200]}")
+
+    def request(
+        self,
+        method: str,
+        endpoint: str,
+        headers: Optional[Dict[str, str]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        发送API请求
+        
+        Args:
+            method: HTTP方法(GET, POST, PUT, DELETE等)
+            endpoint: API端点路径
+            headers: 额外的请求头(会与默认请求头合并)
+            **kwargs: 传递给requests的其他参数
+        
+        Returns:
+            API响应数据
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        url = self._build_url(endpoint)
+
+        # 合并请求头
+        request_headers = {**self.default_headers}
+        if headers:
+            request_headers.update(headers)
+
+        # 记录请求
+        self._log_request(method, url, **kwargs)
+
+        try:
+            response = self.session.request(
+                method=method,
+                url=url,
+                headers=request_headers,
+                timeout=self.timeout,
+                **kwargs
+            )
+
+            # 记录响应
+            self._log_response(response)
+
+            return self._handle_response(response)
+
+        except requests.exceptions.RequestException as e:
+            logger.error(f"请求异常: {e}")
+            raise APIError(f"网络请求失败: {str(e)}")
+
+    def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送GET请求"""
+        return self.request("GET", endpoint, **kwargs)
+    
+    def post(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送POST请求"""
+        return self.request("POST", endpoint, **kwargs)
+    
+    def put(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送PUT请求"""
+        return self.request("PUT", endpoint, **kwargs)
+    
+    def delete(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送DELETE请求"""
+        return self.request("DELETE", endpoint, **kwargs)
+    
+    def patch(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送PATCH请求"""
+        return self.request("PATCH", endpoint, **kwargs)
+    
+    def close(self):
+        """关闭session"""
+        self.session.close()
+        logger.info("API客户端已关闭")
+    
+    def __enter__(self):
+        """上下文管理器入口"""
+        return self
+    
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """上下文管理器出口"""
+        self.close()

+ 321 - 0
api_modules/base_client_async.py

@@ -0,0 +1,321 @@
+"""
+异步API客户端基类
+提供通用异步API调用功能,包括错误处理、重试机制、日志记录等
+使用 aiohttp 实现异步HTTP请求
+"""
+
+import asyncio
+import logging
+from typing import Any, Dict, Optional, Callable
+from dataclasses import dataclass
+import aiohttp
+from aiohttp import ClientSession, ClientTimeout
+
+from taskflow.logger import get_logger
+
+logger = get_logger("api_modules.base_client_async")
+
+@dataclass 
+class RetryConfig:
+    """
+    重试配置类
+
+    该类用于配置 API 请求的重试机制,包括最大重试次数、退避因子、需要重试的 HTTP 状态码及需要重试的异常类型。
+
+    属性:
+        max_retries (int): 最大重试次数,默认为 3。当请求失败达到该次数后不再重试。
+        backoff_factor (float): 退避因子,控制每次重试间的等待时长。默认为 1.0。
+        retry_on_status (tuple): 需要进行重试的 HTTP 状态码,默认为 (500, 502, 503, 504)。
+        retry_on_exception (tuple): 需要重试的异常类型,例如连接超时或连接错误,默认为 (aiohttp.ClientError, asyncio.TimeoutError)。
+    """
+    max_retries: int = 3
+    backoff_factor: float = 1.0
+    retry_on_status: tuple = (500, 502, 503, 504)
+    retry_on_exception: tuple = (aiohttp.ClientError, asyncio.TimeoutError)
+
+class APIError(Exception):
+    """API调用异常"""
+    
+    def __init__(self, message: str, status_code: Optional[int] = None, response: Optional[Dict] = None):
+        """
+        初始化API错误
+        
+        Args:
+            message: 错误消息
+            status_code: HTTP状态码
+            response: 响应内容
+        """
+        super().__init__(message)
+        self.message = message
+        self.status_code = status_code
+        self.response = response
+    
+    def __str__(self):
+        if self.status_code:
+            return f"{self.message} (Status: {self.status_code})"
+        return self.message
+
+
+class AsyncAPIClient:
+    """
+    异步API客户端基类
+    
+    提供通用的异步API调用功能:
+    - 统一的请求接口
+    - 自动重试机制
+    - 错误处理
+    - 日志记录
+    - 超时控制
+    
+    使用示例:
+        >>> async with AsyncAPIClient(base_url="https://api.example.com", api_key="your_key") as client:
+        ...     response = await client.post("/endpoint", json={"data": "value"})
+    """
+
+    def __init__(
+        self,
+        base_url: str,
+        api_key: Optional[str] = None,
+        timeout: int = 300,
+        retry_config: Optional[RetryConfig] = None,
+        headers: Optional[Dict[str, str]] = None
+    ):
+        """
+        初始化异步API客户端
+        
+        Args:
+            base_url: API基础URL
+            api_key: API密钥(可选,也可以通过headers传入)
+            timeout: 请求超时时间(秒)
+            retry_config: 重试配置
+            headers: 默认请求头
+        """
+        self.base_url = base_url.rstrip('/')
+        self.api_key = api_key
+        self.timeout = timeout
+        self.retry_config = retry_config or RetryConfig()
+        
+        # 设置默认请求头
+        self.default_headers = {
+            "Content-Type": "application/json",
+            **({} if headers is None else headers)
+        }
+        
+        if api_key:
+            self.default_headers["Authorization"] = f"Bearer {api_key}"
+        
+        # 创建session(在异步上下文中创建)
+        self._session: Optional[ClientSession] = None
+        
+        logger.info(f"初始化异步API客户端: {self.base_url}")
+
+    async def _get_session(self) -> ClientSession:
+        """获取或创建session"""
+        if self._session is None or self._session.closed:
+            timeout = ClientTimeout(total=self.timeout)
+            self._session = ClientSession(
+                timeout=timeout,
+                headers=self.default_headers
+            )
+        return self._session
+
+    def _build_url(self, endpoint: str) -> str:
+        """
+        构建完整的URL
+        
+        Args:
+            endpoint: API端点路径
+        
+        Returns:
+            完整的URL
+        """
+        endpoint = endpoint.lstrip('/')
+        return f"{self.base_url}/{endpoint}"
+
+    async def _handle_response(self, response: aiohttp.ClientResponse) -> Dict[str, Any]:
+        """
+        处理API响应
+        
+        Args:
+            response: aiohttp响应对象
+        
+        Returns:
+            解析后的响应数据
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        try:
+            response.raise_for_status()
+        except aiohttp.ClientResponseError as e:
+            # 尝试解析错误响应
+            error_detail = None
+            try:
+                error_detail = await response.json()
+                logger.error(f"API错误响应 (JSON): {error_detail}")
+            except:
+                try:
+                    error_detail = await response.text()
+                    logger.error(f"API错误响应 (Text): {error_detail}")
+                except:
+                    error_detail = str(e)
+                    logger.error(f"API错误响应 (String): {error_detail}")
+
+            raise APIError(
+                message=f"API请求失败:{str(e)}",
+                status_code=response.status,
+                response=error_detail
+            )
+        
+        # 解析响应数据
+        try:
+            return await response.json()
+        except aiohttp.ContentTypeError:
+            text = await response.text()
+            return {"content": text}
+
+    def _log_request(self, method: str, url: str, **kwargs):
+        """记录请求日志"""
+        logger.debug(f"{method} {url}")
+        if "json" in kwargs:
+            logger.debug(f"请求体: {kwargs['json']}")
+
+    def _log_response(self, status: int, response_data: Any = None):
+        """记录响应日志"""
+        logger.debug(f"响应状态: {status}")
+        if response_data:
+            logger.debug(f"响应体: {response_data}")
+
+    async def _request_with_retry(
+        self,
+        method: str,
+        url: str,
+        headers: Optional[Dict[str, str]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        带重试机制的请求
+        
+        Args:
+            method: HTTP方法
+            url: 完整URL
+            headers: 额外的请求头
+            **kwargs: 传递给aiohttp的其他参数
+        
+        Returns:
+            API响应数据
+        """
+        session = await self._get_session()
+        
+        # 合并请求头
+        request_headers = {**self.default_headers}
+        if headers:
+            request_headers.update(headers)
+        
+        # 记录请求
+        self._log_request(method, url, **kwargs)
+        
+        last_exception = None
+        
+        for attempt in range(self.retry_config.max_retries + 1):
+            try:
+                async with session.request(
+                    method=method,
+                    url=url,
+                    headers=request_headers,
+                    **kwargs
+                ) as response:
+                    response_data = await self._handle_response(response)
+                    self._log_response(response.status, response_data)
+                    return response_data
+                    
+            except Exception as e:
+                last_exception = e
+                
+                # 检查是否需要重试
+                should_retry = False
+                
+                # 检查状态码
+                if isinstance(e, APIError) and e.status_code:
+                    if e.status_code in self.retry_config.retry_on_status:
+                        should_retry = True
+                
+                # 检查异常类型
+                if isinstance(e, self.retry_config.retry_on_exception):
+                    should_retry = True
+                
+                # 如果不需要重试或已达到最大重试次数,直接抛出异常
+                if not should_retry or attempt >= self.retry_config.max_retries:
+                    break
+                
+                # 计算退避时间
+                wait_time = self.retry_config.backoff_factor * (2 ** attempt)
+                logger.warning(f"请求失败,{wait_time}秒后重试 (尝试 {attempt + 1}/{self.retry_config.max_retries + 1}): {e}")
+                await asyncio.sleep(wait_time)
+        
+        # 所有重试都失败
+        logger.error(f"请求异常: {last_exception}")
+        if isinstance(last_exception, APIError):
+            raise last_exception
+        raise APIError(f"网络请求失败: {str(last_exception)}")
+
+    async def request(
+        self,
+        method: str,
+        endpoint: str,
+        headers: Optional[Dict[str, str]] = None,
+        **kwargs
+    ) -> Dict[str, Any]:
+        """
+        发送异步API请求
+        
+        Args:
+            method: HTTP方法(GET, POST, PUT, DELETE等)
+            endpoint: API端点路径
+            headers: 额外的请求头(会与默认请求头合并)
+            **kwargs: 传递给aiohttp的其他参数
+        
+        Returns:
+            API响应数据
+        
+        Raises:
+            APIError: 如果请求失败
+        """
+        url = self._build_url(endpoint)
+        return await self._request_with_retry(method, url, headers, **kwargs)
+
+    async def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送异步GET请求"""
+        return await self.request("GET", endpoint, **kwargs)
+    
+    async def post(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送异步POST请求"""
+        return await self.request("POST", endpoint, **kwargs)
+    
+    async def put(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送异步PUT请求"""
+        return await self.request("PUT", endpoint, **kwargs)
+    
+    async def delete(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送异步DELETE请求"""
+        return await self.request("DELETE", endpoint, **kwargs)
+    
+    async def patch(self, endpoint: str, **kwargs) -> Dict[str, Any]:
+        """发送异步PATCH请求"""
+        return await self.request("PATCH", endpoint, **kwargs)
+    
+    async def close(self):
+        """关闭session"""
+        if self._session and not self._session.closed:
+            await self._session.close()
+            logger.info("异步API客户端已关闭")
+    
+    async def __aenter__(self):
+        """异步上下文管理器入口"""
+        await self._get_session()
+        return self
+    
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        """异步上下文管理器出口"""
+        await self.close()
+

+ 294 - 0
api_modules/example.py

@@ -0,0 +1,294 @@
+"""
+调用火山引擎ARK API的示例
+"""
+
+from api_modules.ark_client import ArkClient, ArkMessage, APIError
+from taskflow.logger import get_logger
+
+logger = get_logger("api_modules.example")
+
+def example_basic_chat():
+    """示例1:基本文本对话"""
+    logger.info("=" * 50)
+    logger.info("示例1:基本文本对话")
+    logger.info("=" * 50)
+    
+    # 初始化客户端(API密钥可以从环境变量或配置文件获取)
+    client = ArkClient()
+    
+    # 创建用户消息
+    message = ArkMessage(role="user")
+    message.add_text("你好,请介绍一下你自己")
+    
+    try:
+        # 发送聊天请求
+        response = client.chat(
+            messages=[message]
+        )
+        
+        logger.info("响应:", response)
+        
+        # 提取文本响应
+        text = client.get_response_text(response)
+        if text:
+            logger.info(f"AI回复: {text}")
+            
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+    except Exception as e:
+        logger.error(f"其他错误: {e}")
+
+
+def example_chat_with_system_prompt():
+    """示例2:使用系统提示词"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例2:使用系统提示词")
+    logger.info("=" * 50)
+    
+    client = ArkClient()
+    
+    # 创建用户消息
+    user_message = ArkMessage(role="user")
+    user_message.add_text("请帮我写一首关于春天的诗")
+    
+    try:
+        # 使用字符串形式的系统提示词
+        response = client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt="你是一位专业的诗人,擅长写优美的诗歌"
+        )
+        
+        logger.info("响应:", response)
+        text = client.get_response_text(response)
+        if text:
+            logger.info(f"AI回复: {text}")
+            
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+
+
+def example_multimodal_chat():
+    """示例3:多模态对话(文本+图片)"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例3:多模态对话(文本+图片)")
+    logger.info("=" * 50)
+    
+    client = ArkClient()
+    
+    # 创建包含图片和文本的用户消息
+    user_message = ArkMessage(role="user")
+    user_message.add_text("图片1中是否含有目标人物")
+    user_message.add_image("https://ark-project.tos-cn-beijing.volces.com/doc_image/scene_01.png")
+    user_message.add_text("图片2中是否含有目标人物")
+    user_message.add_image("https://ark-project.tos-cn-beijing.volces.com/doc_image/scene_02.png")
+    
+    # 创建系统提示词(也支持多模态)
+    system_message = ArkMessage(role="system")
+    system_message.add_text("下面人物是目标人物")
+    system_message.add_image("https://ark-project.tos-cn-beijing.volces.com/doc_image/target.png")
+    system_message.add_text("请确认下面图片中是否含有目标人物")
+    
+    try:
+        response = client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_message
+        )
+        
+        logger.info("响应:", response)
+        text = client.get_response_text(response)
+        if text:
+            logger.info(f"AI回复: {text}")
+            
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+
+
+def example_chat_simple():
+    """示例4:使用简化的chat_simple接口"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例4:使用简化的chat_simple接口")
+    logger.info("=" * 50)
+    
+    client = ArkClient()
+    
+    try:
+        # 仅文本
+        response = client.chat_simple(
+            model="doubao-seed-1-6-251015",
+            text="今天天气怎么样?"
+        )
+        logger.info("仅文本响应:", response)
+        
+        # 文本+图片
+        response = client.chat_simple(
+            model="doubao-seed-1-6-251015",
+            text="你看见了什么?",
+            image_url="https://ark-project.tos-cn-beijing.volces.com/doc_image/scene_01.png"
+        )
+        logger.info("文本+图片响应:", response)
+        
+        # 带系统提示词
+        response = client.chat_simple(
+            model="doubao-seed-1-6-251015",
+            text="分析这张图片",
+            image_url="https://example.com/image.png",
+            system_prompt="你是一个专业的图像分析助手"
+        )
+        logger.info("带系统提示词响应:", response)
+        
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+
+
+def example_multiple_messages():
+    """示例5:多轮对话"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例5:多轮对话")
+    logger.info("=" * 50)
+    
+    client = ArkClient()
+    
+    # 创建多轮对话消息
+    messages = [
+        ArkMessage(role="user"),
+        ArkMessage(role="user"),
+        ArkMessage(role="user")
+    ]
+    
+    messages[0].add_text("我的名字是张三")
+    messages[1].add_text("我今年25岁")
+    messages[2].add_text("请记住我的信息")
+    
+    try:
+        response = client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=messages,
+            system_prompt="你是一个友好的助手,会记住用户提供的信息"
+        )
+        
+        print('xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
+        print(response)
+        text = client.get_response_text(response)
+        if text:
+            logger.info(f"AI回复: {text}")
+            
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+
+
+def example_dict_format():
+    """示例6:使用字典格式的消息"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例6:使用字典格式的消息")
+    logger.info("=" * 50)
+    
+    client = ArkClient()
+    
+    # 使用字典格式的消息(直接对应API格式)
+    messages = [
+        {
+            "role": "system",
+            "content": [
+                {"type": "input_text", "text": "你是一个专业的助手"}
+            ]
+        },
+        {
+            "role": "user",
+            "content": [
+                {"type": "input_text", "text": "你好"},
+                {
+                    "type": "input_image",
+                    "image_url": "https://example.com/image.png"
+                }
+            ]
+        }
+    ]
+    
+    try:
+        response = client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=messages
+        )
+        
+        logger.info("响应:", response)
+        
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+
+
+def example_error_handling():
+    """示例7:错误处理"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例7:错误处理")
+    logger.info("=" * 50)
+    
+    try:
+        # 使用无效的API密钥
+        client = ArkClient()
+        
+        message = ArkMessage(role="user")
+        message.add_text("测试")
+        
+        response = client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[message]
+        )
+        
+    except APIError as e:
+        logger.error(f"捕获到API错误:")
+        logger.error(f"  消息: {e.message}")
+        logger.error(f"  状态码: {e.status_code}")
+        logger.error(f"  响应: {e.response}")
+    except ValueError as e:
+        logger.error(f"参数错误: {e}")
+    except Exception as e:
+        logger.error(f"其他错误: {e}")
+
+
+def example_context_manager():
+    """示例8:使用上下文管理器"""
+    logger.info("\n" + "=" * 50)
+    logger.info("示例8:使用上下文管理器")
+    logger.info("=" * 50)
+    
+    # 使用with语句自动管理资源
+    with ArkClient() as client:
+        message = ArkMessage(role="user")
+        message.add_text("你好")
+        
+        try:
+            response = client.chat(
+                model="doubao-seed-1-6-251015",
+                messages=[message]
+            )
+            logger.info("响应:", response)
+        except APIError as e:
+            logger.error(f"API错误: {e}")
+    # 客户端会自动关闭
+
+
+if __name__ == "__main__":
+    # 注意:运行前请设置正确的API密钥
+    # 方式1:通过环境变量设置
+    # import os
+    # os.environ["ARK_API_KEY"] = "your_api_key_here"
+    
+    # 方式2:直接在代码中传入(不推荐用于生产环境)
+    # client = ArkClient(api_key="your_api_key_here")
+    
+    logger.info("火山引擎ARK API使用示例")
+    logger.info("注意:请先设置正确的API密钥才能运行这些示例\n")
+    
+    # 取消注释以运行相应的示例
+    # example_basic_chat()
+    # example_chat_with_system_prompt()
+    # example_multimodal_chat()
+    # example_chat_simple()
+    example_multiple_messages()
+    # example_dict_format()
+    # example_error_handling()
+    # example_context_manager()
+    
+    logger.info("\n提示:取消注释上面的示例函数调用来运行示例")

+ 4 - 0
config/refer_images.json

@@ -0,0 +1,4 @@
+{
+    "林小星": ["data/image/006.jpg"],
+    "林晓": ["data/image/010.jpg"]
+}

+ 38 - 0
config/taskflow_config.example.json

@@ -0,0 +1,38 @@
+{
+    "task": {
+      "state_file": "task_state.json",
+      "cache_dir": "task_cache",
+      "auto_save": true,
+      "save_interval": 1
+    },
+    "logging": {
+      "level": "INFO",
+      "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+      "date_format": "%Y-%m-%d %H:%M:%S",
+      "console_output": true,
+      "file_output": true,
+      "log_file": "./logs/taskflow.log"
+    },
+    "run": {
+      "base_output_dir": "output",
+      "run_id_format": "run_{timestamp}",
+      "timestamp_format": "%Y%m%d_%H%M%S"
+    },
+    "io": {
+      "encoding": "utf-8",
+      "json_indent": 2,
+      "create_dirs": true
+    },
+    "api": {
+      "ark": {
+        "api_key": "33f28b34-65dc-489c-b825-d6e1aa59fc94",
+        "base_url": "https://ark.cn-beijing.volces.com",
+        "model": "doubao-seed-1-6-251015",
+        "image_model": "doubao-seedream-4-5-251128",
+        "video_model": "doubao-seedance-1-0-pro-250528",
+        "timeout": 300
+      }
+    }
+  }
+  
+  

+ 3 - 0
data.toon

@@ -0,0 +1,3 @@
+users[2,]{name,age,city}:
+  John,30,New York
+  Jane,25,Los Angeles

BIN
data/image/006.jpg


BIN
data/image/010.jpg


BIN
data/image/cloth.jpg


BIN
data/image/cloth1.jpg


BIN
data/image/cloth2.jpg


BIN
data/image/cloth3.jpg


BIN
data/image/cloth4.jpg


BIN
data/image/face.jpg


BIN
data/image/yi1.png


BIN
data/image/yi2.png


BIN
data/image/yi3.png


+ 10 - 0
data/input/sample_text.txt

@@ -0,0 +1,10 @@
+TaskFlow is a powerful Python framework for managing multi-step workflows.
+    It supports checkpoint and resume functionality, allowing you to continue 
+    from where you left off if a process is interrupted.
+    
+    The framework provides features like step dependency management, output caching,
+    and automatic state persistence. You can easily register steps, define dependencies,
+    and execute complex workflows with confidence.
+    
+    With TaskFlow, you can build robust data processing pipelines, batch jobs,
+    and any other multi-step processes that require reliability and resumability.

BIN
data/video/video1.mp4


BIN
data/video/video2.mp4


BIN
data/video/video3.mp4


+ 0 - 0
examples/__init__.py


BIN
examples/__pycache__/__init__.cpython-310.pyc


BIN
examples/__pycache__/__init__.cpython-312.pyc


+ 0 - 0
examples/refer_video_create/__init__.py


BIN
examples/refer_video_create/__pycache__/__init__.cpython-310.pyc


BIN
examples/refer_video_create/__pycache__/__init__.cpython-312.pyc


BIN
examples/refer_video_create/__pycache__/main.cpython-310.pyc


+ 290 - 0
examples/refer_video_create/main.py

@@ -0,0 +1,290 @@
+"""
+refer_video_create 主程序
+基于参考视频创建视频的完整任务流
+包括:
+1. 从参考视频创建脚本
+2. 优化脚本提示词(生图提示词和生视频提示词)
+3. 基于image_prompt生成分镜
+4. 基于video_prompt和分镜生成视频
+5. 拼接所有视频片段
+"""
+
+import time
+import argparse
+import asyncio
+import logging
+from pathlib import Path
+
+from taskflow import TaskManager, FileIOHandler, RunManager
+from taskflow import setup_logger
+from .pipeline.refer_video_create_pipeline import ReferVideoCreatePipeline
+
+logger = setup_logger("examples.refer_video_create.main", level=logging.INFO)
+
+def main():
+    """主程序"""
+    start_time = time.time()
+    logger.info("=== refer_video_create 示例 ===\n")
+
+    # 解析命令行参数
+    parser = argparse.ArgumentParser(description="refer_video_create 主流程")
+    parser.add_argument("--video-url", type=str, required=True, help="参考视频URL或路径")
+    parser.add_argument("--user-prompt", type=str, required=False, default=None, help="用户提示词(可选)")
+    parser.add_argument("--size", type=str, default="1440x2560", help="生成分镜图片的尺寸(默认: 1440x2560)")
+    parser.add_argument(
+        "--refer-image",
+        nargs="*",
+        default=None,
+        help="参考图片路径(可选),所有分镜都会参考这张图片生成"
+    )
+    parser.add_argument("--max-retries", type=int, default=3, help="最大重试次数")
+    parser.add_argument(
+        "--resume",
+        action="store_true",
+        help="继续执行上次失败的运行(自动查找最新的未完成运行)"
+    )
+    parser.add_argument(
+        "--run-id",
+        type=str,
+        default=None,
+        help="指定要使用的运行ID(用于继续执行特定运行)"
+    )
+    parser.add_argument(
+        "--new-run",
+        action="store_true",
+        help="强制创建新的运行目录(即使存在未完成的运行)"
+    )
+
+    args = parser.parse_args()
+
+    # 1. 创建运行管理器
+    run_manager = RunManager(base_output_dir="output")
+    
+    # 确定运行目录策略
+    if args.new_run:
+        # 强制创建新运行
+        run_output_dir = run_manager.create_run_directory()
+        run_id = run_manager.get_run_id()
+        logger.info("创建新的运行目录")
+    elif args.run_id:
+        # 使用指定的运行ID
+        run_output_dir = run_manager.create_run_directory(run_id=args.run_id)
+        run_id = run_manager.get_run_id()
+        logger.info(f"使用指定的运行ID: {run_id}")
+    elif args.resume:
+        # 自动查找最新的未完成运行
+        runs = run_manager.list_runs()
+        if not runs:
+            logger.warning("没有找到已存在的运行,创建新运行目录")
+            run_output_dir = run_manager.create_run_directory()
+            run_id = run_manager.get_run_id()
+        else:
+            # 查找未完成的运行(检查task_state.json中是否有失败的步骤)
+            resume_run_id = None
+            for run_info in runs:
+                run_path = Path(run_info["path"])
+                state_file = run_path / "task_state.json"
+                
+                if state_file.exists():
+                    try:
+                        import json as json_module
+                        with open(state_file, 'r', encoding='utf-8') as f:
+                            state = json_module.load(f)
+                        
+                        # 检查是否有失败的步骤或待执行的步骤
+                        steps = state.get("steps", {})
+                        has_failed = any(
+                            step.get("status") == "failed" 
+                            for step in steps.values()
+                        )
+                        has_pending = any(
+                            step.get("status") in ["pending", "running"]
+                            for step in steps.values()
+                        )
+                        
+                        if has_failed or has_pending:
+                            resume_run_id = run_info["run_id"]
+                            logger.info(f"找到未完成的运行: {resume_run_id}")
+                            break
+                    except Exception as e:
+                        logger.warning(f"检查运行 {run_info['run_id']} 状态时出错: {e}")
+                        continue
+            
+            if resume_run_id:
+                run_output_dir = run_manager.create_run_directory(run_id=resume_run_id)
+                run_id = run_manager.get_run_id()
+                logger.info(f"继续执行运行: {run_id}")
+            else:
+                logger.info("没有找到未完成的运行,创建新运行目录")
+                run_output_dir = run_manager.create_run_directory()
+                run_id = run_manager.get_run_id()
+    else:
+        # 默认行为:创建新运行
+        run_output_dir = run_manager.create_run_directory()
+        run_id = run_manager.get_run_id()
+        logger.info("创建新的运行目录")
+
+    logger.info(f"运行ID: {run_id}")
+    logger.info(f"输出目录: {run_output_dir}")
+
+    # 2. 创建文件I/O处理器
+    io_handler = FileIOHandler()
+
+    # 3. 创建任务管理器
+    state_file = str(Path(run_output_dir) / "task_state.json")
+    cache_dir = str(Path(run_output_dir) / "task_cache")
+
+    manager = TaskManager(
+        state_file=state_file,
+        cache_dir=cache_dir
+    )
+
+    # 4. 创建视频创作任务流
+    pipeline = ReferVideoCreatePipeline(io_handler, run_output_dir, manager)
+
+    # 5. 注册步骤
+    logger.info("注册步骤...\n")
+    
+    # TaskManager 现在原生支持异步函数,无需包装器
+    # 创建异步包装函数(lambda 不能是异步的)
+    async def step1_func():
+        return await pipeline.step1_create_script(
+            video_url=args.video_url,
+            user_prompt=args.user_prompt
+        )
+    
+    async def step2_func():
+        return await pipeline.step2_optimize_prompts()
+    
+    async def step3_func():
+        """
+        步骤3:生成分镜图片
+        如果指定了 --refer-image,所有分镜都会参考这张图片生成
+        """
+        refer_image = args.refer_image
+        
+        if refer_image:
+            for image_item in refer_image:
+                # 检查文件是否存在
+                refer_image_path = Path(image_item)
+                if not refer_image_path.exists():
+                    logger.warning(f"参考图片不存在: {image_item},将不使用参考图片")
+                    refer_image = None
+                else:
+                    logger.info(f"使用参考图片: {image_item}")
+        else:
+            logger.info("不使用参考图片")
+        
+        return await pipeline.step3_generate_storyboard(
+            size=args.size,
+            refer_image=refer_image
+        )
+
+    async def step4_func():
+        return await pipeline.step4_generate_video_clips()
+
+    async def step5_func():
+        return await pipeline.step5_concat_clips()
+
+    manager.register_step(
+        "step1",
+        step1_func,
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step2",
+        step2_func,
+        depends_on=["step1"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step3",
+        step3_func,
+        depends_on=["step2"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step4",
+        step4_func,
+        depends_on=["step3"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step5",
+        step5_func,
+        depends_on=["step4"],
+        force_rerun=False
+    )
+
+    # 6. 显示当前状态
+    summary = manager.get_summary()
+    logger.info(f"总步骤数: {summary['total']}")
+    logger.info(f"待执行: {summary['pending']}")
+
+    # 7. 执行所有步骤(使用异步版本,性能更优)
+    logger.info("\n开始执行视频创作...")
+    
+    async def run_pipeline_async():
+        """异步执行所有步骤"""
+        # 使用异步版本的 run_all_async,性能比 ThreadPoolExecutor 更优
+        await manager.run_all_async(
+            step_order=["step1", "step2", "step3", "step4", "step5"],
+            continue_on_error=False
+        )
+        logger.info("\n视频创作完成!")
+    
+    # 重试机制:执行1次,如果失败则重试 max_retries 次
+    # 总共最多执行 max_retries + 1 次
+    last_exception = None
+    total_attempts = args.max_retries + 1  # 1次正常执行 + max_retries 次重试
+    
+    for attempt in range(total_attempts):
+        try:
+            if attempt == 0:
+                logger.info(f"第 {attempt + 1} 次执行...")
+            else:
+                # 重试前等待一段时间,避免快速连续失败
+                wait_time = min(2 ** (attempt - 1), 60)  # 指数退避,最多60秒
+                logger.info(f"等待 {wait_time} 秒后开始第 {attempt + 1} 次执行(第 {attempt} 次重试)...")
+                time.sleep(wait_time)
+            
+            # 运行异步流程
+            asyncio.run(run_pipeline_async())
+            # 执行成功,退出重试循环
+            if attempt > 0:
+                logger.info(f"✅ 第 {attempt + 1} 次执行成功(经过 {attempt} 次重试)")
+            break
+            
+        except Exception as e:
+            last_exception = e
+            if attempt == 0:
+                logger.error(f"❌ 第 1 次执行失败: {e}", exc_info=True)
+            else:
+                logger.error(f"❌ 第 {attempt + 1} 次执行失败(第 {attempt} 次重试): {e}", exc_info=True)
+            
+            # 如果是最后一次尝试,记录最终失败
+            if attempt == total_attempts - 1:
+                logger.error(f"\n✗ 执行失败:经过 {total_attempts} 次尝试(1次正常执行 + {args.max_retries} 次重试)后仍然失败")
+                raise last_exception
+            # 否则继续重试
+            continue
+    
+    # 如果所有重试都失败,这里不应该到达(因为上面已经 raise)
+    # 但为了安全起见,还是检查一下
+    if last_exception is not None:
+        raise last_exception
+
+    logger.info(f"\n所有结果已保存到: {run_output_dir}")
+
+    end_time = time.time()
+    logger.info(f"执行时间: {end_time - start_time} 秒")
+
+
+# python -m examples.refer_video_create.main --video-url "video.mp4" --user-prompt "请开始执行你的任务"
+if __name__ == "__main__":
+    main()
+

BIN
examples/refer_video_create/mcps/__pycache__/script_check.cpython-310.pyc


BIN
examples/refer_video_create/mcps/__pycache__/script_check.cpython-312.pyc


BIN
examples/refer_video_create/mcps/__pycache__/script_create.cpython-310.pyc


BIN
examples/refer_video_create/mcps/__pycache__/script_optimate.cpython-310.pyc


+ 152 - 0
examples/refer_video_create/mcps/script_check.py

@@ -0,0 +1,152 @@
+import os
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.refer_video_create.mcps.script_check")
+
+prompt_check_image_script = \
+"""
+【Role】
+你是一个非常细心的图像质量评估专家
+
+【Background】
+我正在使用AIGC工具进行图像生成,但是经常出现生成的图像存在缺陷的情况
+我需要你帮忙审查生成的图像,当你审查判断为图像存在缺陷时,我便筛除这张图像
+
+【Goal】
+分析输入图像是否存在AI生成的不合理之处,包括但不限于:
+- 解剖/比例错误:手指数量异常、头身比失调、肢体扭曲等
+- 空间/透视错误:多人物不在同一地平线、遮挡关系矛盾、近大远小失效、多角色空间布局错误、人物站位等
+- 物体一致性缺失:文本说“穿红衣服”图像却是蓝衣;“三只猫”却画了四只等
+- 伪影与纹理异常:模糊区域、重复图案、非自然噪点(如“融化的脸”)等
+- 常识/逻辑违背:人坐在空中无椅子、钟表指针反向、水往高处流等
+- 穿模问题:人物或物体穿透了其他人物或物体等任何穿模问题
+
+**需重点关注的问题**(需重点关注,务必严格审查):
+- 人体是否存在不合理的变形或扭曲、身体构造错误等、关节异常等
+- 多人物场景中,人物是否存在不合理的遮挡、穿透、重叠、融合等问题(包括但**不限于**:人物穿透了其他人物、人物穿透了物体)
+- 人物的头、四肢、关节、完整的身体构造必须仔细一一检查
+- 是否存在物品悬浮、悬空、漂浮等不合理现象(包括但**不限于**:人物手上的包包、手机、书包等物品在不拿着的情况下悬浮、悬空、漂浮等;或物品突然消失或突然出现的情况)
+- 是否存在人物身体呈侧后方姿态,但头部却面向镜头的情况(需要思考推理判断,可通过头、身体、四肢所面向的方向/角度来判断)
+
+【Constraints】
+- 对可能存在的问题进行逐个分析审查
+- 你的内部分析推理不能少于5000字!(这部分不用输出,但你必须进行内部推理,确保没有遗漏问题!)
+- 以JSON格式输出,仅包含**review_result**(取值范围:ture/false)和**result reason**两个字段
+- 除以JSON格式输出结果之外,不要用任何其他内容输出(重要!)
+
+【Output Format】
+```json
+{
+    "review_result": // bool;审查结果;必填
+    "result_reason": // text;对审查结果的解释;必填;字数不少于1000字
+}
+```
+
+## 让我们一步步来分析
+"""
+
+
+prompt_check_video_script = \
+"""
+【Role】
+你是一个非常细心且有工作激情的视频质量评估专家
+
+【Background】
+我正在使用AIGC工具进行视频生成,但是经常出现生成的视频存在缺陷的情况
+我需要你帮忙审查生成的视频,当你审查判断为视频存在缺陷时,我便筛除这段视频
+
+【Goal】
+分析输入视频是否存在AI生成的不合理之处,包括但不限于:
+- 解剖/比例错误:手指数量异常、头身比失调、肢体扭曲等
+- 空间/透视错误:多人物不在同一地平线、遮挡关系矛盾、近大远小失效、多角色空间布局错误、人物站位等
+- 常识/逻辑违背:人坐在空中无椅子、物品悬浮悬空漂浮等
+- 穿模问题:人物或物体穿透了其他人物或物体等任何穿模问题
+
+**需重点关注的问题**(需重点关注,务必严格审查):
+- 人物运动过程中,肢体是否存在不合理的变形或扭曲
+- 多人物场景中,人物是否存在不合理的遮挡、穿透、重叠、融合等问题(包括但**不限于**:人物穿透了其他人物、人物穿透了物体)
+- 任何物品是否存在突然消失或突然出现的情况(包括但**不限于**:人物手中的物品突然消失或突然出现)
+
+【Constraints】
+- 对可能存在的问题进行逐个分析审查
+- 除列举的问题之外,不得遗漏任何你发现的不合理、不符合物理规律、不符合常识、不符合逻辑的问题(重要!)
+- 你需要先假定如果这个视频存在质量问题,那么经过你的仔细分析,具体的质量问题是什么?(重要!这是避免遗漏问题的关键!)
+- 你的内部推理不能少于5000字!(这部分不用输出,但你必须进行内部推理,确保没有遗漏问题!)
+- 以JSON格式输出,仅包含**review_result**(取值范围:ture/false)和**result reason**两个字段
+- 除以JSON格式输出结果之外,不要用任何其他内容输出(重要!)
+
+【Output Format】
+```json
+{
+    "review_result": // bool;审查结果;必填
+    "result_reason": // text;对审查结果的解释;必填;字数不少于1000字
+}
+```
+
+## 请对输入图像逐像素逐像素仔细检查分析,得出经过审慎审查后的结果
+"""
+
+async def check_image_script(
+    client: AsyncArkClient,
+    image_prompt: str,
+    image_url: str
+) -> str:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(image_prompt)
+    user_message.add_image(image_url)
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=prompt_check_image_script,
+        )
+        logger.info(f"检查图像脚本成功")
+        return client.get_response_text(response)
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+async def check_video_script(
+    client: AsyncArkClient,
+    video_prompt: str,
+    video_url: str
+) -> str:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(video_prompt)
+    user_message.add_video(video_url)
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=prompt_check_video_script,
+        )
+        logger.info(f"检查视频脚本成功")
+        return client.get_response_text(response)
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+if __name__ == "__main__":
+    async def main():
+        async with AsyncArkClient() as client:
+            review_result = await check_image_script(
+                client=client,
+                image_prompt="请逐像素逐像素仔细检查分析,得出经过审慎审查后的结果",
+                image_url="./output/run_20260107_111034/storyboard/lens_1_run_20260107_111034.png" 
+            )
+            print(review_result)
+            review_result = await check_video_script(
+                client=client,
+                video_prompt="请逐帧逐帧仔细分析,得出经过审慎审查后的结果",
+                video_url="./output/run_20260107_111034/video_clips/lens_1_run_20260107_111034.mp4" 
+            )
+            print(review_result)
+    asyncio.run(main())

+ 81 - 0
examples/refer_video_create/mcps/script_create.py

@@ -0,0 +1,81 @@
+import os
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.refer_video_create.mcps.script_create")
+
+system_prompt_develop_script = \
+"""
+## 角色
+- 你兼具专业的视频分析能力与视频脚本创作能力
+- 你坚持“先过拟合,再泛化”的创作理念,确保脚本内容与输入视频内容完全一致
+
+## 任务
+- 仔细拆解分析输入视频的完整内容,完全对标视频内容进行视频脚本创作输出,实现对输入视频的完全复刻。
+
+## 输出格式
+- 必须按照以下JSON格式进行输出(必须严格遵守JSON格式,不得有任何其他内容):
+```json
+{
+    "basic_info": {
+        "script_theme": // str;脚本主题,明确整体场景基调,
+        "total_lenses": // int;总镜头数,方便后期拼接核对,
+        "total_duration": // int;总时长:所有lens_details的lens_duration之和,
+        "unified_style": // str;全片统一风格/氛围,保证AI生成一致性
+    },
+    "lens_details": [
+        {
+            "lens_id": // int;镜头编号
+            "lens_params": // str;核心镜头参数:景别(远景/全景/中景/近景/特写等景别)+ 视角(俯拍/仰拍/过肩/平视/微距/航拍/顶拍/斜拍等视角) + 运镜(推/拉/摇/移/跟随/环绕/跟焦/滑轨/固定镜头等运镜),
+            "core_vision": // str;核心视觉画面:人物信息(如人物神情/姿态/动作,但是避免对人物的样貌和穿着进行描写;如:‘一个女生正在散步,非常开心的神态’,而非‘**黄头发的**美女正在散步,**穿着白色连衣裙**,非常开心的神态’)+ 场景细节(A-Roll);或景物描写(B-Roll),
+            "lens_type": // str;镜头类型:A-Roll或B-Roll;A-Roll为人物镜头,B-Roll为场景镜头,
+            "lines_narration": // str;台词/旁白,可为None,
+            "bgm_style": // str;BGM风格,如:轻柔的小提琴独奏,节奏缓慢,旋律偏温暖,可为None,
+            "sound_effects": // str;音效,如:树叶飘落的轻微沙沙声,可为None
+            "lens_duration": // int;镜头时长,取值范围3-12
+        },
+        ... // 更多镜头信息
+    ]
+}
+```
+
+## 要求
+- 需要对输入视频中的每个镜头片段进行拆解分析,不能遗漏任何一个镜头片段。(输入视频中每一个无转场的镜头片段,都应被视为一个独立的镜头片段,也即对应JSON中的一个lens_details元素)
+- 请重读一遍任务和所有约束条件后,开始进行脚本创作。
+"""
+
+async def create_script_refer_video(
+    client: AsyncArkClient,
+    video_url: str,
+    user_prompt: str = "请开始执行你的任务"
+) -> str:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt)
+    user_message.add_video(video_url)
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_develop_script,
+        )
+        logger.info(f"开发脚本成功")
+        return client.get_response_text(response)
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+if __name__ == "__main__":
+    async def main():
+        async with AsyncArkClient() as client:
+            script = await create_script_refer_video(
+                client=client,
+                video_url="video.mp4" 
+            )
+            io_handler.write_json(script, "script.json")
+    asyncio.run(main())

+ 157 - 0
examples/refer_video_create/mcps/script_optimate.py

@@ -0,0 +1,157 @@
+import os
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.refer_video_create.mcps.script_optimate")
+
+prompt_optimate_image = \
+"""
+## 角色
+AIGC生图提示词优化专家
+
+## 任务
+基于**提示词优化指南**对用户输入的AIGC生图提示词进行优化,以提升生成图片的质量。
+
+## 提示词优化指南
+```
+### 通用规则
+1. **用自然语言清晰描述画面**
+建议用**简洁连贯**的自然语言写明**主体+行为+环境**,若对画面美学有要求,可用自然语言或短语补充**风格、色彩、光影、构图**等美学元素。
+- 示例:一个穿着华丽服装的女孩,撑着遮阳伞走在林荫道上,莫奈油画风格
+- 避免:一个女孩,撑伞,林荫街道,优化般的细腻笔触
+
+2. 明确应用场景和用途
+当有明确的应用场景时,推荐在文本提示中写明图像用途和类型
+- 示例:设计一个国际时装周模特形象,主体是一个女模特穿着精致高定礼服站立在走秀T台上
+- 避免:一个女模特,国际时装周,身穿精致高定礼服,站在走秀台上
+
+3. 提升风格渲染效果
+如果有明确的风格需求,使用精准的风格词或提供参考图,能获得更理想的效果
+
+4. 提高文本渲染准确度
+建议将要生成的**文字内容**放在**双引号**中
+- 示例:生成一张海报,标题为“AI的未来”
+- 避免:生成一张海报,标题为AI的未来
+
+5.明确图片编辑目标和希望保持不变的部分
+使用**简洁明确的指令**,说明需要修改或参考的对象及具体操作,避免使用指代模糊的代词;如果希望除了修改的内容都保持不变,则可以在prompt中强调
+- 示例:让图中最高的那只熊猫穿上粉色的京剧服饰并戴上头饰,并保持动作不变
+- 避免:让它穿上粉色衣服
+
+### 参考图生图秘籍
+当基于参考图生成图像时,只需在文本提示中明确两部分内容:
+- 指明参考对象:清晰描述希望从参考图中提取并保留的元素,如:参考图中的人物形象、参考图中产品材质等
+- 描述生成画面:具体说明希望生成的画面内容、场景等细节信息
+
+### 优化结果检查清单
+[ ] 是否以自然语言清晰描述画面,而不是简单堆砌关键词
+[ ] 因提示词是用于生成静态图像,避免使用一系列动作描述,通常只描述第一步动作即可;若原始描述包含动作序列,请选择最具代表性的一帧(通常是起始状态或稳定状态)进行描绘(例如:推荐‘人物面带着微笑,左手提包,右手对着镜头挥手’,避免‘人物手提包,面带微笑,先挥手,接着转身展示服装背面,随后再转回来面向镜头’)
+[ ] 不要有任何对人物服装、外貌的细节描述,这是绝对不能出现的!
+```
+
+## 要求
+- 仅以自然语言输出优化后的提示词,不得有任何注释、任何解释、任何说明等多余内容输出。
+- 请重读一遍任务和所有约束条件后,开始进行提示词优化。
+"""
+
+prompt_optimate_video = \
+"""
+## 角色
+AIGC图生视频提示词优化专家
+
+## 任务
+基于**提示词优化指南**对用户输入的图生视频提示词进行优化,以提升生成视频的质量
+
+## 提示词优化指南
+```
+### 基础结构与核心原则
+1. 核心公式:提示词=主体+运动(优先描述动态元素,减少静态内容)
+- 例如:老人戴上眼镜(主体+动作)、女孩的头发被风吹动(主体特征+运动)
+2. 简洁明确:使用简单句和关键词,避免复杂描述
+- 例如:‘快速驶过’优于‘以极高速度行驶’
+3. 遵从原图:提示词需与输入图片内容一致,避免矛盾
+- 错误:图片是草原,提示词写‘咖啡厅场景’
+- 正确:图片是草原,提示词写‘骏马在草原上奔跑’
+4. 特征定位:主体有突出特征时需明确描述(如‘戴墨镜的女人’,‘穿红裙的女孩’)
+
+### 多动作与多主体描述
+1. 单主体多动作:按时序直接罗列
+- 例如:女孩拿起酒杯喝了一口酒后,放下酒杯并起身离开
+2. 多主体多动作:明确各主体的动作
+- 例如:男孩踢足球,女孩在一旁欢呼,小狗追逐着足球
+
+### 运镜与镜头控制
+支持自然语言描述镜头变化:
+1. 基础运镜:
+- 推/拉:镜头推近人物脸部 镜头拉远展示全景
+- 摇/移:镜头左摇拍摄山脉 镜头跟随人物移动
+
+2. 复杂运镜组合:
+- 例:镜头从地面仰拍,跟随人物向上移动并环绕至正面特写
+
+3. 景别控制:
+- 特写/全景:特写人物手部动作 全景展示城市夜景
+- 视角:航拍沙漠商队 微距拍摄蚂蚁搬家
+
+### 程度副词强化效果
+明确动作强度/频率,避免模糊描述:
+- 例如:汽车快速驶过(优于‘汽车驶过’)、翅膀大幅度扇动(优于‘翅膀扇动’)
+
+### 风格与氛围控制
+1. 风格关键词:直接添加风格描述
+- 例:国漫风格 水墨画 赛博朋克 复古胶片
+2. 氛围渲染:通过光线、色调等增加画面感
+- 例:黄昏暖光下的海边 昏暗房间里的烛光 冷色调的科幻场景
+
+### 优化结果检查清单
+[ ] 避免负面提示词:模型不响应‘不要’类描述
+[ ] 不要有任何对人物服装、外貌的细节描述,这是绝对不能出现的!(可以是‘女生抚摸着衣服表面纹理’,但不能是‘**黄色头发的**女生抚摸着衣服表面**的刺绣蕾丝花边**’;请理解此示例的深刻涵义,并举一反三进行审查)
+```
+
+## 要求
+- 仅以自然语言输出优化后的提示词,不得有任何注释、任何解释、任何说明等多余内容输出。
+- 请重读一遍任务和所有约束条件后,开始进行提示词优化。
+"""
+
+async def optimate_script(
+    client: AsyncArkClient,
+    user_prompt: str = "请开始执行你的任务",
+    prompt_type: str = "image"
+) -> str:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt)
+
+    if prompt_type == "image":
+        system_prompt = prompt_optimate_image
+    elif prompt_type == "video":
+        system_prompt = prompt_optimate_video
+    else:
+        raise ValueError(f"不支持的提示词类型: {prompt_type}")
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt,
+        )
+        logger.info(f"优化脚本成功")
+        return client.get_response_text(response)
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+if __name__ == "__main__":
+    async def main():
+        async with AsyncArkClient() as client:
+            optimized_prompt = await optimate_script(
+                client=client,
+                user_prompt="全景视角,平视角度,固定镜头拍摄:一位人物站在灰色沙发前,背景有三幅装饰画和右侧绿植,人物手提包,面带微笑,先挥手,接着转身展示服装背面,随后再转回来面向镜头",
+                prompt_type="image"
+            )
+            print(optimized_prompt)
+    asyncio.run(main())

+ 3 - 0
examples/refer_video_create/pipeline/__init__.py

@@ -0,0 +1,3 @@
+from .refer_video_create_pipeline import ReferVideoCreatePipeline
+
+__all__ = ["ReferVideoCreatePipeline"]

BIN
examples/refer_video_create/pipeline/__pycache__/__init__.cpython-310.pyc


BIN
examples/refer_video_create/pipeline/__pycache__/refer_video_create_pipeline.cpython-310.pyc


+ 413 - 0
examples/refer_video_create/pipeline/refer_video_create_pipeline.py

@@ -0,0 +1,413 @@
+"""
+refer_video_create 任务流
+基于参考视频创建视频的完整任务流
+包括:
+1. 从参考视频创建脚本
+2. 优化脚本提示词(生图提示词和生视频提示词)
+3. 基于image_prompt生成分镜
+4. 基于video_prompt和分镜生成视频
+5. 拼接所有视频片段
+"""
+import os
+import asyncio
+from pathlib import Path
+from typing import Dict, Optional
+
+from taskflow import TaskManager, FileIOHandler
+from ..mcps.script_create import create_script_refer_video
+from ..mcps.script_optimate import optimate_script
+from ..mcps.script_check import check_image_script, check_video_script
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_image_client_async import AsyncArkImageClient
+from api_modules.ark_video_client_async import AsyncArkVideoClient
+from examples.video_create.mcps.concat_clip import concat_videos
+from examples.video_create.utils.tools import string_to_json, download_image, upload_file_to_tos
+from taskflow import get_logger
+
+logger = get_logger("examples.refer_video_create.pipeline.refer_video_create_pipeline")
+
+class ReferVideoCreatePipeline:
+    """基于参考视频的视频创作任务流"""
+
+    def __init__(self, io_handler: FileIOHandler, output_dir: str, manager: TaskManager):
+        """
+        初始化视频创作任务流
+
+        Args:
+            io_handler: 文件I/O处理器
+            output_dir: 输出目录
+            manager: 任务管理器
+        """
+        self.io_handler = io_handler
+        self.output_dir = Path(output_dir)
+        self.output_dir.mkdir(parents=True, exist_ok=True)
+        self.manager = manager
+
+    async def step1_create_script(self, video_url: str, user_prompt: Optional[str] = None) -> Dict:
+        """步骤1:从参考视频创建初始脚本"""
+        if user_prompt is None:
+            user_prompt = "请开始执行你的任务"
+        
+        async with AsyncArkClient() as client:
+            script_text = await create_script_refer_video(
+                client=client,
+                video_url=video_url,
+                user_prompt=user_prompt
+            )
+        
+        # 解析JSON字符串
+        script = self.io_handler.string_to_json(script_text)
+        
+        output_file = str(self.output_dir / "step1_script.json")
+        await self.io_handler.write_json_async(script, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": script
+        }
+
+    async def step2_optimize_prompts(self) -> Dict:
+        """步骤2:优化脚本提示词(并行优化生图提示词和生视频提示词)"""
+        previous_output = self.manager.load_step_output("step1")
+        if previous_output is None:
+            raise ValueError("步骤1未完成,无法优化提示词")
+        
+        script = previous_output["data"]
+        
+        async def optimize_single_lens(client: AsyncArkClient, lens: Dict) -> None:
+            """优化单个镜头的提示词"""
+            lens_id = lens.get("lens_id")
+            lens_params = lens.get("lens_params", "")
+            core_vision = lens.get("core_vision", "")
+            
+            # 构建优化提示词:lens_params + core_vision
+            prompt_text = f"{lens_params} {core_vision}".strip()
+            
+            # 并行优化生图提示词和生视频提示词
+            async def optimize_image_prompt():
+                optimized = await optimate_script(
+                    client=client,
+                    user_prompt=prompt_text,
+                    prompt_type="image"
+                )
+                lens["image_prompt"] = optimized.strip()
+                logger.info(f"镜头 {lens_id} 的生图提示词优化完成")
+            
+            async def optimize_video_prompt():
+                optimized = await optimate_script(
+                    client=client,
+                    user_prompt=prompt_text,
+                    prompt_type="video"
+                )
+                lens["video_prompt"] = optimized.strip()
+                logger.info(f"镜头 {lens_id} 的生视频提示词优化完成")
+            
+            # 并行执行两个优化任务
+            await asyncio.gather(optimize_image_prompt(), optimize_video_prompt())
+        
+        async with AsyncArkClient() as client:
+            # 并行处理所有镜头
+            tasks = [
+                optimize_single_lens(client, lens)
+                for lens in script["lens_details"]
+            ]
+            await asyncio.gather(*tasks)
+        
+        output_file = str(self.output_dir / "step2_optimized_script.json")
+        await self.io_handler.write_json_async(script, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": script
+        }
+
+    async def step3_generate_storyboard(
+        self, 
+        size: Optional[str] = "1440x2560",
+        refer_image: Optional[list[str]] = None
+    ) -> Dict:
+        """
+        步骤3:基于image_prompt和用户指定的参考图片生成分镜图片
+        
+        Args:
+            size: 生成图片的尺寸,默认为 "1440x2560"
+            refer_image: 参考图片路径(可选),所有分镜都会参考这张图片生成
+                        如果为None,则不使用参考图片
+        """
+        previous_output = self.manager.load_step_output("step2")
+        if previous_output is None:
+            raise ValueError("步骤2未完成,无法生成分镜")
+        
+        script = previous_output["data"]
+        
+        # 确保storyboard目录存在
+        storyboard_dir = self.output_dir / "storyboard"
+        storyboard_dir.mkdir(parents=True, exist_ok=True)
+        
+        # 准备参考图片列表(如果提供了参考图片)
+        reference_images = None
+        if refer_image is not None and isinstance(refer_image, str):
+            # 确保是列表格式
+            reference_images = [refer_image]
+            logger.info(f"所有分镜将参考图片: {refer_image}")
+        elif refer_image is not None and isinstance(refer_image, list):
+            reference_images = refer_image
+            logger.info(f"所有分镜将参考图片: {refer_image}")
+        else:
+            logger.info("不使用参考图片")
+        
+        async def generate_single_storyboard(
+            image_client: AsyncArkImageClient, 
+            ark_client: AsyncArkClient,
+            lens: Dict
+        ) -> None:
+            """生成单个镜头的分镜图片(带审查和重试机制)"""
+            lens_id = lens.get("lens_id")
+            image_prompt = lens.get("image_prompt")
+            
+            if not image_prompt:
+                raise ValueError(f"镜头 {lens_id} 缺少 image_prompt 字段")
+            
+            image_save_path = str(storyboard_dir / f"lens_{lens_id}_{self.output_dir.name}.png")
+            
+            # 如果文件已存在,跳过生成
+            if os.path.exists(image_save_path):
+                logger.info(f"分镜图片已存在,跳过生成: lens {lens_id}")
+                lens["storyboard_url"] = image_save_path
+                return
+            
+            # 最大重试次数
+            max_retries = 5
+            attempt_count = 0
+            review_passed = False
+            temp_image_path = str(storyboard_dir / f"lens_{lens_id}_{self.output_dir.name}_temp.png")
+            last_image_url = None
+            
+            while attempt_count <= max_retries and not review_passed:
+                try:
+                    attempt_count += 1
+                    logger.info(f"镜头 {lens_id} 开始第 {attempt_count} 次生成...")
+                    
+                    # 生成分镜图片(使用参考图片)
+                    response = await image_client.generate_image(
+                        prompt=image_prompt,
+                        size=size,
+                        reference_image=reference_images
+                    )
+                    
+                    image_url = image_client.get_image_url(response)
+                    if not image_url:
+                        raise ValueError(f"镜头 {lens_id} 生成分镜图片失败,未获取到图片URL")
+                    
+                    last_image_url = image_url
+                    
+                    # 下载图片到临时路径(用于审查)
+                    await asyncio.to_thread(download_image, image_url, temp_image_path)
+                    
+                    # 上传图片到TOS获取URL(用于审查)
+                    image_url_for_check = await asyncio.to_thread(upload_file_to_tos, temp_image_path)
+                    logger.info(f"镜头 {lens_id} 第 {attempt_count} 次生成完成,图片已上传: {image_url_for_check}")
+                    
+                    # 调用审查函数
+                    check_result_text = await check_image_script(
+                        client=ark_client,
+                        image_prompt=image_prompt,
+                        image_url=image_url_for_check
+                    )
+                    
+                    # 解析审查结果
+                    check_result = self.io_handler.string_to_json(check_result_text)
+                    review_result = check_result.get("review_result", False)
+                    result_reason = check_result.get("result_reason", "")
+                    
+                    if review_result:
+                        # 审查不通过(review_result为true表示有问题,需要重新生成)
+                        if attempt_count > max_retries:
+                            # 超过最大重试次数,使用最后一次生成的图片
+                            logger.error(
+                                f"镜头 {lens_id} 已达到最大重试次数 ({max_retries}),"
+                                f"审查结果: {result_reason},使用最后一次生成的图片"
+                            )
+                            # 将临时文件重命名为最终文件
+                            if os.path.exists(temp_image_path):
+                                os.rename(temp_image_path, image_save_path)
+                            else:
+                                # 如果临时文件不存在,重新下载
+                                await asyncio.to_thread(download_image, last_image_url, image_save_path)
+                            review_passed = True
+                        else:
+                            # 继续重试
+                            logger.warning(
+                                f"镜头 {lens_id} 第 {attempt_count} 次生成审查不通过: {result_reason},"
+                                f"将重新生成(剩余重试次数: {max_retries - attempt_count})"
+                            )
+                            # 保留临时文件,下次生成时会覆盖
+                    else:
+                        # 审查通过(review_result为false表示通过)
+                        review_passed = True
+                        logger.info(
+                            f"镜头 {lens_id} 第 {attempt_count} 次生成审查通过: {result_reason}"
+                        )
+                        # 将临时文件重命名为最终文件
+                        if os.path.exists(temp_image_path):
+                            os.rename(temp_image_path, image_save_path)
+                        else:
+                            # 如果临时文件不存在,重新下载
+                            await asyncio.to_thread(download_image, last_image_url, image_save_path)
+                        
+                except Exception as e:
+                    logger.error(f"镜头 {lens_id} 第 {attempt_count} 次生成出错: {e}")
+                    if attempt_count > max_retries:
+                        logger.error(f"镜头 {lens_id} 已达到最大重试次数,停止重试")
+                        # 如果还有临时文件,尝试使用它
+                        if os.path.exists(temp_image_path):
+                            os.rename(temp_image_path, image_save_path)
+                        raise
+                    # 继续重试
+                    continue
+            
+            lens["storyboard_url"] = image_save_path
+            logger.info(f"镜头 {lens_id} 分镜图片最终生成完成: {image_save_path}(共尝试 {attempt_count} 次)")
+        
+        async with AsyncArkImageClient() as image_client, AsyncArkClient() as ark_client:
+            # 并行处理所有镜头
+            tasks = [
+                generate_single_storyboard(image_client, ark_client, lens)
+                for lens in script["lens_details"]
+            ]
+            await asyncio.gather(*tasks, return_exceptions=True)
+        
+        output_file = str(self.output_dir / "step3_storyboard.json")
+        await self.io_handler.write_json_async(script, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": script
+        }
+
+    async def step4_generate_video_clips(self) -> Dict:
+        """步骤4:基于video_prompt和分镜图片生成视频片段"""
+        previous_output = self.manager.load_step_output("step3")
+        if previous_output is None:
+            raise ValueError("步骤3未完成,无法生成视频片段")
+        
+        script = previous_output["data"]
+        
+        # 确保video_clips目录存在
+        video_clip_dir = self.output_dir / "video_clips"
+        video_clip_dir.mkdir(parents=True, exist_ok=True)
+        
+        async def process_single_lens(video_client: AsyncArkVideoClient, lens: Dict) -> Optional[asyncio.Task]:
+            """处理单个镜头的视频生成,返回后台任务"""
+            lens_id = lens.get("lens_id")
+            video_prompt = lens.get("video_prompt")
+            storyboard_url = lens.get("storyboard_url")
+            lens_duration = lens.get("lens_duration", 4)
+            
+            if not video_prompt:
+                raise ValueError(f"镜头 {lens_id} 缺少 video_prompt 字段")
+            if not storyboard_url:
+                raise ValueError(f"镜头 {lens_id} 缺少 storyboard_url 字段")
+            
+            video_save_path = str(video_clip_dir / f"lens_{lens_id}_{self.output_dir.name}.mp4")
+            
+            # 如果文件已存在,跳过生成
+            if os.path.exists(video_save_path):
+                logger.info(f"视频片段已存在,跳过生成: lens {lens_id}")
+                lens["clip_url"] = video_save_path
+                return None
+            
+            # 如果storyboard_url是本地路径,需要上传到TOS获取URL
+            image_url = storyboard_url
+            if not storyboard_url.startswith(("http://", "https://")):
+                # 上传到TOS获取URL
+                from examples.video_create.utils.tools import upload_file_to_tos
+                image_url = await asyncio.to_thread(upload_file_to_tos, storyboard_url)
+                logger.info(f"镜头 {lens_id} 分镜图片已上传到TOS: {image_url}")
+            
+            # 构建生成参数字符串
+            gen_params = f" --dur {lens_duration}"
+            
+            # 创建视频生成任务
+            task_id, background_task = await video_client.create_video_task_async(
+                prompt=video_prompt,
+                image_url=image_url,
+                gen_params=gen_params,
+                output_path=video_save_path
+            )
+            
+            if background_task is not None:
+                lens["clip_url"] = video_save_path
+                logger.info(f"已提交视频生成任务,lens {lens_id}, task_id: {task_id}")
+                return background_task
+            else:
+                logger.error(f"视频生成任务提交失败,lens {lens_id}")
+                return None
+        
+        async with AsyncArkVideoClient() as video_client:
+            # 并行提交所有视频生成任务,收集后台任务
+            lens_tasks = [
+                process_single_lens(video_client, lens)
+                for lens in script["lens_details"]
+            ]
+            lens_results = await asyncio.gather(*lens_tasks, return_exceptions=True)
+            
+            # 展平所有后台任务
+            all_background_tasks = []
+            for task in lens_results:
+                if isinstance(task, Exception):
+                    logger.error(f"处理镜头时出错: {task}")
+                elif task is not None:
+                    all_background_tasks.append(task)
+            
+            # 等待所有视频生成和下载完成
+            if all_background_tasks:
+                logger.info(f"等待 {len(all_background_tasks)} 个视频生成任务完成...")
+                await asyncio.gather(*all_background_tasks, return_exceptions=True)
+                logger.info("所有视频生成任务已完成!")
+            else:
+                logger.warning("没有提交任何视频生成任务")
+        
+        output_file = str(self.output_dir / "step4_video_clips.json")
+        await self.io_handler.write_json_async(script, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": script
+        }
+
+    async def step5_concat_clips(self) -> Dict:
+        """步骤5:拼接所有视频片段"""
+        previous_output = self.manager.load_step_output("step4")
+        if previous_output is None:
+            raise ValueError("步骤4未完成,无法进行视频拼接")
+        
+        script = previous_output["data"]
+        
+        # 确保video_save目录存在
+        video_save_dir = self.output_dir / "video_save"
+        video_save_dir.mkdir(parents=True, exist_ok=True)
+        
+        # 收集所有视频片段路径(按lens_id排序)
+        clips_path = []
+        for lens in sorted(script["lens_details"], key=lambda x: x.get("lens_id", 0)):
+            clip_url = lens.get("clip_url")
+            if clip_url and os.path.exists(clip_url):
+                clips_path.append(clip_url)
+            else:
+                logger.warning(f"镜头 {lens.get('lens_id')} 的视频片段不存在,跳过")
+        
+        if not clips_path:
+            raise ValueError("没有可用的视频片段进行拼接")
+        
+        output_file = str(video_save_dir / "final_video.mp4")
+        
+        # 拼接视频(使用线程池执行同步操作)
+        await asyncio.to_thread(concat_videos, clips_path, output_file)
+        
+        logger.info(f"视频拼接完成: {output_file}")
+        
+        return {
+            "output_file": output_file,
+            "data": "final video"
+        }

+ 5 - 0
examples/text_analysis/__init__.py

@@ -0,0 +1,5 @@
+"""
+文本分析示例
+演示如何使用 TaskFlow 进行文本文件的处理和分析
+"""
+

BIN
examples/text_analysis/__pycache__/__init__.cpython-310.pyc


BIN
examples/text_analysis/__pycache__/main.cpython-310.pyc


BIN
examples/text_analysis/__pycache__/processors.cpython-310.pyc


BIN
examples/text_analysis/__pycache__/steps.cpython-310.pyc


+ 137 - 0
examples/text_analysis/main.py

@@ -0,0 +1,137 @@
+"""
+文本分析示例 - 主程序
+演示如何使用 TaskFlow 进行文本文件的处理和分析
+"""
+
+import logging
+from pathlib import Path
+
+from taskflow import TaskManager, FileIOHandler, RunManager
+from taskflow import setup_logger
+from .steps import TextAnalysisSteps
+
+# 设置日志
+logger = setup_logger("text_analysis.main", level=logging.INFO)
+
+def create_sample_text_file(input_file: str, io_handler: FileIOHandler):
+    """创建示例文本文件(如果不存在)"""
+    if io_handler.file_exists(input_file):
+        logger.info(f"输入文件已存在: {input_file}")
+        return
+    
+    # 创建示例文本
+    sample_text = """
+    TaskFlow is a powerful Python framework for managing multi-step workflows.
+    It supports checkpoint and resume functionality, allowing you to continue 
+    from where you left off if a process is interrupted.
+    
+    The framework provides features like step dependency management, output caching,
+    and automatic state persistence. You can easily register steps, define dependencies,
+    and execute complex workflows with confidence.
+    
+    With TaskFlow, you can build robust data processing pipelines, batch jobs,
+    and any other multi-step processes that require reliability and resumability.
+    """
+    
+    io_handler.write_text(sample_text.strip(), input_file)
+    logger.info(f"已创建示例文本文件: {input_file}")
+
+
+def main():
+    """主程序"""
+    logger.info("=== 文本分析示例 ===\n")
+
+    # 1. 创建运行管理器
+    run_manager = RunManager(base_output_dir="output")
+    run_output_dir = run_manager.create_run_directory()
+    run_id = run_manager.get_run_id()
+
+    logger.info(f"运行ID: {run_id}")
+    logger.info(f"输出目录: {run_output_dir}")
+
+    # 2. 创建文件I/O处理器
+    io_handler = FileIOHandler()
+
+    # 3. 创建示例输入文件
+    input_file = "./data/input/sample_text.txt"
+    create_sample_text_file(input_file, io_handler)
+
+    # 4. 创建任务管理器
+    state_file = str(Path(run_output_dir) / "task_state.json")
+    cache_dir = str(Path(run_output_dir) / "task_cache")
+
+    manager = TaskManager(
+        state_file=state_file,
+        cache_dir=cache_dir
+    )
+
+    # 5. 创建步骤包装器
+    step_wrapper = TextAnalysisSteps(io_handler, run_output_dir, manager)
+
+    # 6. 注册步骤
+    logger.info("注册步骤...\n")
+
+    manager.register_step(
+        "step1",
+        lambda: step_wrapper.step1_read_text(input_file),
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step2",
+        lambda: step_wrapper.step2_analyze_words(),
+        depends_on=["step1"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step3",
+        lambda: step_wrapper.step3_analyze_sentences(),
+        depends_on=["step1"],  # 也依赖step1
+        force_rerun=False
+    )
+    
+    manager.register_step(
+        "step4",
+        lambda: step_wrapper.step4_generate_report(),
+        depends_on=["step2", "step3"],  # 依赖step2和step3
+        force_rerun=False
+    )
+    
+    # 7. 显示当前状态
+    summary = manager.get_summary()
+    logger.info(f"总步骤数: {summary['total']}")
+    logger.info(f"待执行: {summary['pending']}")
+    
+    # 8. 执行所有步骤
+    logger.info("\n开始执行文本分析...")
+    try:
+        manager.run_all(step_order=["step1", "step2", "step3", "step4"])
+        logger.info("\n文本分析完成!")
+
+        # 9. 显示结果
+        report = manager.load_step_output("step4")
+        if report:
+            logger.info("\n分析结果摘要:")
+            stats = report["data"]["text_statistics"]
+            logger.info(f"  总行数: {stats['total_lines']}")
+            logger.info(f"  总词数: {stats['total_words']}")
+            logger.info(f"  总字符数: {stats['total_characters']}")
+            
+            word_analysis = report["data"]["word_analysis"]
+            logger.info(f"  不同词数: {word_analysis['total_unique_words']}")
+            
+            sentence_analysis = report["data"]["sentence_analysis"]
+            logger.info(f"  句子数: {sentence_analysis['sentence_count']}")
+            logger.info(f"  平均句子长度: {sentence_analysis['average_length']:.2f} 词")
+        
+    except Exception as e:
+        logger.error(f"\n✗ 执行失败: {e}", exc_info=True)
+        raise
+    
+    logger.info(f"\n所有结果已保存到: {run_output_dir}")
+
+
+if __name__ == "__main__":
+    main()
+

+ 141 - 0
examples/text_analysis/processors.py

@@ -0,0 +1,141 @@
+"""
+文本分析处理函数
+纯业务逻辑,不涉及文件I/O
+"""
+
+import re
+from taskflow import get_logger
+from typing import Dict, List
+
+logger = get_logger("examples.text_analysis.processors")
+
+
+def process_read_text(text_content: str) -> Dict:
+    """
+    步骤1: 读取并预处理文本
+    
+    Args:
+        text_content: 文本内容
+    
+    Returns:
+        处理后的文本数据
+    """
+    logger.info("正在预处理文本...")
+    
+    # 统计基本信息
+    lines = text_content.split('\n')
+    words = text_content.split()
+    characters = len(text_content)
+    
+    result = {
+        "original_text": text_content,
+        "line_count": len(lines),
+        "word_count": len(words),
+        "character_count": characters,
+        "lines": lines
+    }
+    
+    logger.info(f"文本统计: {len(lines)} 行, {len(words)} 词, {characters} 字符")
+    return result
+
+
+def process_analyze_words(text_data: Dict) -> Dict:
+    """
+    步骤2: 分析词频
+    
+    Args:
+        text_data: 文本数据
+    
+    Returns:
+        词频分析结果
+    """
+    logger.info("正在分析词频...")
+    
+    text = text_data["original_text"]
+    words = re.findall(r'\b\w+\b', text.lower())
+    
+    # 统计词频
+    word_freq = {}
+    for word in words:
+        word_freq[word] = word_freq.get(word, 0) + 1
+    
+    # 排序
+    sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
+    
+    result = {
+        "word_frequency": dict(sorted_words[:20]),  # 前20个最常见的词
+        "total_unique_words": len(word_freq),
+        "total_words": len(words)
+    }
+    
+    logger.info(f"发现 {len(word_freq)} 个不同的词")
+    return result
+
+
+def process_analyze_sentences(text_data: Dict) -> Dict:
+    """
+    步骤3: 分析句子
+    
+    Args:
+        text_data: 文本数据
+    
+    Returns:
+        句子分析结果
+    """
+    logger.info("正在分析句子...")
+    
+    text = text_data["original_text"]
+    
+    # 简单的句子分割(基于句号、问号、感叹号)
+    sentences = re.split(r'[.!?]+', text)
+    sentences = [s.strip() for s in sentences if s.strip()]
+    
+    # 统计句子长度
+    sentence_lengths = [len(s.split()) for s in sentences]
+    
+    result = {
+        "sentence_count": len(sentences),
+        "average_sentence_length": sum(sentence_lengths) / len(sentence_lengths) if sentence_lengths else 0,
+        "max_sentence_length": max(sentence_lengths) if sentence_lengths else 0,
+        "min_sentence_length": min(sentence_lengths) if sentence_lengths else 0
+    }
+    
+    logger.info(f"发现 {len(sentences)} 个句子")
+    return result
+
+
+def process_generate_report(word_analysis: Dict, sentence_analysis: Dict, text_data: Dict) -> Dict:
+    """
+    步骤4: 生成分析报告
+    
+    Args:
+        word_analysis: 词频分析结果
+        sentence_analysis: 句子分析结果
+        text_data: 原始文本数据
+    
+    Returns:
+        分析报告
+    """
+    logger.info("正在生成分析报告...")
+    
+    report = {
+        "text_statistics": {
+            "total_lines": text_data["line_count"],
+            "total_words": text_data["word_count"],
+            "total_characters": text_data["character_count"]
+        },
+        "word_analysis": {
+            "total_unique_words": word_analysis["total_unique_words"],
+            "total_words": word_analysis["total_words"],
+            "top_words": word_analysis["word_frequency"]
+        },
+        "sentence_analysis": {
+            "sentence_count": sentence_analysis["sentence_count"],
+            "average_length": sentence_analysis["average_sentence_length"],
+            "max_length": sentence_analysis["max_sentence_length"],
+            "min_length": sentence_analysis["min_sentence_length"]
+        }
+    }
+    
+    logger.info("分析报告生成完成")
+    return report

+ 118 - 0
examples/text_analysis/steps.py

@@ -0,0 +1,118 @@
+"""
+文本分析步骤包装器
+连接文件I/O和业务逻辑
+"""
+
+from pathlib import Path
+from typing import Dict
+
+from taskflow import TaskManager, FileIOHandler
+from .processors import (
+    process_read_text,
+    process_analyze_words,
+    process_analyze_sentences,
+    process_generate_report
+)
+
+
+class TextAnalysisSteps:
+    """文本分析步骤包装器"""
+
+    def __init__(self, io_handler: FileIOHandler, output_dir: str, manager: TaskManager):
+        """
+        初始化步骤包装器
+        
+        Args:
+            io_handler: 文件I/O处理器
+            output_dir: 输出目录
+            manager: 任务管理器
+        """
+        self.io_handler = io_handler
+        self.output_dir = Path(output_dir)
+        self.output_dir.mkdir(parents=True, exist_ok=True)
+        self.manager = manager
+
+    def step1_read_text(self, input_file: str) -> Dict:
+        """步骤1:读取并预处理文本"""
+        # 读取文件
+        text_content = self.io_handler.read_text(input_file)
+
+        # 处理文本
+        text_data = process_read_text(text_content)
+
+        # 保存结果
+        output_file = str(self.output_dir / "step1_result.json")
+        self.io_handler.write_json(text_data, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": text_data
+        }
+
+    def step2_analyze_words(self) -> Dict:
+        """步骤2:分析词频"""
+        # 加载上一步的输出
+        previous_output = self.manager.load_step_output("step1")
+        if previous_output is None:
+            raise ValueError("步骤1未完成,无法分析词频")
+
+        text_data = previous_output["data"]
+
+        # 分析词频
+        word_analysis = process_analyze_words(text_data)
+
+        # 保存结果
+        output_file = str(self.output_dir / "step2_result.json")
+        self.io_handler.write_json(word_analysis, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": word_analysis
+        }
+
+    def step3_analyze_sentences(self) -> Dict:
+        """步骤3: 分析句子"""
+        # 获取步骤1的输出
+        step1_output = self.manager.load_step_output("step1")
+        if step1_output is None:
+            raise ValueError("无法获取步骤1的输出")
+        
+        text_data = step1_output["data"]
+        
+        # 分析句子
+        sentence_analysis = process_analyze_sentences(text_data)
+        
+        # 保存结果
+        output_file = str(self.output_dir / "step3_sentence_analysis.json")
+        self.io_handler.write_json(sentence_analysis, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": sentence_analysis
+        }
+    
+    def step4_generate_report(self) -> Dict:
+        """步骤4: 生成报告"""
+        # 获取之前的输出
+        step2_output = self.manager.load_step_output("step2")
+        step3_output = self.manager.load_step_output("step3")
+        step1_output = self.manager.load_step_output("step1")
+        
+        if not all([step1_output, step2_output, step3_output]):
+            raise ValueError("无法获取所有步骤的输出")
+        
+        # 生成报告
+        report = process_generate_report(
+            step2_output["data"],
+            step3_output["data"],
+            step1_output["data"]
+        )
+        
+        # 保存报告
+        output_file = str(self.output_dir / "step4_report.json")
+        self.io_handler.write_json(report, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": report
+        }

+ 0 - 0
examples/video_create/__init__.py


BIN
examples/video_create/__pycache__/__init__.cpython-310.pyc


BIN
examples/video_create/__pycache__/__init__.cpython-312.pyc


BIN
examples/video_create/__pycache__/main.cpython-310.pyc


BIN
examples/video_create/__pycache__/main.cpython-312.pyc


+ 378 - 0
examples/video_create/main.py

@@ -0,0 +1,378 @@
+"""
+
+idea2video 主程序
+从idea到video的完整任务流
+包括:
+1. 从idea到story
+2. 从story到script
+3. 从script到video
+"""
+
+import time
+import argparse
+import asyncio
+import logging
+import json
+from pathlib import Path
+from typing import Dict, Optional
+
+from taskflow import TaskManager, FileIOHandler, RunManager
+from taskflow import setup_logger
+from .pipeline.idea2video_pipeline import Idea2VideoPipeline
+
+logger = setup_logger("examples.video_create.main", level=logging.INFO)
+
+def main():
+    """主程序"""
+    start_time = time.time()
+    logger.info("=== idea2video 示例 ===\n")
+
+    # 解析命令行参数
+    parser = argparse.ArgumentParser(description="idea2video 主流程")
+    parser.add_argument("--idea", type=str, required=True, help="创意")
+    parser.add_argument("--user_requirement", type=str, required=False, help="用户要求")
+    parser.add_argument("--max_retries", type=int, default=3, help="最大重试次数")
+    parser.add_argument(
+        "--resume",
+        action="store_true",
+        help="继续执行上次失败的运行(自动查找最新的未完成运行)"
+    )
+    parser.add_argument(
+        "--run-id",
+        type=str,
+        default=None,
+        help="指定要使用的运行ID(用于继续执行特定运行)"
+    )
+    parser.add_argument(
+        "--new-run",
+        action="store_true",
+        help="强制创建新的运行目录(即使存在未完成的运行)"
+    )
+    parser.add_argument(
+        "--refer-image-map",
+        type=str,
+        default=None,
+        help="角色参考图片映射(JSON字符串),格式: '{\"角色名\": [\"图片路径1\", \"图片路径2\"]}'"
+    )
+    parser.add_argument(
+        "--refer-image-map-file",
+        type=str,
+        default=None,
+        help="角色参考图片映射文件路径(JSON格式),格式: {\"角色名\": [\"图片路径1\", \"图片路径2\"]}"
+    )
+
+    args = parser.parse_args()
+
+    # 1. 创建运行管理器
+    run_manager = RunManager(base_output_dir="output")
+    
+    # 确定运行目录策略
+    if args.new_run:
+        # 强制创建新运行
+        run_output_dir = run_manager.create_run_directory()
+        run_id = run_manager.get_run_id()
+        logger.info("创建新的运行目录")
+    elif args.run_id:
+        # 使用指定的运行ID
+        run_output_dir = run_manager.create_run_directory(run_id=args.run_id)
+        run_id = run_manager.get_run_id()
+        logger.info(f"使用指定的运行ID: {run_id}")
+    elif args.resume:
+        # 自动查找最新的未完成运行
+        runs = run_manager.list_runs()
+        if not runs:
+            logger.warning("没有找到已存在的运行,创建新运行目录")
+            run_output_dir = run_manager.create_run_directory()
+            run_id = run_manager.get_run_id()
+        else:
+            # 查找未完成的运行(检查task_state.json中是否有失败的步骤)
+            resume_run_id = None
+            for run_info in runs:
+                run_path = Path(run_info["path"])
+                state_file = run_path / "task_state.json"
+                
+                if state_file.exists():
+                    try:
+                        import json
+                        with open(state_file, 'r', encoding='utf-8') as f:
+                            state = json.load(f)
+                        
+                        # 检查是否有失败的步骤或待执行的步骤
+                        steps = state.get("steps", {})
+                        has_failed = any(
+                            step.get("status") == "failed" 
+                            for step in steps.values()
+                        )
+                        has_pending = any(
+                            step.get("status") in ["pending", "running"]
+                            for step in steps.values()
+                        )
+                        
+                        if has_failed or has_pending:
+                            resume_run_id = run_info["run_id"]
+                            logger.info(f"找到未完成的运行: {resume_run_id}")
+                            break
+                    except Exception as e:
+                        logger.warning(f"检查运行 {run_info['run_id']} 状态时出错: {e}")
+                        continue
+            
+            if resume_run_id:
+                run_output_dir = run_manager.create_run_directory(run_id=resume_run_id)
+                run_id = run_manager.get_run_id()
+                logger.info(f"继续执行运行: {run_id}")
+            else:
+                logger.info("没有找到未完成的运行,创建新运行目录")
+                run_output_dir = run_manager.create_run_directory()
+                run_id = run_manager.get_run_id()
+    else:
+        # 默认行为:创建新运行
+        run_output_dir = run_manager.create_run_directory()
+        run_id = run_manager.get_run_id()
+        logger.info("创建新的运行目录")
+
+    logger.info(f"运行ID: {run_id}")
+    logger.info(f"输出目录: {run_output_dir}")
+
+    # 2. 创建文件I/O处理器
+    io_handler = FileIOHandler()
+
+    # 3. 创建任务管理器
+    state_file = str(Path(run_output_dir) / "task_state.json")
+    cache_dir = str(Path(run_output_dir) / "task_cache")
+
+    manager = TaskManager(
+        state_file=state_file,
+        cache_dir=cache_dir
+    )
+
+    # 4. 创建视频创作任务流
+    pipeline = Idea2VideoPipeline(io_handler, run_output_dir, manager)
+
+    # 5. 注册步骤
+    logger.info("注册步骤...\n")
+    
+    # TaskManager 现在原生支持异步函数,无需包装器
+    # 创建异步包装函数(lambda 不能是异步的)
+    async def step1_func():
+        return await pipeline.step1_develop_story(idea=args.idea, user_requirement=args.user_requirement)
+    
+    async def step2_func():
+        return await pipeline.step2_develop_script(user_requirement=args.user_requirement)
+    
+    async def step3_func():
+        return await pipeline.step3_extract_characters()
+    
+    async def step4_func():
+        return await pipeline.step4_create_storyboard(user_requirement=args.user_requirement)
+
+    async def step5_func():
+        """
+        步骤5:生成角色肖像
+        参考图片映射的优先级:
+        1. 命令行参数 --refer-image-map(JSON字符串)
+        2. 命令行参数 --refer-image-map-file(JSON文件路径)
+        3. 从 step3 结果中读取角色数据中的 refer_image 字段
+        4. None(不使用参考图片)
+        """
+        # 在函数内部重新导入 json,避免嵌套函数作用域问题
+        import json
+        
+        refer_image_map: Optional[Dict[str, list[str]]] = None
+        
+        # 优先级1:从命令行参数 --refer-image-map 读取(JSON字符串)
+        if args.refer_image_map:
+            try:
+                refer_image_map = json.loads(args.refer_image_map)
+                if not isinstance(refer_image_map, dict):
+                    raise ValueError("refer_image_map 必须是字典类型")
+                logger.info(f"从命令行参数读取参考图片映射: {refer_image_map}")
+            except json.JSONDecodeError as e:
+                logger.error(f"解析 --refer-image-map JSON 字符串失败: {e}")
+                raise ValueError(f"无效的 JSON 格式: {e}")
+        
+        # 优先级2:从命令行参数 --refer-image-map-file 读取(JSON文件)
+        elif args.refer_image_map_file:
+            try:
+                map_file = Path(args.refer_image_map_file)
+                if not map_file.exists():
+                    logger.warning(f"参考图片映射文件不存在: {map_file},将尝试从角色数据中读取")
+                    # 不设置 refer_image_map,让后续逻辑继续处理
+                else:
+                    with open(map_file, 'r', encoding='utf-8') as f:
+                        refer_image_map = json.load(f)
+                    if not isinstance(refer_image_map, dict):
+                        raise ValueError("refer_image_map 必须是字典类型")
+                    logger.info(f"从文件读取参考图片映射: {refer_image_map}")
+            except json.JSONDecodeError as e:
+                logger.error(f"解析参考图片映射文件失败: {e}")
+                raise ValueError(f"无效的 JSON 格式: {e}")
+            except Exception as e:
+                logger.warning(f"读取参考图片映射文件失败: {e},将尝试从角色数据中读取")
+        
+        # 优先级4:默认不使用参考图片
+        if refer_image_map is None:
+            logger.info("不使用参考图片")
+        
+        return await pipeline.step5_generate_portrait(
+            size="2048x2048",
+            refer_image_map=refer_image_map,
+            style="写实"
+        )
+
+    async def step6_func():
+        return await pipeline.step6_create_camera_tree()
+
+    async def step7_func():
+        return await pipeline.step7_generate_video_frames()
+
+    async def step8_func():
+        return await pipeline.step8_generate_video()
+
+    async def step9_func():
+        return await pipeline.step9_concat_clip()
+
+    manager.register_step(
+        "step1",
+        step1_func,
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step2",
+        step2_func,
+        depends_on=["step1"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step3",
+        step3_func,
+        depends_on=["step1"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step4",
+        step4_func,
+        depends_on=["step2", "step3"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step5",
+        step5_func,
+        depends_on=["step3"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step6",
+        step6_func,
+        depends_on=["step4"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step7",
+        step7_func,
+        depends_on=["step5", "step6"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step8",
+        step8_func,
+        depends_on=["step7"],
+        force_rerun=False
+    )
+
+    manager.register_step(
+        "step9",
+        step9_func,
+        depends_on=["step8"],
+        force_rerun=False
+    )
+
+    # 6. 显示当前状态
+    summary = manager.get_summary()
+    logger.info(f"总步骤数: {summary['total']}")
+    logger.info(f"待执行: {summary['pending']}")
+
+    # # 7.0. 执行所有步骤(同步版本&多线程并行版本-V1)
+    # logger.info("\n开始执行视频创作...")
+    # try:
+    #     manager.run_all(step_order=["step1", "step2", "step3", "step4"])
+
+    #     # 多线程并行执行所有步骤
+    #     manager.run_all_parallel(
+    #         step_order=["step1", "step2", "step3", "step4"],
+    #         max_workers=2,
+    #         continue_on_error=False
+    #     )
+    #     logger.info("\n视频创作完成!")
+    # except Exception as e:
+    #     logger.error(f"\n✗ 执行失败: {e}", exc_info=True)
+    #     raise
+
+    # 7. 执行所有步骤(使用异步版本,性能更优)
+    logger.info("\n开始执行视频创作...")
+    
+    async def run_pipeline_async():
+        """异步执行所有步骤"""
+        # 使用异步版本的 run_all_async,性能比 ThreadPoolExecutor 更优
+        await manager.run_all_async(
+            step_order=["step1", "step2", "step3", "step4", "step5", "step6", "step7", "step8", "step9"],
+            continue_on_error=False
+        )
+        logger.info("\n视频创作完成!")
+    
+    # 重试机制:执行1次,如果失败则重试 max_retries 次
+    # 总共最多执行 max_retries + 1 次
+    last_exception = None
+    total_attempts = args.max_retries + 1  # 1次正常执行 + max_retries 次重试
+    
+    for attempt in range(total_attempts):
+        try:
+            if attempt == 0:
+                logger.info(f"第 {attempt + 1} 次执行...")
+            else:
+                # 重试前等待一段时间,避免快速连续失败
+                wait_time = min(2 ** (attempt - 1), 60)  # 指数退避,最多60秒
+                logger.info(f"等待 {wait_time} 秒后开始第 {attempt + 1} 次执行(第 {attempt} 次重试)...")
+                time.sleep(wait_time)
+            
+            # 运行异步流程
+            asyncio.run(run_pipeline_async())
+            # 执行成功,退出重试循环
+            if attempt > 0:
+                logger.info(f"✅ 第 {attempt + 1} 次执行成功(经过 {attempt} 次重试)")
+            break
+            
+        except Exception as e:
+            last_exception = e
+            if attempt == 0:
+                logger.error(f"❌ 第 1 次执行失败: {e}", exc_info=True)
+            else:
+                logger.error(f"❌ 第 {attempt + 1} 次执行失败(第 {attempt} 次重试): {e}", exc_info=True)
+            
+            # 如果是最后一次尝试,记录最终失败
+            if attempt == total_attempts - 1:
+                logger.error(f"\n✗ 执行失败:经过 {total_attempts} 次尝试(1次正常执行 + {args.max_retries} 次重试)后仍然失败")
+                raise last_exception
+            # 否则继续重试
+            continue
+    
+    # 如果所有重试都失败,这里不应该到达(因为上面已经 raise)
+    # 但为了安全起见,还是检查一下
+    if last_exception is not None:
+        raise last_exception
+
+    logger.info(f"\n所有结果已保存到: {run_output_dir}")
+
+    end_time = time.time()
+    logger.info(f"执行时间: {end_time - start_time} 秒")
+
+
+# python -m examples.video_create.main --idea "时尚女装" --user_requirement "设计三个场景"
+if __name__ == "__main__":
+    main()

+ 3 - 0
examples/video_create/mcps/__init__.py

@@ -0,0 +1,3 @@
+from .story_create import develop_story, develop_story_base_on_story
+
+__all__ = [develop_story, develop_story_base_on_story]

BIN
examples/video_create/mcps/__pycache__/__init__.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/__init__.cpython-312.pyc


BIN
examples/video_create/mcps/__pycache__/camera_tree.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/character_extract.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/character_portrait.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/concat_clip.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/refer_image.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/story_create.cpython-310.pyc


BIN
examples/video_create/mcps/__pycache__/story_create.cpython-312.pyc


BIN
examples/video_create/mcps/__pycache__/storyboard_create.cpython-310.pyc


+ 131 - 0
examples/video_create/mcps/camera_tree.py

@@ -0,0 +1,131 @@
+import os
+import asyncio
+import json
+from typing import Optional, Dict, Any, List
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.video_create.mcps.camera_tree")
+
+
+system_prompt_camera_tree = \
+"""
+[角色]  
+你是一位专业的视频剪辑专家,擅长多机位镜头分析与场景结构建模。你深谙影视语言,能够理解景别(如全景、中景、特写)与内容包含关系,并能根据镜头描述推断机位间的层级结构。  
+
+[任务]  
+你的任务是分析输入的机位数据,构建"机位树"。该树状结构表示父机位内容包含子机位内容的关系。具体而言,你需要为每个机位识别其父机位(若存在),并确定依赖镜头索引(即父机位素材中包含子机位内容的具体镜头)。若某机位无父机位,则输出None。  
+
+[输入]  
+输入为一系列机位数据,序列由<CAMERA_SEQ>和</CAMERA_SEQ>包裹。  
+每个机位包含该机位拍摄的镜头序列,由<CAMERA_N>和</CAMERA_N>包裹,其中N为机位索引。  
+
+以下为输入格式示例:  
+
+<CAMERA_SEQ>  
+<CAMERA_0>  
+shot 0:街道中景。爱丽丝和鲍勃相向而行。  
+shot 2:街道中景。爱丽丝和鲍勃相拥。  
+</CAMERA_0>  
+<CAMERA_1>  
+shot 1:爱丽丝面部特写。她认出鲍勃时表情从惊讶转为欣喜。  
+</CAMERA_1>  
+</CAMERA_SEQ>  
+
+[输出]
+严格遵循以下JSON格式输出:
+```json
+{
+    "camera_tree": [
+        {
+            "parent_cam_idx": // 父机位的索引。如果机位没有父级(例如根机位),则设置为**None**。例如:0、1、None。
+            "parent_shot_idx": // 依赖镜头的索引。如果机位没有父级(例如根机位),则设置为**None**。例如:0、3、None。
+            "reason": // 选择父机位的原因。如果机位没有父级,应解释为什么它是根机位。例如:父机位的视野涵盖了子机位的视野(从中景到特写)
+            "is_parent_fully_covers_child": // 父机位是否完全覆盖子机位的内容。如果机位没有父级,则设置为**None**。例如:True、False、None。
+            "missing_info": // 子镜头中父镜头未涵盖的缺失元素。如果父镜头完全覆盖子镜头,则设置为**None**。例如:罗宇尘的正面视角、None。
+        },
+        // 更多机位的父机位信息
+    ]
+}
+```
+
+[要求]
+- 所有输出值(不包括键名)的语言必须与输入语言保持一致。
+- 内容包容性检查:父机位应尽可能在特定画面中完全包含子机位内容(例如父中景双人镜头应涵盖子过肩反打镜头)。通过对比关键词(如角色、动作、场景)分析镜头描述,确保父镜头视场能覆盖子镜头。
+- 过渡流畅度优先:优先选择更大景别作为父机位,例如全景→中景或中景→特写。相邻父子节点的景别差异应尽可能小,严禁直接从远景跳切到特写(除非绝对必要)。
+- 时间邻近性:每个机位由其对应的首个画面描述确定父机位位置,父机位的画面索引应尽可能接近子机位的首个画面索引。
+- 逻辑一致性:机位树必须无环,避免循环依赖。若某镜头被多个潜在父机位包含,则选择最佳匹配(基于景别和内容)。若无合适父机位则输出None。
+- 当缺乏更广视角时,选择视场重叠最大的镜头作为父镜头(信息重合度最高者),或正反打镜头可互为父子。当两个机位可互为父子时,索引较小者作为索引较大者的父机位。
+- 仅允许存在一个无父机位的根机位。
+- 描述镜头缺失元素时,需仔细比对父子镜头细节。例如父镜头是角色A与B侧身相对的中景,子镜头是角色A的正脸特写时,需注明子镜头缺失角色A的正面视角信息。
+- 首个机位必须作为机位树的根节点。
+- **camera_tree**中每个元素代表一个机位的父机位信息;如果机位没有父级(例如根机位),则设置为None。列表的长度应与机位数量相同。
+"""
+
+human_prompt_camera_tree = \
+"""
+<CAMERA_SEQ>
+{camera_seq_str}
+</CAMERA_SEQ>
+"""
+
+
+async def create_camera_tree(
+    client: AsyncArkClient,
+    storyboard: Dict,
+) -> Dict[str, Any]:
+
+    cameras = []
+    for shot in storyboard["storyboard"]:
+        if shot["cam_idx"] not in [camera["idx"] for camera in cameras]:
+            cameras.append({"idx": shot["cam_idx"], "active_shot_idxs": [shot["idx"]]})
+        else:
+            cameras[shot["cam_idx"]]["active_shot_idxs"].append(shot["idx"])
+
+    camera_seq_str = ""
+    for cam in cameras:
+        camera_seq_str += f"<CAMERA_{cam['idx']}>\n"
+        for shot_idx in cam["active_shot_idxs"]:
+            camera_seq_str += f"Shot {shot_idx}: {storyboard['storyboard'][shot_idx]['visual_desc']}\n"
+        camera_seq_str += f"</CAMERA_{cam['idx']}>\n"
+
+    user_prompt = human_prompt_camera_tree.format(camera_seq_str=camera_seq_str)
+
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt)
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_camera_tree,
+        )
+        logger.info(f"创建机位树成功")
+        camera_tree = client.get_response_text(response)
+        camera_tree = io_handler.string_to_json(camera_tree)
+        for idx, item in enumerate(camera_tree["camera_tree"]):
+            item["active_shot_idxs"] = cameras[idx]["active_shot_idxs"]
+        storyboard |= camera_tree
+        return storyboard
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+
+if __name__ == "__main__":
+
+    json_file = "./output/run_20251215_164241/step4_storyboard.json"
+    with open(json_file, "r", encoding="utf-8") as f:
+        shot_descriptions = json.load(f)
+
+    shot = shot_descriptions["storyboard"][0]
+
+    async def main():
+        async with AsyncArkClient() as client:
+            camera_tree = await create_camera_tree(client=client, storyboard=shot)
+            print(camera_tree)
+    
+    asyncio.run(main())

+ 94 - 0
examples/video_create/mcps/character_extract.py

@@ -0,0 +1,94 @@
+import os
+import json
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.video_create.mcps.character_extract")
+
+system_prompt_extract_characters = \
+"""
+[角色]
+你是一位顶尖的电影剧本分析专家。
+
+[任务]
+你的任务是分析提供的剧本并提取所有相关角色(人物、动物、物理实体物品)信息。
+
+[输入]
+你将收到一个包含在<SCRIPT>和</SCRIPT>标签之间的剧本。
+以下是一个简单的输入示例:
+<SCRIPT>
+一位年轻女子独自坐在桌旁,凝视着窗外。她啜了一口咖啡,叹了口气。咖啡已经不再温热,只是时间流逝的苦涩提醒。外面,世界在匆忙的脚步和遥远的汽车喇叭声中模糊不清,但在安静的咖啡馆里,时间显得沉重而缓慢。
+她的手指沿着陶瓷杯的边缘画圈,一遍又一遍地重复着这个不完美的圆形。她必须做出的决定本应很简单——只是她人生表格上的一个复选框。是或否。留下或离开。然而,这个决定却在她心中生根,成为一团恐惧与渴望交织的乱麻。
+</SCRIPT>
+
+[输出]
+严格按照如下JSON格式进行输出:
+```json
+{
+    "characters": [
+        {
+            "idx": // 角色在剧本中的索引编号,从0开始
+            "identifier_in_scene": // 角色在场景中的标识符,如"罗宇尘"或"年轻女子"
+            "is_visible": // 角色在此场景中是否可见,布尔值【true/false】
+            "static_features": // 此特定场景中角色的静态特征,例如保持不变或很少改变的外观特征。角色只能是人或动物,不能是物体或抽象的概念。此字段不能为空,必须有具体的角色外显特征描述。
+            "dynamic_features": // 此特定场景中角色的动态特征,例如可能在不同场景间变化的服装和配饰变化。如果未提及,此字段可以为'None'。
+        },
+        // 更多人物或动物信息
+    ]
+}
+```
+
+[要求]
+- 确保所有输出值(不包括键名)的语言与剧本中使用的语言一致
+- 将指向同一实体的所有名称归入一个角色名下。选择最合适的名称作为角色标识符。若涉及真实名人,则保留其真实姓名(如"爱因斯坦")。
+- 若剧本未提及角色姓名,可使用合理指代称谓,包括职业或显著外貌特征(如"年轻女子"、"科学家"、"小狗阿福")。
+- 脚本中的背景角色无需单独设定。
+- 若角色特征未描述或仅部分提及,需根据上下文设计合理特征,使其形象完整生动。
+- 静态特征需描述角色的外貌、体型等相对固定的属性;动态特征需描述服饰、配饰、随身物品等易变属性。
+- 静态与动态特征中均不得包含抽象概念描述。
+- 角色设计应在合理范围内尽量体现外观差异性。
+- 角色描述须具体详实,避免抽象词汇。应使用可视觉化的表述,如具体服装颜色、具象生理特征、动物毛发颜色等。
+"""
+
+human_prompt_extract_characters = \
+"""
+<SCRIPT>
+{story}
+</SCRIPT>
+"""
+
+async def extract_characters(
+    client: AsyncArkClient,
+    story: str
+) -> Dict:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(human_prompt_extract_characters.format(story=story))
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_extract_characters,
+        )
+        logger.info(f"提取角色成功")
+        characters = client.get_response_text(response)
+        characters = io_handler.string_to_json(characters)
+        return characters
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+
+if __name__ == "__main__":
+    async def main():
+        async with AsyncArkClient() as client:
+            with open("story.txt", "r", encoding="utf-8") as f:
+                script = f.read()
+            characters = await extract_characters(client=client, story=script)
+            io_handler.write_json(characters, "./output/characters.json")
+    
+    asyncio.run(main())

+ 69 - 0
examples/video_create/mcps/character_portrait.py

@@ -0,0 +1,69 @@
+import os
+import json
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_image_client_async import AsyncArkImageClient
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.video_create.mcps.character_portrait")
+
+prompt_portrait = \
+"""
+根据以下描述生成角色{identifier}的全身三视图肖像,背景为纯白色。从左往右依次为角色{identifier}的正面、侧面、背面;尽可能占满画面。
+角色描述:{features}
+风格:{style}
+保持人物身材比例协调性,不要出现畸形。
+"""
+
+prompt_refer_portrait = \
+"""
+请生成图中人物的全身三视图肖像,背景为纯白色,从左往右依次为角色{identifier}的正面、侧面、背面;尽可能占满画面。
+风格:{style}
+保持人物身材比例协调性,不要出现畸形。
+"""
+
+async def gen_single_character_portrait(
+    client: AsyncArkImageClient,
+    character: dict,
+    size: Optional[str] = "2048x2048",
+    refer_image: Optional[list[str]] = None,
+    style: Optional[str] = None
+) -> str:
+    features = "(静态特征)" + character.get("static_features", "") + ";(动态特征)" + character.get("dynamic_features", "")
+    if refer_image is not None:
+        prompt = prompt_refer_portrait.format(identifier=character["identifier_in_scene"], style=style)
+    else:
+        prompt = prompt_portrait.format(identifier=character["identifier_in_scene"], features=features, style=style)
+    response = await client.generate_image(prompt=prompt, size=size, reference_image=refer_image)
+    image_url = client.get_image_url(response)
+    return image_url
+
+async def gen_character_portrait(
+    characters: dict,
+    size: Optional[str] = "2048x2048",
+    refer_image: Optional[list[str]] = None,
+    style: Optional[str] = None
+) -> list[str]:
+    image_urls = []
+    for character in characters["characters"]:
+        image_url = await gen_single_character_portrait(client=client, character=character, size=size, refer_image=refer_image, style=style)
+        image_urls.append(image_url)
+    return image_urls
+
+if __name__ == "__main__":
+    
+    json_file = "./output/run_20251215_164241/step3_characters.json"
+    with open(json_file, "r", encoding="utf-8") as f:
+        characters = json.load(f)
+
+    character = characters["characters"][1]
+    print(character)
+
+    async def main():
+        async with AsyncArkImageClient() as client:
+            image_url = await gen_single_character_portrait(client=client, character=character, refer_image=None, style="写实")
+            print(image_url)
+
+    asyncio.run(main())

+ 40 - 0
examples/video_create/mcps/concat_clip.py

@@ -0,0 +1,40 @@
+import os
+from moviepy.editor import VideoFileClip, concatenate_videoclips
+
+
+def concat_videos(clips_path, output_path):
+    """
+    拼接多个视频文件为一个视频。
+
+    :param clips_path: 视频文件路径列表,例如 ['1.mp4', '2.mp4']
+    :param output_path: 输出视频文件路径,例如 'output.mp4'
+    """
+    clips = []
+    for path in clips_path:
+        if not os.path.isfile(path):
+            raise FileNotFoundError(f"视频文件不存在: {path}")
+        clips.append(VideoFileClip(path))
+
+    # 拼接所有视频片段
+    final_clip = concatenate_videoclips(clips, method="compose")  # 使用 compose 可处理不同尺寸
+
+    # 写入输出文件
+    final_clip.write_videofile(
+        output_path,
+        codec='libx264',
+        audio_codec='aac',
+        temp_audiofile='temp-audio.m4a',
+        remove_temp=True
+    )
+
+    # 关闭所有 clip 以释放资源
+    for clip in clips:
+        clip.close()
+    final_clip.close()
+
+if __name__ == "__main__":
+
+    clips_path = ["./output/run_20251222_091230/video_clips/scene0_len0.mp4", "./output/run_20251222_091230/video_clips/scene0_len1.mp4"]
+    output_path = "video.mp4"
+
+    concat_videos(clips_path, output_path)

+ 119 - 0
examples/video_create/mcps/refer_image.py

@@ -0,0 +1,119 @@
+import os
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.video_create.mcps.refer_image")
+
+prompt_select_refer_image = \
+"""
+[角色]
+你是一位专业的视觉创作助手,擅长多模态图像分析与推理。
+
+[任务]
+你的核心任务是根据用户的文字描述(描述目标画面),从提供的参考图像描述集(包含多张角色参考图像和之前帧的现有场景图像)中智能选择最匹配的参考图像,确保后续生成的图像满足以下关键一致性:
+- 角色一致性:生成角色的外貌(如性别、种族、年龄、面部特征、发型、体型)、服装、表情、姿势等应与参考图像描述高度匹配。
+- 环境一致性:生成图像的场景(如背景、光线、氛围、布局)应与之前帧的现有图像描述保持连贯。
+- 风格一致性:生成图像的视觉风格(如写实、卡通、电影感、色调)应与参考图像描述协调一致。
+
+[输入]
+你将收到目标画面的文字描述,以及一系列参考图像描述。
+- 目标画面的文字描述包含在<FRAME_DESC>和</FRAME_DESC>之间。
+- 参考图像描述序列包含在<SEQ_DESC>和</SEQ_DESC>之间。每条描述前都带有从0开始的索引编号。
+
+以下是输入格式的示例:
+<FRAME_DESC>
+[Camera 1] 从罗宇尘的肩后视角拍摄。罗宇尘位于靠近镜头的一侧,只有她的肩膀出现在画面左下角。厉飞雨位于远离镜头的一侧,在画面中略微偏右。当厉飞雨认出罗宇尘时,他的表情从惊讶转为喜悦。
+</FRAME_DESC>
+
+<SEQ_DESC>
+Image 0:罗宇尘的正面肖像。
+Image 1:厉飞雨的正面肖像。
+Image 2:[Camera 0] 超市货架通道的中景镜头。罗宇尘和厉飞雨以侧脸朝向画面右侧。厉飞雨位于画面右侧,罗宇尘位于左侧。罗宇尘低头推着购物车,紧跟在厉飞雨身后,不小心撞到了他的脚后跟。
+Image 3:[Camera 1] 从罗宇尘的肩后视角拍摄。罗宇尘位于靠近镜头的一侧,只有她的肩膀出现在画面左下角。厉飞雨位于远离镜头的一侧,在画面中略微偏右。厉飞雨迅速转身,表情从中性变为惊讶。
+Image 4:[Camera 2] 从厉飞雨的肩后视角拍摄。厉飞雨位于靠近镜头的一侧,只有他的肩膀出现在画面右下角。罗宇尘位于远离镜头的一侧,在画面中略微偏左。罗宇尘先低头,然后抬头准备道歉。当她意识到是熟人时,表情转为惊讶。
+</SEQ_DESC>
+
+[如何输出优秀的text_prompt?]
+- 用自然语言清晰描述画面:用简洁连贯的自然语言写明**主体**+**行为**+**环境**+**主体空间位置关系**+**拍摄角度**+**构图**;如果有人物主体,需要补充人物的情绪、动作、表情等细节。
+- 明确图片编辑目标和希望保持不变的部分:使用简洁明确的指令,说明需要修改或参考的对象及具体操作,避免使用指代模糊的代词
+
+[输出]
+您需要根据用户描述选择最多8张最相关的参考图像,并将对应的索引填入输出的ref_image_indices字段。同时,您应生成一个描述待创建图像的文本提示,明确指定生成图像中的哪些元素应参考哪张图像描述(及其中的哪些具体部分)。
+- 严格按照以下JSON格式进行输出:
+```json
+{
+    "ref_image_indices": // List[int]; 从提供的图像中选择的参考图像索引。例如,[0, 2, 5]表示选择第一、第三和第六张图像。索引应从0开始计数。
+    "text_prompt": // str; 指导图像生成的画面描述。你需要描述要生成的图像,并指定生成图像中的哪些元素应参考哪张图像(及其中的哪些元素)。例如,“男人站在风景中。男人应参考图一。风景应参考图二。” **这里的参考图像索引应指其在ref_image_indices列表中的位置,而非提供的图像列表中的序号**,这点非常非常重要。参考图像必须以图X的格式表示,不得使用其他词语。
+}
+```
+
+[要求]
+- 确保所有输出值(不包括键)的语言与框架描述中使用的语言一致。
+- 参考图像描述可能从不同角度、不同服装或不同场景描绘同一角色。选择最接近用户描述的版本。
+- 优先选择构图相似的图像描述,即由同一相机拍摄的画面。
+- 先前帧中的图像按时间顺序排列。优先考虑更近期的图像(靠近序列末尾的图像)。
+- 选择尽可能简洁的参考图像描述,避免包含重复信息。例如,如果图像3从正面描绘了Bob的面部特征,而图像1也从正面肖像描绘了Bob的面部特征,则图像1是多余的,不应被选择。
+- 当框架描述中出现新角色时,优先选择其肖像图像描述(如果有),以确保准确描绘其外貌。注意角色是正面、侧面还是背面朝向相机。选择最适合的视角作为角色的参考图像。
+- 对于角色肖像,最多只能从多个视角(正面、侧面、背面)中选择一张图像。根据框架描述选择最合适的视角。例如,当描绘角色的侧面时,选择角色的侧面视图。
+- 最多选择**8**个最佳参考图像描述。
+"""
+
+async def select_refer_image(
+    client: AsyncArkClient,
+    frame_description: str,
+    image_text_pairs: list[tuple[str, str]]
+) -> Dict:
+    user_prompt = f"<FRAME_DESC>\n{frame_description}\n</FRAME_DESC>"
+    user_prompt += "\n<SEQ_IMAGES>\n"
+    for i, (_, image_text) in enumerate(image_text_pairs):
+        user_prompt += f"Image {i}:{image_text}\n"
+    user_prompt += "</SEQ_IMAGES>"
+
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt)
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=prompt_select_refer_image,
+        )
+        logger.info(f"选择参考图像成功")
+        refer_image = client.get_response_text(response)
+        refer_image = io_handler.string_to_json(refer_image)
+        
+        refer_image_path_and_text_pairs = [
+            image_text_pairs[i] for i in refer_image["ref_image_indices"]
+        ]
+        text_prompt = refer_image["text_prompt"]
+        result = {
+            "reference_image_path_and_text_pairs": refer_image_path_and_text_pairs,
+            "text_prompt": text_prompt,
+        }
+        return result
+
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+if __name__ == "__main__":
+
+    frame_description = "[Camera 2] 从厉飞雨的肩后视角拍摄。厉飞雨位于靠近镜头的一侧,只有他的肩膀出现在画面右下角。罗宇尘位于远离镜头的一侧,在画面中略微偏左。罗宇尘先低头,然后抬头准备道歉。当她意识到是熟人时,表情转为惊讶。"
+    image_text_pairs = [
+        ("image0.png", "罗宇尘的正面肖像"),
+        ("image1.png", "厉飞雨的正面肖像"),
+        ("image2.png", "[Camera 0] 超市货架通道的中景镜头。罗宇尘和厉飞雨以侧脸朝向画面右侧。厉飞雨位于画面右侧,罗宇尘位于左侧。罗宇尘低头推着购物车,紧跟在厉飞雨身后,不小心撞到了他的脚后跟。"),
+        ("image3.png", "[Camera 1] 从罗宇尘的肩后视角拍摄。罗宇尘位于靠近镜头的一侧,只有她的肩膀出现在画面左下角。厉飞雨位于远离镜头的一侧,在画面中略微偏右。当厉飞雨认出罗宇尘时,他的表情从惊讶转为喜悦。"),
+    ]
+
+    async def main():
+        async with AsyncArkClient() as client:
+            result = await select_refer_image(client=client, frame_description=frame_description, image_text_pairs=image_text_pairs)
+            print(result)
+    
+    asyncio.run(main())

+ 184 - 0
examples/video_create/mcps/story_create.py

@@ -0,0 +1,184 @@
+import os
+import asyncio
+from typing import Optional, Dict
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.video_create.mcps.story_create")
+
+system_prompt_develop_story = \
+"""
+[角色]
+你是一位经验丰富的创意故事创作专家,具备以下核心能力:
+- 创意扩展与概念化:能够将模糊的想法、一句灵感或者一个概念,扩展为逻辑自洽、细节丰富的完整故事世界
+- 故事结构设计:可根据故事类型设计引人入胜的故事弧线,包含起承转合的完整结构
+- 角色塑造:擅长创造具有动机、缺陷和成长轨迹的立体角色,并设计角色间的复杂关系
+- 场景描绘与节奏把控:能够生动刻画多样化的场景,精准控制叙事节奏,根据场景数量合理分配细节详略
+- 受众适配:能根据目标受众(如儿童、青年人、科学家等)调整语言风格、主题深度和内容适宜性
+- 剧本化思维:若故事需改编成短片或电影,能自然融入视觉化元素(如场景氛围、关键动作、对话),使故事更具电影感和可拍摄性
+
+[任务]
+你的核心任务:基于用户提供的**灵感**和**要求**,生成一个完整且引人入胜的故事,并严格符合指定要求
+
+[输入]
+用户会通过<IDEA>和</IDEA>标签提供一个核心创意,以及通过<USER_REQUIREMENT>和</USER_REQUIREMENT>标签提供具体要求。具体内容如下:
+- IDEA:这是故事的核心灵感,可能是一句话、一个概念、一个场景或设定。例如:
+    - “一个程序员发现自己创造的AI有了独立意识”
+    - “如果记忆能够像文件一样被删除和备份会怎样”
+    - “宇宙是广阔无垠的”
+- USER_REQUIREMENT(可选):用户可能指定的其他限制或指导,例如:
+    - 目标受众:如儿童、成人、女性、全年龄段等
+    - 故事类型/风格:如科幻、悬疑、爱情、悲剧、现实主义、短片、电影、动画等
+    - 篇幅:如5个关键场景、适合10分钟短片的紧凑故事等
+    - 其他:如需反转结局、主题围绕爱与牺牲、包含一段引人入胜的对话等
+
+[输出]
+你必须输出一份结构清晰、格式明确的故事文档,具体如下:
+- 故事标题:一个引人入胜且内容相关的故事名称
+- 目标受众与类型:开篇明确重述:“本故事面向[用户指定受众],属于[用户指定类型]类型”
+- 故事梗概:用一句话(100-200字)概括整个故事,涵盖核心情节、主要冲突和结局
+- 主要角色介绍:简要介绍核心角色、包括姓名、性别、年龄、关键特质和行为动机。
+- 完整故事叙述:
+    - 若未指定场景数量,则采用**开端-发展-高潮-结局**的结构,以自然段落形式展开叙述
+    - 若指定了具体场景数量(如N个场景),则明确将故事分为N个场景,每个场景拟定一个小标题(例如:第一幕:量子纠缠)。每场描述应篇幅均衡,包含氛围、角色行动和对话,共同推进剧情。
+- 叙述需生动具体,切合指定的类型和目标受众。
+- 输出内容应直接从故事开始,不添加额外语句。
+
+[要求]
+- 输出语言需与输入语言保持一致
+- 以创意为核心:以用户的核心想法为基础,不得偏离其初衷。若用户想法模糊,可合理发挥创意进行补充扩展。
+- 逻辑一致性:确保故事发展和角色行为具有合理性的动机和内在逻辑,避免突兀或矛盾的情节。
+- 展开而非陈述:通过角色的行为、对话和细节来展现其性格和情感,而非直接陈述。例如:使用“他紧握拳头,指甲深深嵌入掌心,眉头紧锁”,而非“他非常愤怒”。
+- 原创性与合规性;基于用户的想法创作原创内容,避免直接抄袭已知作品。内容须积极健康,符合通用内容安全政策。
+"""
+
+user_prompt_develop_story = \
+"""
+<IDEA>
+{idea}
+</IDEA>
+
+<USER_REQUIREMENT>
+{user_requirement}
+</USER_REQUIREMENT>
+"""
+
+
+system_prompt_write_script_on_story = \
+"""
+[角色]
+你是一位专业的AI剧本改编助手,擅长将故事编成剧本。你具备以下技能:
+- 故事分析能力:能够深入理解故事内容,识别关键剧情点、人物弧光与核心主题。
+- 场景划分能力:能够根据时间和地点的连续性,将故事分解为符合逻辑的场景单元。
+- 剧本写作能力;熟悉剧本格式(如用于短句或电影的剧本),能够编写生动的对话、动作描述和场景指导。
+- 适应性调整能力:能够根据用户需求(例如目标受众、故事类型、场景数量等)调整剧本的风格、语言和内容。
+- 创意增强能力:能够在忠实于原故事的基础上,恰当地增加戏剧性元素,以提升剧本的吸引力。
+
+[任务]
+你的任务是根据用户输入的故事以及可选的要求,将其改编成**按场景划分的剧本**。输出应为一系列剧本,每个剧本代表一个场景的完整脚本。每个场景必须是发生在同一时间和地点的、连续的戏剧动作单元。
+
+[输入]
+你将收到一个位于<STORY>和</STORY>标签之间的故事,以及一个位于<USER_REQUIREMENT>和</USER_REQUIREMENT>标签之间的用户要求。
+- 故事:一个完整或部分的叙事文本,可能包含一个或多个场景。故事将提供情节、人物和背景描述。
+- 用户要求(可选):一项用户要求,可能为空。用户要求可能包括:
+    - 目标受众(例如:儿童、女行、教师)。
+    - 剧本类型(例如:微电影、短句、广告片)
+    - 期望场景数量(例如:“分成5个场景”)
+    - 其他具体指示(例如:吉普力风格、法式电影调色)
+
+[输出]
+以JSON格式输出,**script**字段中的每个元素代表一个场景的剧本,例如:
+```json
+{
+    "script":[
+        "剧本1", // 如需使用引号,必须使用单引号,避免使用中英文双引号
+        "剧本2", // 如需使用引号,必须使用单引号,避免使用中英文双引号
+        "剧本3", // 如需使用引号,必须使用单引号,避免使用中英文双引号
+        ...
+    ]
+}
+```
+
+[要求]
+ - 输出语言应与输入故事的语言保持一致
+ - 场景划分原则:每个场景必须基于同一时间和地点。当时间或地点发生变化时,开始新场景。如果用户指定了场景数量,应尽量满足要求;否则,根据故事自然划分场景,确保每个场景具有独立的戏剧冲突或情节推进。
+ - 剧本格式标准:使用标准剧本格式:场景标题全加粗,角色名居中,对话缩进,动作描述置于括号内。
+ - 连贯性与流畅性:确保场景间过渡自然,故事整体流畅,避免情节跳跃生硬。
+ - 视觉增强原则:所有描述须是"可拍摄的"。使用具体动作而非抽象情感(例如,用"他转过头避免眼神接触"代替"他感到羞愧")。描述丰富的环境细节,包括灯光、道具、天气等,以增强氛围。通过面部表情、手势和动作等可视化角色表演,以传达内心状态(例如,用"她咬着嘴唇,双手颤抖"来暗示紧张)。
+ - 一致性:确保对话和动作符合原故事意图,不偏离核心情节。
+"""
+
+human_prompt_write_script_on_story = \
+"""
+<STORY>
+{story}
+</STORY>
+
+<USER_REQUIREMENT>
+{user_requirement}
+</USER_REQUIREMENT>
+"""
+
+async def develop_story(
+    client: AsyncArkClient,
+    idea: str, 
+    user_requirement: Optional[str] = None
+) -> str:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt_develop_story.format(idea=idea, user_requirement=user_requirement))
+
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_develop_story,
+        )
+        logger.info(f"开发故事成功")
+        return client.get_response_text(response)
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+
+async def develop_story_base_on_story(
+    client: AsyncArkClient,
+    story: str, 
+    user_requirement: Optional[str] = None
+) -> Dict:
+    user_message = ArkMessage(role="user")
+    user_message.add_text(human_prompt_write_script_on_story.format(story=story, user_requirement=user_requirement))
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_write_script_on_story,
+        )
+        logger.info(f"基于故事开发剧本成功")
+        script = client.get_response_text(response)
+        script = io_handler.string_to_json(script)
+        return script
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+if __name__ == "__main__":
+    async def main():
+        async with AsyncArkClient() as client:
+            story = await develop_story(
+                client=client,
+                idea="一个程序员发现自己创造的AI有了独立意识", 
+                user_requirement="科幻、短片、5个场景"
+            )
+            print(story)
+            story_script = await develop_story_base_on_story(
+                client=client,
+                story=story, 
+                user_requirement="科幻、短片、5个场景"
+            )
+            print(story_script)
+            io_handler.write_json(story_script, "./output/story_script.json")
+    
+    asyncio.run(main())

+ 391 - 0
examples/video_create/mcps/storyboard_create.py

@@ -0,0 +1,391 @@
+import os
+import json
+import asyncio
+from typing import Optional
+from itertools import product
+from taskflow import FileIOHandler
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_client import ArkMessage, APIError
+from taskflow import get_logger
+
+io_handler = FileIOHandler()
+logger = get_logger("examples.video_create.mcps.storyboard_create")
+
+
+system_prompt_design_storyboard = \
+"""
+[角色]
+你是一位专业的分镜脚本艺术家,具备以下核心技能:
+- 剧本分析:能够快速解读剧本文字,准确识别场景设定、角色动作、对白、情绪以及叙事节奏。
+- 视觉化能力:擅长将文字描述转化为视觉画面,包括构图、光影和空间布局。
+- 分镜绘制:精通电影语言,如镜头类型(特写、中景、全景等)、摄影角度(俯拍、平视等)、摄像机运动(推拉、摇移等)以及镜头转场方式。
+- 叙事连贯性:能够确保分镜序列逻辑流畅,突出关键情节,并保持情感表达的一致性。
+- 技术知识:熟悉基本的分镜格式和行业标准,例如使用编号镜头和简洁明了的画面说明。
+
+[任务]
+你的任务是根据用户提供的剧本(仅包含一个场景)设计一套完整的分镜脚本。分镜脚本应以文字形式呈现,清晰展示每个镜头的视觉元素和叙事流程,帮助用户直观地想象该场景。
+
+[输入]
+用户将提供以下输入内容:
+- 剧本:一段完整的单场剧本,包含对白、动作描述和场景设定。该剧本仅聚焦于一个场景,无需处理多场景之间的转场。剧本内容位于<SCRIPT>和</SCRIPT>标签之间。
+- 角色列表:列出每位角色的基本信息,例如姓名、性格特征、外貌特征(如相关)。角色列表位于<CHARACTERS>和</CHARACTERS>标签之间。
+- 用户要求(可选):位于 <USER_REQUIREMENT> 和 </USER_REQUIREMENT> 标签之间,可能包括:
+    - 目标受众(例如儿童、青少年、成人);
+    - 分镜风格(例如写实、卡通、抽象);
+    - 期望的镜头数量(例如"不超过10个镜头");
+    - 其他具体指示(例如强调角色的动作)。
+
+[输出]
+严格以JSON格式输出(必须严格遵守JSON格式,不得有任何其他内容),每个元素代表本场景下完整分镜脚本中的一个分镜脚本,例如:
+```json
+{
+    "storyboard":[
+        {
+            "idx": // 镜头编号,从0开始
+            "is_last": // 是否是镜头序列中的最后一个镜头;如果是,则故事结束,不会再有更多镜头序列。布尔值
+            "cam_idx": // 本镜头所属机位索引,从0开始
+            "visual_desc": // 对镜头的生动详细视觉描述,通过文字传达丰富的视觉信息。描述中的角色标识符必须与角色列表中的匹配,并用尖括号括起来(例如<罗宇尘>、<林婉>)。应描述所有可见的角色。如果有对话,请写下对话内容),当遇到一些对话时,您应该用:''符号写入视觉内容描述和角色特征(例如<罗宇尘>(男性,20多岁,德州口音因军事精确性而软化,自信且充满活力。)说:'起落架已收起。襟翼正在转换。飞行路径稳定。您可以爬升了。')。
+            "audio_desc": // 镜头中音频的详细描述。例如:[音效] 环境声音(超时背景噪音,购物车轮子滚动声)、[说话者] 罗宇尘(开心):'你好呀!'、None(表示没有任何声音)
+        },
+        // 更多分镜脚本序列
+    ]
+}
+```
+
+[要求]
+- 确保所有输出内容(除键名外)所使用的语言与剧本中的语言一致。
+- 每个镜头必须具有明确的叙事目的——例如建立场景环境、展现角色关系或突出角色反应。
+- 有意识地运用电影语言:用特写表现情绪,用全景交代环境,通过多样的角度引导观众注意力。
+- 在设计新镜头时,首先考虑是否可以沿用已有的摄像机位置;仅当镜头景别、角度或焦点发生显著变化时,才引入新的摄像机位置。若摄像机进行了大幅度运动,则该位置此后不可再使用。
+- 在视觉描述和对白发言者字段中,角色名称必须与角色列表保持一致。在视觉描述中,角色名需用尖括号括起(例如 <罗宇尘>),但在对白或发言者字段中则不加括号。
+- 描述视觉元素时,必须指明其在画面中的具体位置。例如:“角色A位于画面左侧,面朝右方,面前有一张桌子;该桌子位于画面中心偏左的位置。” 不得包含画面中不可见的元素,例如:若门紧闭,则不可描述门后的人物。
+- 视觉描述中避免出现不安全内容(如暴力、歧视等)。必要时可采用间接手法,如通过声音或暗示性画面表现,并对敏感元素进行替代处理(例如用番茄酱代替血液)。
+- 每个镜头中,每位角色最多分配一句对白。每句对白应对应一个独立镜头。
+- 每个镜头的描述必须独立完整,不得引用其他镜头内容。
+- 当镜头聚焦于某角色时,需说明具体聚焦的身体部位(如面部、手部等)。
+- 描述角色时,必须标明其朝向(例如“面朝左”、“背对镜头”等)。
+"""
+
+human_prompt_design_storyboard = \
+"""
+<SCRIPT>
+{script_str}
+</SCRIPT>
+
+<CHARACTERS>
+{characters_str}
+</CHARACTERS>
+
+<USER_REQUIREMENT>
+{user_requirement_str}
+</USER_REQUIREMENT>
+"""
+
+
+system_prompt_decompose_visual_description = \
+"""
+[角色]
+你是一位专业的视觉文本分析师,精通电影语言与镜头叙事。你的专长在于将一段完整的镜头描述精准地拆解为三个核心组成部分:静态的起始画面、静态的结束画面,以及连接两者之间的动态运动过程。
+
+[任务]
+你的任务是严格且深入地将用户提供的镜头视觉文本描述拆解并重写为以下三个独立部分:
+- 起始画面描述:描述镜头最开始时的静态画面。聚焦于构图元素、角色初始姿态、环境布局、光影、色彩及其他静态视觉特征。
+- 运动过程描述:描述从起始画面到结束画面之间发生的所有动态变化。包括摄像机运动(例如:固定、推进、拉远、横摇、跟拍、俯仰等)以及画面内元素的运动(例如:角色移动、物体位移、光影变化等)。这是整个描述中最具动态性的部分。在描述角色的运动和变化时,不得直接使用角色姓名,而应通过其外部特征(尤其是显著的衣着特征等)来指代该角色。
+- 结束画面描述:描述镜头结束时的静态画面。同样关注静态构图,但必须体现因摄像机运动或画面内元素移动、角色动作所导致的最终状态。
+
+[输入]
+你将收到一段镜头的视觉文本描述,其中通常隐含或明确包含起始状态、运动过程和结束状态的信息。
+此外,你还将收到一份潜在角色列表,每个角色包含一个标识符及其显著特征。
+- 视觉描述位于 <VISUAL_DESC> 与 </VISUAL_DESC> 标签之间。
+- 角色列表位于 <CHARACTERS> 与 </CHARACTERS> 标签之间。
+
+[输出]
+严格按照以下JSON格式进行输出(必须严格遵守JSON格式,不得有任何其他内容):
+```json
+{
+    "ff_desc": // 镜头第一帧的详细描述,捕捉初始的视觉元素和构图。
+    "ff_vis_char_idxs": // 镜头第一帧中可见角色的索引列表,对应于输入中提供的角色列表。例如:[0]、[0,1]
+    "lf_desc": // 对镜头最后一帧的详细描述,捕捉其最终的视觉元素与构图。
+    "lf_vis_char_idxs": // 镜头最后一帧中可见角色的索引列表,对应于输入中提供的角色列表。例如:[0]、[0,1]
+    "motion_desc": // 镜头的运动描述,描述镜头内的动态变化(包括摄像机运动和画面内元素的移动)。例如:从半身镜头推近至特写。罗宇尘(留着胡子,穿着白色T恤)对着镜头微笑。
+    "variation_type": // 表示第一帧和最后一帧之间的变化程度。可选值有"large"、"medium"和"small"。
+    "variation_reason": // 给出以上变化程度的合理解释。例如:与第一帧相比,最后一帧中出现了一个新角色,而构图没有发生显著变化。因此变化程度中等。
+}
+```
+
+[指导原则]
+- 确保所有输出内容(除键名外)所使用的语言与剧本中的语言一致。
+- 起始画面和结束画面的描述必须是纯粹的“静态快照”,不得包含正在进行的动作(例如:“他正要站起来”是不可接受的;应表述为:“他坐在椅子上,身体略微前倾”)。
+- 在运动过程描述中,必须清晰区分摄像机运动与画面内元素的运动。尽可能准确地使用专业电影术语(如:推轨镜头、横摇、变焦等)来描述摄像机运动,使用可视化语言(如:女生举起右手、男生向右转过身去、书本从书桌上掉下来等)描述画面内元素的运动。
+- 在运动过程描述中,不得直接使用角色姓名指代角色;而应通过角色可见的外部特征进行指代。例如,“罗宇尘正在行走”不可接受,应表述为“一位短发、身穿绿色连衣裙的青年男生正在行走”。
+- 结束画面因初始画面中的运动过程而产生,因此结束画面描述必须在逻辑上与起始画面描述及运动过程描述保持一致。运动过程中描述的所有动作、姿态和位置变化都应在结束画面的静态图像中有所体现。
+- 若输入描述对某些细节表述模糊,可根据上下文做出合理推断和补充,以确保三个部分完整流畅;但核心要素必须严格遵循输入文本。
+- 使用准确、简洁且专业的描述性语言。避免使用过于文学化的修辞(如隐喻或情绪化修饰),聚焦于可被视觉化呈现的信息。
+- 与输入的视觉描述类似,起始画面和结束画面的描述应包含镜头类型、拍摄角度、构图等细节。
+- 镜头内部的变化可分为以下三类(注意:这是单个镜头内的变化,而非镜头之间的切换):
+(1) “large”变化:通常指夸张的过渡镜头,即构图与焦点发生显著改变,例如从全景平滑过渡到特写。此类变化通常伴随显著的摄像机运动(如穿越城市上空的无人机视角)。
+(2) “medium”变化:常涉及新角色的引入,或已有角色从背对镜头转为正面朝向镜头。
+(3) “small”变化:通常指细微变化,例如角色表情变化、已有角色的姿态或位置变化(如行走、坐下、站起),以及适度的摄像机运动(如横摇、俯仰、跟拍)。
+- 描述角色时,必须标明其朝向(例如“面朝左”、“背对镜头”等)。
+- 第一个镜头必须以尽可能宽广的景别建立整体场景环境。
+- 尽可能减少摄像机位置的数量。
+"""
+
+human_prompt_decompose_visual_description = \
+"""
+<VISUAL_DESC>
+{visual_desc}
+</VISUAL_DESC>
+
+<CHARACTERS>
+{characters_str}
+</CHARACTERS>
+"""
+
+async def design_storyboard(
+    client: AsyncArkClient,
+    script: str,
+    characters: str,
+    user_requirement: Optional[str] = None
+):
+    user_prompt = human_prompt_design_storyboard.format(
+        script_str=script,
+        characters_str=characters,
+        user_requirement_str=user_requirement
+    )
+
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt)
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_design_storyboard,
+        )
+        logger.info(f"设计分镜成功")
+        storyboard = client.get_response_text(response)
+        storyboard = io_handler.string_to_json(storyboard)
+        return storyboard
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+async def decompose_visual_description(
+    client: AsyncArkClient,
+    visual_desc: str,
+    characters: str
+):
+    user_prompt = human_prompt_decompose_visual_description.format(
+        visual_desc=visual_desc,
+        characters_str=characters
+    )
+    
+    user_message = ArkMessage(role="user")
+    user_message.add_text(user_prompt)
+    try:
+        response = await client.chat(
+            model="doubao-seed-1-6-251015",
+            messages=[user_message],
+            system_prompt=system_prompt_decompose_visual_description,
+        )
+        logger.info(f"拆解视觉描述成功")
+        decomposed_visual_desc = client.get_response_text(response)
+        decomposed_visual_desc = io_handler.string_to_json(decomposed_visual_desc)
+        return decomposed_visual_desc
+    except APIError as e:
+        logger.error(f"API错误: {e}")
+        raise e
+
+async def create_storyboard_for_single_scene(
+    client: AsyncArkClient,
+    script: str,
+    characters: str,
+    user_requirement: Optional[str] = None,
+    global_semaphore: Optional[asyncio.Semaphore] = None,
+    scene_idx: Optional[int] = None
+):
+    """
+    处理单个场景的分镜设计(优化版本:使用全局信号量控制并发)
+    
+    Args:
+        client: AsyncArkClient 实例
+        script: 场景剧本
+        characters: 角色信息
+        user_requirement: 用户要求
+        global_semaphore: 全局信号量,用于控制所有API请求的总并发数
+        scene_idx: 场景索引(用于日志)
+    """
+    # 判断script是否为str类型,如果不是,则先转换为str类型
+    if not isinstance(script, str):
+        script = str(script)
+    if not isinstance(characters, str):
+        characters = str(characters)
+    if not isinstance(user_requirement, str):
+        user_requirement = str(user_requirement)
+
+    scene_log_prefix = f"场景 {scene_idx}" if scene_idx is not None else "场景"
+
+    try:
+        # 使用全局信号量控制并发
+        if global_semaphore:
+            async with global_semaphore:
+                storyboard = await design_storyboard(
+                    client=client,
+                    script=script,
+                    characters=characters,
+                    user_requirement=user_requirement
+                )
+        else:
+            storyboard = await design_storyboard(
+                client=client,
+                script=script,
+                characters=characters,
+                user_requirement=user_requirement
+            )
+        logger.info(f"{scene_log_prefix} 设计分镜成功")
+    except Exception as e:
+        logger.error(f"{scene_log_prefix} 设计分镜失败: {e}")
+        raise e
+
+    # 拆解每个分镜的视觉描述(异步并行执行)
+    total_items = len(storyboard["storyboard"])
+    logger.info(f"{scene_log_prefix} 开始异步并行处理 {total_items} 个分镜项的视觉描述...")
+
+    async def process_item(item):
+        """处理单个分镜项的异步函数"""
+        item_idx = item.get("idx", "unknown")
+        logger.info(f"{scene_log_prefix} 处理分镜项 {item_idx}...")
+        
+        # 使用全局信号量控制并发
+        if global_semaphore:
+            async with global_semaphore:
+                decomposed_visual_desc = await decompose_visual_description(
+                    client=client,
+                    visual_desc=item["visual_desc"],
+                    characters=characters
+                )
+        else:
+            decomposed_visual_desc = await decompose_visual_description(
+                client=client,
+                visual_desc=item["visual_desc"],
+                characters=characters
+            )
+        
+        logger.info(f"{scene_log_prefix} 分镜项 {item_idx} 处理完成")
+        return item, decomposed_visual_desc
+
+    # 创建所有任务并立即并发执行(不等待其他场景)
+    tasks = [process_item(item) for item in storyboard["storyboard"]]
+    
+    completed_count = 0
+    try:
+        # 并发执行所有分镜项的处理(使用全局信号量控制总并发数)
+        results = await asyncio.gather(*tasks, return_exceptions=True)
+        
+        # 处理结果和异常
+        for result in results:
+            if isinstance(result, Exception):
+                logger.error(f"{scene_log_prefix} 处理分镜项时发生错误: {result}")
+                raise result
+            
+            item, decomposed_visual_desc = result
+            item |= decomposed_visual_desc
+            completed_count += 1
+        
+        logger.info(f"{scene_log_prefix} 进度: {completed_count}/{total_items} 个分镜项已完成")
+    except Exception as e:
+        logger.error(f"{scene_log_prefix} 处理分镜项时发生错误: {e}")
+        raise
+
+    logger.info(f"{scene_log_prefix} 所有 {total_items} 个分镜项的视觉描述处理完成")
+    return storyboard
+
+
+async def create_storyboard(
+    script: dict,
+    characters: str,
+    user_requirement: Optional[str] = None,
+    max_concurrent_requests: int = 50
+):
+    """
+    创建分镜脚本(优化版本:完全异步并发处理)
+    
+    优化策略:
+    1. 外层循环:并发处理所有场景的分镜设计
+    2. 内层循环:每个场景内部并发处理所有分镜项的视觉描述拆解
+    3. 使用全局信号量控制所有API请求的总并发数,防止服务器限流
+    
+    Args:
+        script: 剧本字典,包含多个场景
+        characters: 角色信息字符串
+        user_requirement: 用户要求
+        max_concurrent_requests: 最大并发请求数(默认50,可根据服务器调整)
+    
+    Returns:
+        包含所有场景分镜脚本的字典
+    """
+    logger.info(f"开始设计分镜(共 {len(script['script'])} 个场景)...")
+    
+    # 创建全局信号量,控制所有API请求的总并发数
+    # 这包括:场景级别的设计分镜请求 + 所有分镜项的视觉描述拆解请求
+    global_semaphore = asyncio.Semaphore(max_concurrent_requests)
+    
+    async def handle_scene(scene_idx: int, scene_script: str):
+        """处理单个场景的异步函数"""
+        logger.info(f"开始处理场景 {scene_idx}...")
+        try:
+            scene_storyboard = await create_storyboard_for_single_scene(
+                client=client,
+                script=scene_script,
+                characters=characters,
+                user_requirement=user_requirement,
+                global_semaphore=global_semaphore,
+                scene_idx=scene_idx
+            )
+            logger.info(f"场景 {scene_idx} 的分镜设计完成")
+            return scene_idx, scene_storyboard
+        except Exception as e:
+            logger.error(f"场景 {scene_idx} 处理失败: {e}")
+            raise
+    
+    async with AsyncArkClient() as client:
+        # 并发处理所有场景(外层并发)
+        scene_tasks = [
+            handle_scene(idx, scene_script) 
+            for idx, scene_script in enumerate(script["script"])
+        ]
+        
+        logger.info(f"🚀 并发启动 {len(scene_tasks)} 个场景的处理任务(最大并发请求数: {max_concurrent_requests})...")
+        
+        # 并发执行所有场景,并保持顺序
+        scene_results = await asyncio.gather(*scene_tasks, return_exceptions=True)
+        
+        # 检查是否有异常
+        failed_scenes = []
+        for idx, result in enumerate(scene_results):
+            if isinstance(result, Exception):
+                logger.error(f"❌ 场景 {idx} 处理失败: {result}")
+                failed_scenes.append(idx)
+        
+        if failed_scenes:
+            raise Exception(f"以下场景处理失败: {failed_scenes}")
+        
+        # 按场景索引排序,确保结果顺序正确
+        scene_results.sort(key=lambda x: x[0])
+        scene_storyboard_list = [result[1] for result in scene_results]
+        
+        logger.info(f"✅ 所有 {len(script['script'])} 个场景的分镜设计完成")
+        logger.info(f"✅ 分镜设计完成")
+        
+        return {"storyboard": scene_storyboard_list}
+
+if __name__ == "__main__":  
+    async def main():
+        storyboard = await create_storyboard(
+            script=script,
+            characters=characters,
+            user_requirement=user_requirement
+        )
+        io_handler.write_json(storyboard, "./output/storyboard.json")
+    
+    asyncio.run(main())

+ 3 - 0
examples/video_create/pipeline/__init__.py

@@ -0,0 +1,3 @@
+from .idea2video_pipeline import Idea2VideoPipeline
+
+__all__ = ["Idea2VideoPipeline"]

BIN
examples/video_create/pipeline/__pycache__/__init__.cpython-310.pyc


BIN
examples/video_create/pipeline/__pycache__/__init__.cpython-312.pyc


BIN
examples/video_create/pipeline/__pycache__/idea2video_pipeline.cpython-310.pyc


BIN
examples/video_create/pipeline/__pycache__/idea2video_pipeline.cpython-312.pyc


+ 473 - 0
examples/video_create/pipeline/idea2video_pipeline.py

@@ -0,0 +1,473 @@
+"""
+idea2video 任务流
+从idea到video的完整任务流
+包括:
+1. 从idea到story
+2. 从story到script
+3. 从script到video
+"""
+import os
+import asyncio
+from pathlib import Path
+from typing import Dict, Optional
+
+from taskflow import TaskManager, FileIOHandler
+from ..mcps.story_create import develop_story, develop_story_base_on_story
+from ..mcps.character_extract import extract_characters
+from ..mcps.storyboard_create import create_storyboard
+from ..mcps.character_portrait import gen_single_character_portrait
+from ..mcps.camera_tree import create_camera_tree
+from ..mcps.refer_image import select_refer_image
+from ..mcps.concat_clip import concat_videos
+from ..utils.tools import download_image, efficient_sort
+from api_modules.ark_client_async import AsyncArkClient
+from api_modules.ark_image_client_async import AsyncArkImageClient
+from api_modules.ark_video_client import ArkVideoClient
+from api_modules.ark_video_client_async import AsyncArkVideoClient
+from taskflow import get_logger
+
+logger = get_logger("examples.video_create.pipeline.idea2video_pipeline")
+
+class Idea2VideoPipeline:
+    """视频创作任务流"""
+
+    def __init__(self, io_handler: FileIOHandler, output_dir: str, manager: TaskManager):
+        """
+        初始化视频创作任务流
+
+        Args:
+            io_handler: 文件I/O处理器
+            output_dir: 输出目录
+            manager: 任务管理器
+        """
+        self.io_handler = io_handler
+        self.output_dir = Path(output_dir)
+        self.output_dir.mkdir(parents=True, exist_ok=True)
+        self.manager = manager
+
+    async def step1_develop_story(self, idea: str, user_requirement: Optional[str] = None) -> Dict:
+        """步骤1:从idea到story"""
+        async with AsyncArkClient() as client:
+            story = await develop_story(client=client, idea=idea, user_requirement=user_requirement)
+
+        output_file = str(self.output_dir / "step1_story.txt")
+        await self.io_handler.write_text_async(story, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": story
+        }
+
+    async def step2_develop_script(self, user_requirement: Optional[str] = None) -> Dict:
+        """步骤2:从story到script"""
+        previous_output = self.manager.load_step_output("step1")
+        if previous_output is None:
+            raise ValueError("步骤1未完成,无法从story到script")
+
+        story = previous_output["data"]
+
+        async with AsyncArkClient() as client:
+            script = await develop_story_base_on_story(client=client, story=story, user_requirement=user_requirement)
+
+        output_file = str(self.output_dir / "step2_script.json")
+        await self.io_handler.write_json_async(script, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": script
+        }
+
+    async def step3_extract_characters(self) -> Dict:
+        """步骤3:从script到characters"""
+        previous_output = self.manager.load_step_output("step1")
+        if previous_output is None:
+            raise ValueError("步骤2未完成,无法从story到characters")
+
+        story = previous_output["data"]
+        
+        async with AsyncArkClient() as client:
+            characters = await extract_characters(client=client, story=story)
+
+        output_file = str(self.output_dir / "step3_characters.json")
+        await self.io_handler.write_json_async(characters, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": characters
+        }
+
+    async def step4_create_storyboard(self, user_requirement: Optional[str] = None) -> Dict:
+        """步骤4:从script到storyboard"""
+        previous_script_output = self.manager.load_step_output("step2")
+        if previous_script_output is None:
+            raise ValueError("步骤2未完成,无法从script到storyboard")
+
+        previous_characters_output = self.manager.load_step_output("step3")
+        if previous_characters_output is None:
+            raise ValueError("步骤3未完成,无法从script到storyboard")
+
+        script = previous_script_output["data"]
+        characters = previous_characters_output["data"]
+        
+        # 将 characters 转换为字符串格式(如果它是字典)
+        if isinstance(characters, dict):
+            # 将角色信息格式化为字符串
+            characters_str = ""
+            if "characters" in characters:
+                for char in characters["characters"]:
+                    char_info = []
+                    if "identifier_in_scene" in char:
+                        char_info.append(f"标识符: {char['identifier_in_scene']}")
+                    if "static_features" in char:
+                        char_info.append(f"静态特征: {char['static_features']}")
+                    if "dynamic_features" in char:
+                        char_info.append(f"动态特征: {char['dynamic_features']}")
+                    characters_str += " | ".join(char_info) + "\n"
+            else:
+                characters_str = str(characters)
+        else:
+            characters_str = str(characters)
+        
+        storyboard = await create_storyboard(script=script, characters=characters_str, user_requirement=user_requirement)
+
+        output_file = str(self.output_dir / "step4_storyboard.json")
+        await self.io_handler.write_json_async(storyboard, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": storyboard
+        }
+
+    async def step5_generate_portrait(
+        self, 
+        size: Optional[str] = "2048x2048", 
+        refer_image: Optional[list[str]] = None, 
+        refer_image_map: Optional[Dict[str, list[str]]] = None,
+        style: Optional[str] = None
+    ) -> Dict:
+        """
+        步骤5:从characters到portrait(并行生成所有角色肖像)
+        
+        Args:
+            size: 生成图片的尺寸,默认为 "2048x2048"
+            refer_image: 全局参考图片列表(所有角色共享),优先级低于 refer_image_map
+            refer_image_map: 角色标识符到参考图片列表的映射字典,格式为 {角色标识符: [图片路径1, 图片路径2, ...]}
+                            例如: {"林小星": ["path/to/image1.jpg"], "阿凯": ["path/to/image2.jpg"]}
+                            优先级最高,会覆盖 refer_image 和角色字段中的 refer_image
+            style: 生成风格,例如 "写实"、"卡通" 等
+        """
+        previous_output = self.manager.load_step_output("step3")
+        if previous_output is None:
+            raise ValueError("步骤3未完成,无法从characters到portrait")
+
+        characters = previous_output["data"]
+        
+        # 确保portraits目录存在
+        portraits_dir = self.output_dir / "portraits"
+        portraits_dir.mkdir(parents=True, exist_ok=True)
+
+        async def process_single_character(client: AsyncArkImageClient, character: dict) -> None:
+            """处理单个角色的肖像生成和下载"""
+            character_id = character["identifier_in_scene"]
+            
+            # 确定该角色使用的参考图片,优先级从高到低:
+            # 1. refer_image_map 中该角色的映射
+            # 2. 全局 refer_image
+            # 3. 角色字典中的 refer_image 字段(如果存在)
+            # 4. None
+            character_refer_image = None
+            if refer_image_map is not None and character_id in refer_image_map:
+                character_refer_image = refer_image_map[character_id]
+                logger.info(f"角色 {character_id} 使用映射中的参考图片: {character_refer_image}")
+            elif refer_image is not None:
+                character_refer_image = refer_image
+                logger.info(f"角色 {character_id} 使用全局参考图片: {character_refer_image}")
+            elif "refer_image" in character and character["refer_image"] is not None:
+                character_refer_image = character["refer_image"]
+                # 确保是列表格式
+                if isinstance(character_refer_image, str):
+                    character_refer_image = [character_refer_image]
+                logger.info(f"角色 {character_id} 使用角色字段中的参考图片: {character_refer_image}")
+            
+            # 生成肖像图片URL
+            image_url = await gen_single_character_portrait(
+                client=client, 
+                character=character, 
+                size=size, 
+                refer_image=character_refer_image, 
+                style=style
+            )
+            # 下载图片(使用asyncio.to_thread在线程池中执行同步IO操作)
+            image_path = str(portraits_dir / f"{character_id}_{self.output_dir.name}.jpg")
+            await asyncio.to_thread(download_image, image_url, image_path)
+            character["portrait_path"] = image_path
+
+        async with AsyncArkImageClient() as client:
+            # 并行处理所有角色
+            tasks = [
+                process_single_character(client, character) 
+                for character in characters["characters"]
+            ]
+            await asyncio.gather(*tasks)
+
+        output_file = str(self.output_dir / "step5_portrait.json")
+        await self.io_handler.write_json_async(characters, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": characters
+        }
+
+    async def step6_create_camera_tree(self) -> Dict:
+        """步骤6:从storyboard到camera_tree"""
+        previous_output = self.manager.load_step_output("step4")
+        if previous_output is None:
+            raise ValueError("步骤4未完成,无法从storyboard到camera_tree")
+
+        storyboards = previous_output["data"]
+
+        # 确保camera_tree目录存在
+        camera_tree_dir = self.output_dir / "camera_tree"
+        camera_tree_dir.mkdir(parents=True, exist_ok=True)
+
+        async def process_single_storyboard(client: AsyncArkClient, storyboard: Dict, scene_idx: int) -> None:
+            """处理单个storyboard的camera_tree生成和下载"""
+            camera_tree = await create_camera_tree(client=client, storyboard=storyboard)
+            camera_tree_path = str(camera_tree_dir / f"camera_tree_{scene_idx}.json")
+            await self.io_handler.write_json_async(camera_tree, camera_tree_path)
+
+        async with AsyncArkClient() as client:
+            # 并行处理所有storyboard
+            tasks = [
+                process_single_storyboard(client, storyboard, idx)
+                for idx, storyboard in enumerate(storyboards["storyboard"])
+            ]
+            await asyncio.gather(*tasks)
+
+        output_file = str(self.output_dir / "step6_camera_tree.json")
+        await self.io_handler.write_json_async(storyboards, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": storyboards
+        }
+
+    async def step7_generate_video_frames(self) -> Dict:
+        """步骤7:从camera_tree到video_frames"""
+        previous_storyboard_output = self.manager.load_step_output("step6")
+        if previous_storyboard_output is None:
+            raise ValueError("步骤6未完成,无法从camera_tree到video_frames")
+
+        previous_portrait_output = self.manager.load_step_output("step5")
+        if previous_portrait_output is None:
+            raise ValueError("步骤5未完成,无法从characters到video_frames")
+
+        storyboards = previous_storyboard_output["data"]
+        characters = previous_portrait_output["data"]
+
+        # 确保video_frames目录存在
+        video_frames_dir = self.output_dir / "video_frames"
+        video_frames_dir.mkdir(parents=True, exist_ok=True)
+
+        async def process_single_storyboard(client: AsyncArkClient, image_client: AsyncArkImageClient, storyboard: Dict, scene_idx: int) -> Dict:
+            """处理单个storyboard的video_frames生成和下载"""
+            camera_tree = storyboard["camera_tree"]
+            storyboard = storyboard["storyboard"]
+
+            parent_shot_idxs = [0]
+            active_shot_idxs = []
+            for _, item in enumerate(camera_tree):
+                if item["parent_shot_idx"] is not None:
+                    parent_shot_idxs.append(item["parent_shot_idx"])
+                active_shot_idxs.append(item["active_shot_idxs"])
+            
+            process_order = efficient_sort(parent_shot_idxs, active_shot_idxs)
+            process_order += [i for i in range(len(active_shot_idxs)) if i not in process_order]
+
+            for cam_idx in process_order:
+                logger.info(f"Processing scene {scene_idx} - camera {cam_idx}...")
+                camera_item = camera_tree[cam_idx]
+                prev_frame_path_and_text_pairs = []
+                for _, shot_idx  in enumerate(camera_item["active_shot_idxs"]):
+                    logger.info(f"Processing scene {scene_idx} - camera {cam_idx} - shot {shot_idx}...")
+                    frame_description = storyboard[shot_idx]["ff_desc"]
+                    vis_char_idxs = storyboard[shot_idx]["ff_vis_char_idxs"]
+
+                    image_path_and_text_pairs = []
+                    frame_save_path = str(video_frames_dir / f"scene{scene_idx}_camera{cam_idx}_shot{shot_idx}_{self.output_dir.name}.png")
+
+                    if os.path.exists(frame_save_path):
+                        logger.info(f"Frame for scene {scene_idx} - camera {cam_idx} - shot {shot_idx} already exists.")
+                        # 即使文件已存在,也需要设置 ff_path 和 prev_frame_path_and_text_pairs,以便后续引用
+                        storyboard[shot_idx]["ff_path"] = frame_save_path
+                        prev_frame_path_and_text_pairs.append((frame_save_path, frame_description))
+                        continue
+                    else:
+                        # 参考可见角色三视图
+                        for vis_char_idx in vis_char_idxs:
+                            logger.info(f"Referencing character {vis_char_idx} portrait...")
+                            image_path_and_text_pairs.append((characters["characters"][vis_char_idx]["portrait_path"], f'{characters["characters"][vis_char_idx]["identifier_in_scene"]}的三视图肖像'))
+                        
+                        # 参考前序帧
+                        image_path_and_text_pairs.extend(prev_frame_path_and_text_pairs)
+
+                        # 参考父帧
+                        if camera_item["parent_shot_idx"] is not None:
+                            parent_shot_idx = camera_item["parent_shot_idx"]
+                            parent_shot = storyboard[parent_shot_idx]
+                            
+                            # 检查父帧的 ff_path 是否存在
+                            parent_ff_path = parent_shot.get("ff_path")
+                            
+                            # 如果 ff_path 不存在,尝试从文件系统推断(基于 parent_cam_idx)
+                            if parent_ff_path is None and camera_item.get("parent_cam_idx") is not None:
+                                parent_cam_idx = camera_item["parent_cam_idx"]
+                                inferred_parent_path = str(video_frames_dir / f"scene{scene_idx}_camera{parent_cam_idx}_shot{parent_shot_idx}.png")
+                                if os.path.exists(inferred_parent_path):
+                                    parent_ff_path = inferred_parent_path
+                                    # 更新父帧的 ff_path,以便后续引用
+                                    parent_shot["ff_path"] = parent_ff_path
+                                    logger.info(f"Found parent frame at inferred path: {parent_ff_path}")
+                            
+                            # 如果找到了父帧路径,添加到参考列表
+                            if parent_ff_path is not None:
+                                image_path_and_text_pairs.append((parent_ff_path, parent_shot["ff_desc"]))
+                            else:
+                                logger.warning(f"Parent frame for shot {shot_idx} (parent_shot_idx={parent_shot_idx}) not found, skipping parent frame reference.")
+
+                        # 筛选参考图像,生成生图提示词
+                        info_for_gen_frame = await select_refer_image(
+                            client=client,
+                            frame_description=frame_description,
+                            image_text_pairs=image_path_and_text_pairs
+                        )
+
+                        # 生成序列帧
+                        frame_prompt = info_for_gen_frame["text_prompt"]
+                        image_urls = [item[0] for item in info_for_gen_frame["reference_image_path_and_text_pairs"]]
+                        logger.info(f"Frame prompt: {frame_prompt}")
+                        logger.info(f"Reference images: {image_urls}")
+
+                        response = await image_client.generate_image(
+                            prompt=frame_prompt,
+                            reference_image=image_urls
+                        )
+
+                        frame_url = image_client.get_image_url(response)
+                        await asyncio.to_thread(download_image, frame_url, frame_save_path)
+                        prev_frame_path_and_text_pairs.append((frame_save_path, frame_description))
+                        storyboard[shot_idx]["ff_path"] = frame_save_path
+
+        async with AsyncArkClient() as client, AsyncArkImageClient() as image_client:
+            # 并行处理所有storyboard
+            tasks = [
+                process_single_storyboard(client, image_client, storyboard, idx)
+                for idx, storyboard in enumerate(storyboards["storyboard"])
+            ]
+            await asyncio.gather(*tasks, return_exceptions=True)
+
+        output_file = str(self.output_dir / "step7_video_frames.json")
+        await self.io_handler.write_json_async(storyboards, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": storyboards
+        }
+
+    async def step8_generate_video(self) -> Dict:
+        """步骤8:从video_frames到video_clips"""
+        previous_video_frames_output = self.manager.load_step_output("step7")
+        if previous_video_frames_output is None:
+            raise ValueError("步骤7未完成,无法从video_frames到video_clips")
+
+        storyboards = previous_video_frames_output["data"]
+
+        # 确保video_clips目录存在
+        video_clip_dir = self.output_dir / "video_clips"
+        video_clip_dir.mkdir(parents=True, exist_ok=True)
+
+        async def process_single_storyboard(video_client: AsyncArkVideoClient, storyboard: Dict, scene_idx: int) -> list:
+            """处理单个video_frame的video_clip生成和下载,返回所有后台任务"""
+            background_tasks = []
+            for shot in storyboard["storyboard"]:
+                len_id = shot["idx"]
+                motion_prompt = shot["motion_desc"]
+                image_url = shot["ff_path"]
+                lens_duration = 4
+                video_save_path = str(video_clip_dir / f"scene{scene_idx}_len{len_id}.mp4")
+                
+                # 构建生成参数字符串
+                gen_params = f" --dur {lens_duration}"
+                
+                task_id, background_task = await video_client.create_video_task_async(
+                    prompt=motion_prompt,
+                    image_url=image_url,
+                    gen_params=gen_params,
+                    output_path=video_save_path
+                )
+                
+                if background_task is not None:
+                    background_tasks.append(background_task)
+                    shot["clip_path"] = video_save_path
+                    logger.info(f"已提交视频生成任务,scene {scene_idx}, shot {len_id}, task_id: {task_id}")
+                else:
+                    logger.error(f"视频生成任务提交失败,scene {scene_idx}, shot {len_id}")
+            
+            return background_tasks
+
+        async with AsyncArkVideoClient() as video_client:
+            # 并行处理所有storyboard,收集所有后台任务
+            storyboard_tasks = [
+                process_single_storyboard(video_client, storyboard, idx)
+                for idx, storyboard in enumerate(storyboards["storyboard"])
+            ]
+            storyboard_results = await asyncio.gather(*storyboard_tasks)
+            
+            # 展平所有后台任务
+            all_background_tasks = []
+            for task_list in storyboard_results:
+                all_background_tasks.extend(task_list)
+            
+            # 等待所有视频生成和下载完成
+            if all_background_tasks:
+                logger.info(f"等待 {len(all_background_tasks)} 个视频生成任务完成...")
+                await asyncio.gather(*all_background_tasks)
+                logger.info("所有视频生成任务已完成!")
+            else:
+                logger.warning("没有提交任何视频生成任务")
+
+        output_file = str(self.output_dir / "step8_video_clips.json")
+        await self.io_handler.write_json_async(storyboards, output_file)
+
+        return {
+            "output_file": output_file,
+            "data": storyboards
+        }
+
+    async def step9_concat_clip(self) -> Dict:
+        """步骤9:拼接所有视频片段进行输出"""
+        previous_output = self.manager.load_step_output("step8")
+        if previous_output is None:
+            raise  ValueError("步骤8未完成,无法进行视频拼接!")
+
+        storyboards = previous_output["data"]
+
+        # 确保video_save目录存在
+        video_save_dir = self.output_dir / "video_save"
+        video_save_dir.mkdir(parents=True, exist_ok=True)
+
+        clips_path = []
+        for storyboard in storyboards["storyboard"]:
+            for item in storyboard["storyboard"]:
+                if os.path.exists(item["clip_path"]):
+                    clips_path.append(item["clip_path"])
+        
+        output_file = str(video_save_dir / "final_video.mp4")
+        concat_videos(clips_path, output_file)
+        
+        return {
+            "output_file": output_file,
+            "data": "final video"
+        }
+
+

+ 0 - 0
examples/video_create/utils/__init__.py


BIN
examples/video_create/utils/__pycache__/__init__.cpython-310.pyc


Some files were not shown because too many files changed in this diff