ark_video_client.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. """
  2. 火山引擎ARK视频生成API客户端
  3. 封装ARK视频生成API的调用,提供类型安全的接口
  4. """
  5. import os
  6. import time
  7. import threading
  8. from typing import Optional, Dict, Any, Callable
  9. from enum import Enum
  10. from examples.video_create.utils.tools import download_video
  11. from .base_client import APIClient, APIError
  12. from taskflow.logger import get_logger
  13. from taskflow.config import get_config
  14. logger = get_logger("api_modules.ark_video_client")
  15. class TaskStatus(str, Enum):
  16. """任务状态枚举"""
  17. PENDING = "pending"
  18. PROCESSING = "processing"
  19. SUCCEEDED = "succeeded"
  20. FAILED = "failed"
  21. def handle_video_result(
  22. task_id: str,
  23. output_path: str,
  24. result: Optional[Dict],
  25. error: Optional[str]
  26. ) -> None:
  27. if error:
  28. logger.info(f"\n任务 {task_id} 处理失败:{error}")
  29. else:
  30. video_url = result.get("content", {}).get("video_url")
  31. download_video(video_url, output_path)
  32. logger.info(f"生成视频已下载:{output_path}")
  33. class ArkVideoClient(APIClient):
  34. """
  35. 火山引擎ARK视频生成API客户端
  36. 封装ARK视频生成API的调用,提供便捷的接口
  37. """
  38. DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com"
  39. DEFAULT_ENDPOINT = "/api/v3/contents/generations/tasks"
  40. DEFAULT_MODEL = "doubao-seedance-1-0-pro-250528"
  41. def __init__(
  42. self,
  43. api_key: Optional[str] = None,
  44. base_url: Optional[str] = None,
  45. model: Optional[str] = None,
  46. timeout: int = 60,
  47. poll_interval: int = 5,
  48. max_poll_time: int = 500,
  49. **kwargs
  50. ):
  51. """
  52. 初始化ARK视频生成API客户端
  53. Args:
  54. api_key: API密钥(如果为None,会尝试从环境变量或配置中获取)
  55. base_url: API基础URL(默认使用官方URL)
  56. model: 模型名称(如果为None,会尝试从配置中获取)
  57. timeout: 请求超时时间(秒,默认60秒)
  58. poll_interval: 轮询间隔(秒,默认5秒)
  59. max_poll_time: 最大轮询总时间(秒,默认500秒)
  60. **kwargs: 传递给APIClient的其他参数
  61. """
  62. # 获取API密钥(优先级:参数 > 环境变量 > 配置)
  63. if api_key is None:
  64. api_key = os.getenv("ARK_API_KEY")
  65. if api_key is None:
  66. config = get_config()
  67. api_key = config.get("api.ark.api_key")
  68. if not api_key:
  69. raise ValueError("ARK API密钥未提供,请通过参数、环境变量ARK_API_KEY或配置文件提供")
  70. # 获取base_url(优先级:参数 > 配置 > 默认值)
  71. if base_url is None:
  72. config = get_config()
  73. base_url = config.get("api.ark.base_url", self.DEFAULT_BASE_URL)
  74. # 获取model(优先级:参数 > 配置 > 默认值)
  75. if model is None:
  76. config = get_config()
  77. model = config.get("api.ark.video_model", self.DEFAULT_MODEL)
  78. super().__init__(
  79. base_url=base_url,
  80. api_key=api_key,
  81. timeout=timeout,
  82. **kwargs
  83. )
  84. # 保存视频生成相关配置
  85. self.model = model
  86. self.poll_interval = poll_interval
  87. self.max_poll_time = max_poll_time
  88. logger.info(f"ARK视频生成API客户端初始化完成,模型: {self.model}")
  89. def _build_gen_params(
  90. self,
  91. duration: Optional[int] = None,
  92. ratio: Optional[str] = None,
  93. resolution: Optional[str] = None,
  94. watermark: Optional[str] = None,
  95. camerafixed: Optional[str] = None,
  96. **kwargs
  97. ) -> str:
  98. """
  99. 构建生成参数字符串
  100. 如果参数为None,则使用默认值或从kwargs中获取
  101. Args:
  102. duration: 视频时长(秒,默认4秒)
  103. ratio: 视频比例(默认"16:9")
  104. resolution: 视频分辨率(默认"1080p")
  105. watermark: 水印开关(默认"false")
  106. camerafixed: 相机固定开关(默认"false")
  107. **kwargs: 其他参数,会优先从kwargs中获取
  108. Returns:
  109. 生成参数字符串,格式如:"--dur 4 --rt 16:9 --rs 1080p --wm false --cf false"
  110. """
  111. # 默认值
  112. defaults = {
  113. "duration": 4,
  114. "ratio": "16:9",
  115. "resolution": "1080p",
  116. "watermark": "false",
  117. "camerafixed": "false"
  118. }
  119. # 从kwargs中获取参数,如果没有则使用传入的参数,再没有则使用默认值
  120. # 优先级:kwargs > 显式参数 > 默认值
  121. def get_param(key: str, param_value: Any) -> Any:
  122. if key in kwargs:
  123. return kwargs[key]
  124. return param_value if param_value is not None else defaults[key]
  125. duration = get_param("duration", duration)
  126. ratio = get_param("ratio", ratio)
  127. resolution = get_param("resolution", resolution)
  128. watermark = get_param("watermark", watermark)
  129. camerafixed = get_param("camerafixed", camerafixed)
  130. # 构建参数字符串
  131. params = [
  132. f"--dur {duration}",
  133. f"--rt {ratio}",
  134. f"--rs {resolution}",
  135. f"--wm {watermark}",
  136. f"--cf {camerafixed}"
  137. ]
  138. return " ".join(params) + " "
  139. def create_video_task(
  140. self,
  141. prompt: str,
  142. image_url: str,
  143. gen_params: Optional[str] = None,
  144. duration: Optional[int] = None,
  145. ratio: Optional[str] = None,
  146. resolution: Optional[str] = None,
  147. watermark: Optional[str] = None,
  148. camerafixed: Optional[str] = None,
  149. **kwargs
  150. ) -> Dict[str, Any]:
  151. """
  152. 创建视频生成任务
  153. Args:
  154. prompt: 视频生成提示词(必填)
  155. image_url: 参考图片URL(必填,必须是可访问的HTTP/HTTPS URL)
  156. gen_params: 自定义生成参数字符串(可选,如果提供则忽略其他生成参数)
  157. duration: 视频时长(秒,默认4秒)
  158. ratio: 视频比例(默认"16:9")
  159. resolution: 视频分辨率(默认"1080p")
  160. watermark: 水印开关(默认"false")
  161. camerafixed: 相机固定开关(默认"false")
  162. **kwargs: 其他请求参数(会覆盖默认配置,包括生成参数)
  163. Returns:
  164. API响应数据,包含任务ID等信息
  165. Raises:
  166. APIError: 如果请求失败
  167. ValueError: 如果参数无效
  168. 示例:
  169. >>> client.create_video_task(
  170. ... prompt="一个美丽的风景",
  171. ... image_url="https://example.com/image.jpg",
  172. ... duration=5,
  173. ... ratio="9:16",
  174. ... resolution="720p"
  175. ... )
  176. """
  177. if not prompt or not prompt.strip():
  178. raise ValueError("prompt不能为空")
  179. if not image_url or not image_url.strip():
  180. raise ValueError("image_url不能为空")
  181. # 验证image_url是否为URL格式
  182. if not image_url.startswith(("http://", "https://")):
  183. raise ValueError(
  184. f"image_url必须是HTTP/HTTPS URL格式,当前值: {image_url}。"
  185. "如果是本地文件路径,请先上传到云存储获取URL。"
  186. )
  187. # 构建生成参数:如果提供了gen_params字符串,直接使用;否则根据参数构建
  188. if gen_params is None:
  189. # 合并kwargs和显式参数
  190. gen_params_kwargs = {
  191. "duration": duration,
  192. "ratio": ratio,
  193. "resolution": resolution,
  194. "watermark": watermark,
  195. "camerafixed": camerafixed,
  196. **kwargs
  197. }
  198. gen_params = self._build_gen_params(**gen_params_kwargs)
  199. else:
  200. # 如果提供了gen_params字符串,确保以空格结尾(如果没有)
  201. gen_params = gen_params.strip()
  202. if gen_params and not gen_params.endswith(" "):
  203. gen_params += " "
  204. # 构建请求体
  205. request_data = {
  206. "model": kwargs.get("model", self.model),
  207. "content": [
  208. {
  209. "type": "text",
  210. "text": prompt + gen_params
  211. },
  212. {
  213. "type": "image_url",
  214. "image_url": {
  215. "url": image_url
  216. }
  217. }
  218. ],
  219. **{k: v for k, v in kwargs.items() if k != "model"}
  220. }
  221. logger.info(f"创建视频生成任务,模型: {request_data['model']}, 提示词: {prompt[:50]}...")
  222. logger.info(f"参考图片: {image_url}")
  223. try:
  224. response = self.post(
  225. endpoint=self.DEFAULT_ENDPOINT,
  226. json=request_data
  227. )
  228. logger.info(f"视频生成任务创建成功,任务ID: {response.get('id', 'unknown')}")
  229. return response
  230. except APIError as e:
  231. logger.error(f"创建视频生成任务失败: {e}")
  232. raise
  233. def query_video_task(self, task_id: str) -> Dict[str, Any]:
  234. """
  235. 查询视频生成任务状态
  236. Args:
  237. task_id: 任务ID(从create_video_task响应中获取)
  238. Returns:
  239. 任务状态详情,包含状态、视频URL等信息
  240. Raises:
  241. APIError: 如果请求失败
  242. ValueError: 如果参数无效
  243. """
  244. if not task_id or not task_id.strip():
  245. raise ValueError("task_id不能为空")
  246. query_endpoint = f"{self.DEFAULT_ENDPOINT}/{task_id}"
  247. logger.debug(f"查询视频生成任务状态,任务ID: {task_id}")
  248. try:
  249. response = self.get(endpoint=query_endpoint)
  250. status = response.get("status", "").lower()
  251. logger.debug(f"任务 {task_id} 状态: {status}")
  252. return response
  253. except APIError as e:
  254. logger.error(f"查询视频生成任务状态失败: {e}")
  255. raise
  256. def wait_for_task(
  257. self,
  258. task_id: str,
  259. callback: Optional[Callable] = None,
  260. callback_kwargs: Optional[Dict[str, Any]] = None
  261. ) -> Dict[str, Any]:
  262. """
  263. 等待任务完成(同步轮询)
  264. Args:
  265. task_id: 任务ID
  266. callback: 可选的回调函数,可以是以下两种签名之一:
  267. 1. (task_id, result, error) -> None
  268. 2. (task_id, output_path, result, error) -> None
  269. callback_kwargs: 传递给回调函数的额外关键字参数(如 output_path)
  270. Returns:
  271. 任务完成后的结果
  272. Raises:
  273. APIError: 如果请求失败
  274. TimeoutError: 如果任务超时
  275. """
  276. start_time = time.time()
  277. callback_kwargs = callback_kwargs or {}
  278. while True:
  279. elapsed = time.time() - start_time
  280. if elapsed > self.max_poll_time:
  281. error_msg = f"任务超时(超过 {self.max_poll_time} 秒)"
  282. logger.error(f"任务 {task_id} {error_msg}")
  283. if callback:
  284. # 检查回调函数签名,支持两种格式
  285. import inspect
  286. sig = inspect.signature(callback)
  287. param_count = len(sig.parameters)
  288. if param_count == 4:
  289. # 4参数版本:(task_id, output_path, result, error)
  290. callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
  291. else:
  292. # 3参数版本:(task_id, result, error)
  293. callback(task_id, {}, error_msg)
  294. raise TimeoutError(error_msg)
  295. # 查询任务状态
  296. result = self.query_video_task(task_id)
  297. if not result:
  298. logger.warning(f"任务 {task_id} 查询结果为空,继续等待...")
  299. time.sleep(self.poll_interval)
  300. continue
  301. # 解析状态
  302. status = result.get("status", "").lower()
  303. if status == TaskStatus.SUCCEEDED:
  304. logger.info(f"任务 {task_id} 完成,耗时: {int(elapsed)}秒")
  305. if callback:
  306. # 检查回调函数签名,支持两种格式
  307. import inspect
  308. sig = inspect.signature(callback)
  309. param_count = len(sig.parameters)
  310. if param_count == 4:
  311. # 4参数版本:(task_id, output_path, result, error)
  312. callback(task_id, callback_kwargs.get("output_path", ""), result, None)
  313. else:
  314. # 3参数版本:(task_id, result, error)
  315. callback(task_id, result, None)
  316. return result
  317. elif status == TaskStatus.FAILED:
  318. error_msg = result.get("error", {}).get("message", "未知错误")
  319. logger.error(f"任务 {task_id} 失败: {error_msg}")
  320. if callback:
  321. # 检查回调函数签名,支持两种格式
  322. import inspect
  323. sig = inspect.signature(callback)
  324. param_count = len(sig.parameters)
  325. if param_count == 4:
  326. # 4参数版本:(task_id, output_path, result, error)
  327. callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
  328. else:
  329. # 3参数版本:(task_id, result, error)
  330. callback(task_id, {}, error_msg)
  331. raise APIError(f"任务失败: {error_msg}")
  332. elif status in [TaskStatus.PENDING, TaskStatus.PROCESSING]:
  333. logger.info(f"任务 {task_id} 处理中({int(elapsed)}秒),状态: {status}")
  334. time.sleep(self.poll_interval)
  335. else:
  336. logger.warning(f"任务 {task_id} 未知状态: {status},继续等待...")
  337. time.sleep(self.poll_interval)
  338. def _background_poll(
  339. self,
  340. task_id: str,
  341. callback: Optional[Callable] = None,
  342. callback_kwargs: Optional[Dict[str, Any]] = None
  343. ):
  344. """
  345. 后台轮询任务状态的线程函数
  346. Args:
  347. task_id: 任务ID
  348. callback: 回调函数
  349. callback_kwargs: 传递给回调函数的额外参数
  350. """
  351. callback_kwargs = callback_kwargs or {}
  352. start_time = time.time()
  353. while True:
  354. elapsed = time.time() - start_time
  355. if elapsed > self.max_poll_time:
  356. error_msg = f"任务超时(超过 {self.max_poll_time} 秒)"
  357. logger.error(f"任务 {task_id} {error_msg}")
  358. if callback:
  359. import inspect
  360. sig = inspect.signature(callback)
  361. param_count = len(sig.parameters)
  362. if param_count == 4:
  363. callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
  364. else:
  365. callback(task_id, {}, error_msg)
  366. return
  367. # 查询任务状态
  368. try:
  369. result = self.query_video_task(task_id)
  370. except Exception as e:
  371. logger.error(f"查询任务 {task_id} 状态失败: {e}")
  372. time.sleep(self.poll_interval)
  373. continue
  374. if not result:
  375. logger.warning(f"任务 {task_id} 查询结果为空,继续等待...")
  376. time.sleep(self.poll_interval)
  377. continue
  378. # 解析状态
  379. status = result.get("status", "").lower()
  380. if status == TaskStatus.SUCCEEDED:
  381. logger.info(f"任务 {task_id} 完成,耗时: {int(elapsed)}秒")
  382. if callback:
  383. import inspect
  384. sig = inspect.signature(callback)
  385. param_count = len(sig.parameters)
  386. if param_count == 4:
  387. callback(task_id, callback_kwargs.get("output_path", ""), result, None)
  388. else:
  389. callback(task_id, result, None)
  390. return
  391. elif status == TaskStatus.FAILED:
  392. error_msg = result.get("error", {}).get("message", "未知错误")
  393. logger.error(f"任务 {task_id} 失败: {error_msg}")
  394. if callback:
  395. import inspect
  396. sig = inspect.signature(callback)
  397. param_count = len(sig.parameters)
  398. if param_count == 4:
  399. callback(task_id, callback_kwargs.get("output_path", ""), {}, error_msg)
  400. else:
  401. callback(task_id, {}, error_msg)
  402. return
  403. elif status in [TaskStatus.PENDING, TaskStatus.PROCESSING]:
  404. logger.info(f"任务 {task_id} 处理中({int(elapsed)}秒),状态: {status}")
  405. time.sleep(self.poll_interval)
  406. else:
  407. logger.warning(f"任务 {task_id} 未知状态: {status},继续等待...")
  408. time.sleep(self.poll_interval)
  409. def create_video_task_async(
  410. self,
  411. prompt: str,
  412. image_url: str,
  413. gen_params: Optional[str] = None,
  414. callback: Optional[Callable] = handle_video_result,
  415. output_path: Optional[str] = None,
  416. duration: Optional[int] = None,
  417. ratio: Optional[str] = None,
  418. resolution: Optional[str] = None,
  419. watermark: Optional[str] = None,
  420. camerafixed: Optional[str] = None,
  421. **kwargs
  422. ) -> Optional[str]:
  423. """
  424. 创建视频生成任务并立即返回task_id(不阻塞主流程)
  425. 任务会在后台线程中轮询,完成后调用回调函数。
  426. Args:
  427. prompt: 视频生成提示词(必填)
  428. image_url: 参考图片URL(必填)
  429. gen_params: 自定义生成参数字符串(可选,如果提供则忽略其他生成参数)
  430. callback: 可选的回调函数,可以是以下两种签名之一:
  431. 1. (task_id, result, error) -> None
  432. 2. (task_id, output_path, result, error) -> None
  433. output_path: 视频输出路径(可选,会传递给回调函数)
  434. duration: 视频时长(秒,默认4秒)
  435. ratio: 视频比例(默认"16:9")
  436. resolution: 视频分辨率(默认"1080p")
  437. watermark: 水印开关(默认"false")
  438. camerafixed: 相机固定开关(默认"false")
  439. **kwargs: 其他请求参数(会覆盖默认配置)
  440. Returns:
  441. 任务ID(task_id),如果创建失败则返回None
  442. Raises:
  443. APIError: 如果创建任务失败
  444. """
  445. # 创建任务
  446. task_response = self.create_video_task(
  447. prompt=prompt,
  448. image_url=image_url,
  449. gen_params=gen_params,
  450. duration=duration,
  451. ratio=ratio,
  452. resolution=resolution,
  453. watermark=watermark,
  454. camerafixed=camerafixed,
  455. **kwargs
  456. )
  457. task_id = task_response.get("id")
  458. if not task_id:
  459. logger.error("任务提交失败,无法启动后台轮询")
  460. return None
  461. logger.info(f"任务提交成功,task_id: {task_id},启动后台轮询...")
  462. # 准备回调参数
  463. callback_kwargs = {}
  464. if output_path:
  465. callback_kwargs["output_path"] = output_path
  466. # 启动后台线程轮询结果
  467. poll_thread = threading.Thread(
  468. target=self._background_poll,
  469. args=(task_id, callback),
  470. kwargs={"callback_kwargs": callback_kwargs},
  471. daemon=True # 守护线程:主程序退出时自动结束
  472. )
  473. poll_thread.start()
  474. return task_id
  475. def create_and_wait(
  476. self,
  477. prompt: str,
  478. image_url: str,
  479. gen_params: Optional[str] = None,
  480. callback: Optional[Callable] = None,
  481. output_path: Optional[str] = None,
  482. duration: Optional[int] = None,
  483. ratio: Optional[str] = None,
  484. resolution: Optional[str] = None,
  485. watermark: Optional[str] = None,
  486. camerafixed: Optional[str] = None,
  487. **kwargs
  488. ) -> Dict[str, Any]:
  489. """
  490. 创建视频生成任务并等待完成(同步方法,会阻塞)
  491. Args:
  492. prompt: 视频生成提示词(必填)
  493. image_url: 参考图片URL(必填)
  494. gen_params: 自定义生成参数字符串(可选,如果提供则忽略其他生成参数)
  495. callback: 可选的回调函数,可以是以下两种签名之一:
  496. 1. (task_id, result, error) -> None
  497. 2. (task_id, output_path, result, error) -> None
  498. output_path: 视频输出路径(可选,会传递给回调函数)
  499. duration: 视频时长(秒,默认4秒)
  500. ratio: 视频比例(默认"16:9")
  501. resolution: 视频分辨率(默认"1080p")
  502. watermark: 水印开关(默认"false")
  503. camerafixed: 相机固定开关(默认"false")
  504. **kwargs: 其他请求参数(会覆盖默认配置)
  505. Returns:
  506. 任务完成后的结果
  507. Raises:
  508. APIError: 如果请求失败
  509. TimeoutError: 如果任务超时
  510. """
  511. # 创建任务
  512. task_response = self.create_video_task(
  513. prompt=prompt,
  514. image_url=image_url,
  515. gen_params=gen_params,
  516. duration=duration,
  517. ratio=ratio,
  518. resolution=resolution,
  519. watermark=watermark,
  520. camerafixed=camerafixed,
  521. **kwargs
  522. )
  523. task_id = task_response.get("id")
  524. if not task_id:
  525. raise APIError("创建任务成功但未返回任务ID")
  526. logger.info(f"任务已创建,任务ID: {task_id},开始等待完成...")
  527. # 等待任务完成
  528. callback_kwargs = {}
  529. if output_path:
  530. callback_kwargs["output_path"] = output_path
  531. return self.wait_for_task(task_id, callback=callback, callback_kwargs=callback_kwargs)
  532. def get_video_url(self, result: Dict[str, Any]) -> Optional[str]:
  533. """
  534. 从任务结果中提取视频URL
  535. Args:
  536. result: 任务结果(从query_video_task或wait_for_task返回)
  537. Returns:
  538. 视频URL,如果不存在则返回None
  539. """
  540. try:
  541. content = result.get("content", {})
  542. if isinstance(content, dict):
  543. return content.get("video_url")
  544. return None
  545. except (KeyError, TypeError, AttributeError) as e:
  546. logger.warning(f"提取视频URL失败: {e}")
  547. return None
  548. def get_task_status(self, result: Dict[str, Any]) -> Optional[str]:
  549. """
  550. 从任务结果中提取任务状态
  551. Args:
  552. result: 任务结果
  553. Returns:
  554. 任务状态字符串,如果不存在则返回None
  555. """
  556. try:
  557. return result.get("status", "").lower()
  558. except (KeyError, TypeError, AttributeError):
  559. return None
  560. if __name__ == "__main__":
  561. # 示例用法
  562. client = ArkVideoClient()
  563. # 方式1:异步创建任务(立即返回task_id,不阻塞主流程)推荐
  564. # 使用默认生成参数
  565. task_id = client.create_video_task_async(
  566. prompt="图中的女生在街道上散步",
  567. 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",
  568. callback=handle_video_result,
  569. output_path="./output/video3.mp4"
  570. )
  571. # 方式1b:使用自定义生成参数(推荐方式)
  572. # task_id = client.create_video_task_async(
  573. # prompt="图中的女生在街道上散步",
  574. # image_url="https://example.com/image.jpg",
  575. # duration=5,
  576. # ratio="9:16",
  577. # resolution="720p",
  578. # callback=handle_video_result,
  579. # output_path="./output/video2.mp4"
  580. # )
  581. # 方式1c:使用自定义生成参数字符串
  582. # task_id = client.create_video_task_async(
  583. # prompt="图中的女生在街道上散步",
  584. # image_url="https://example.com/image.jpg",
  585. # gen_params="--dur 5 --rt 9:16 --rs 720p --wm false --cf false",
  586. # callback=handle_video_result,
  587. # output_path="./output/video2.mp4"
  588. # )
  589. print(f"任务已提交,task_id: {task_id},主流程继续执行...")
  590. # 等待视频下载完成(可选)
  591. while True:
  592. if os.path.exists("./output/video3.mp4"):
  593. print(f"视频下载完成,退出循环...")
  594. break
  595. time.sleep(10)
  596. print(f"等待10秒...")