liblibai_comfyui_runner.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. #!/usr/bin/env python3
  2. """LibLibAI ComfyUI Workflow Runner
  3. 用法:
  4. python liblibai_comfyui_runner.py workflow.json
  5. python liblibai_comfyui_runner.py workflow.json --output result.png
  6. """
  7. import argparse
  8. import base64
  9. import hashlib
  10. import hmac
  11. import json
  12. import os
  13. import sys
  14. import time
  15. import uuid
  16. from pathlib import Path
  17. import requests
  18. from dotenv import load_dotenv
  19. load_dotenv()
  20. DOMAIN = os.getenv("LIBLIBAI_DOMAIN", "https://openapi.liblibai.cloud")
  21. ACCESS_KEY = os.getenv("LIBLIBAI_ACCESS_KEY")
  22. SECRET_KEY = os.getenv("LIBLIBAI_SECRET_KEY")
  23. DEFAULT_TEMPLATE = "4df2efa0f18d46dc9758803e478eb51c"
  24. def generate_auth_url(uri: str) -> str:
  25. """生成带 HMAC-SHA1 签名的完整 URL"""
  26. ts = str(int(time.time() * 1000))
  27. nonce = uuid.uuid4().hex
  28. sign_str = f"{uri}&{ts}&{nonce}"
  29. dig = hmac.new(SECRET_KEY.encode(), sign_str.encode(), hashlib.sha1).digest()
  30. signature = base64.urlsafe_b64encode(dig).rstrip(b"=").decode()
  31. return f"{DOMAIN}{uri}?AccessKey={ACCESS_KEY}&Timestamp={ts}&SignatureNonce={nonce}&Signature={signature}"
  32. def submit_workflow(workflow_data: dict) -> str:
  33. """提交 ComfyUI workflow 生图任务"""
  34. uri = "/api/generate/comfyui/app"
  35. url = generate_auth_url(uri)
  36. payload = {
  37. "templateUuid": DEFAULT_TEMPLATE,
  38. "generateParams": workflow_data
  39. }
  40. print(f"提交任务...")
  41. resp = requests.post(url, json=payload, headers={"Content-Type": "application/json"})
  42. resp.raise_for_status()
  43. data = resp.json()
  44. if data.get("code") != 0:
  45. raise Exception(f"提交失败: {data.get('msg', 'unknown error')}")
  46. generate_uuid = data["data"]["generateUuid"]
  47. print(f"任务 ID: {generate_uuid}")
  48. return generate_uuid
  49. def poll_result(generate_uuid: str, timeout: int = 600) -> dict:
  50. """轮询任务结果"""
  51. uri = "/api/generate/comfy/status"
  52. url = generate_auth_url(uri)
  53. start_time = time.time()
  54. print(f"轮询结果 (超时 {timeout}s)...")
  55. while time.time() - start_time < timeout:
  56. resp = requests.post(url, json={"generateUuid": generate_uuid})
  57. resp.raise_for_status()
  58. data = resp.json()
  59. if data.get("code") != 0:
  60. raise Exception(f"查询失败: {data.get('msg', 'unknown error')}")
  61. result = data["data"]
  62. status = result["generateStatus"]
  63. # 1=等待 2=执行中 3=已生图 4=审核中 5=成功 6=失败
  64. if status in [1, 2, 3, 4]:
  65. status_text = {1: "等待", 2: "执行中", 3: "已生图", 4: "审核中"}[status]
  66. print(f" 状态: {status_text} 进度: {result.get('percentCompleted', 0):.0%}")
  67. time.sleep(5)
  68. continue
  69. if status == 5:
  70. print(f"✓ 任务成功")
  71. print(f" 消耗积分: {result.get('pointsCost', 0)}")
  72. print(f" 剩余积分: {result.get('accountBalance', 0)}")
  73. return result
  74. if status == 6:
  75. raise Exception(f"任务失败: {result.get('generateMsg', 'unknown')}")
  76. raise TimeoutError(f"轮询超时 ({timeout}s)")
  77. def save_results(result: dict, output_dir: Path):
  78. """保存生成的图片和视频"""
  79. output_dir.mkdir(parents=True, exist_ok=True)
  80. images = result.get("images", [])
  81. videos = result.get("videos", [])
  82. print(f"\n生成结果:")
  83. print(f" 图片: {len(images)} 张")
  84. print(f" 视频: {len(videos)} 个")
  85. for i, img in enumerate(images):
  86. if img.get("auditStatus") != 3:
  87. print(f" 图片 {i+1}: 审核未通过 (status={img.get('auditStatus')})")
  88. continue
  89. url = img["imageUrl"]
  90. filename = f"image_{i+1}.png"
  91. filepath = output_dir / filename
  92. print(f" 下载: {filename}")
  93. resp = requests.get(url)
  94. resp.raise_for_status()
  95. filepath.write_bytes(resp.content)
  96. print(f" -> {filepath}")
  97. for i, vid in enumerate(videos):
  98. if vid.get("auditStatus") != 3:
  99. print(f" 视频 {i+1}: 审核未通过 (status={vid.get('auditStatus')})")
  100. continue
  101. url = vid["videoUrl"]
  102. filename = f"video_{i+1}.mp4"
  103. filepath = output_dir / filename
  104. print(f" 下载: {filename}")
  105. resp = requests.get(url)
  106. resp.raise_for_status()
  107. filepath.write_bytes(resp.content)
  108. print(f" -> {filepath}")
  109. def main():
  110. parser = argparse.ArgumentParser(description="LibLibAI ComfyUI Workflow Runner")
  111. parser.add_argument("workflow", help="workflow JSON 文件路径")
  112. parser.add_argument("--output", "-o", default="output", help="输出目录,默认 output/")
  113. parser.add_argument("--timeout", "-t", type=int, default=600, help="轮询超时秒数,默认 600")
  114. args = parser.parse_args()
  115. if not ACCESS_KEY or not SECRET_KEY:
  116. print("ERROR: 请设置环境变量 LIBLIBAI_ACCESS_KEY 和 LIBLIBAI_SECRET_KEY")
  117. sys.exit(1)
  118. workflow_path = Path(args.workflow)
  119. if not workflow_path.exists():
  120. print(f"ERROR: 文件不存在: {workflow_path}")
  121. sys.exit(1)
  122. print(f"加载 workflow: {workflow_path}")
  123. with open(workflow_path, "r", encoding="utf-8") as f:
  124. workflow_data = json.load(f)
  125. try:
  126. generate_uuid = submit_workflow(workflow_data)
  127. result = poll_result(generate_uuid, timeout=args.timeout)
  128. save_results(result, Path(args.output))
  129. print("\n✓ 完成")
  130. except Exception as e:
  131. print(f"\n✗ 错误: {e}")
  132. sys.exit(1)
  133. if __name__ == "__main__":
  134. main()