PrintGuard_Aholo 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lux3d_client.py ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """Aholo 开放平台 API 客户端:生成任务/轮询/下载、Asset 上传,
3
+ 以及概念图能力(schema 直译自官方 OpenAPI,已实测)。
4
+ 常规一条龙走 pipeline.py;概念图可独立导入调用(与生成同串行规则)。"""
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import time
9
+ from pathlib import Path
10
+
11
+ import requests
12
+
13
+ BASE = "https://api.aholo3d.cn"
14
+ KEY_FILE = "secrets.local.json"
15
+ OUT_DIR = Path("output/smoke")
16
+ POLL_SEC = 15
17
+ TIMEOUT_SEC = 20 * 60
18
+
19
+
20
+ def load_key(explicit: str | None = None) -> str:
21
+ """取 API Key,优先级:显式入参(公网部署由用户填写)> 环境变量 > 本地 secrets.local.json。
22
+
23
+ 公网部署(魔搭等托管平台)里不能把 key 写进镜像/环境变量,必须由调用方
24
+ 通过工具参数 api_key 传入;本地开发仍可用环境变量或 secrets.local.json。"""
25
+ import os
26
+ for cand in (explicit, os.environ.get("AHOLO_API_KEY", "")):
27
+ s = (cand or "").strip()
28
+ if s:
29
+ return s
30
+ p = Path(KEY_FILE)
31
+ if p.exists():
32
+ raw = p.read_text(encoding="utf-8").strip()
33
+ s = json.loads(raw) if raw.startswith('"') else raw
34
+ if ":" in s:
35
+ s = s.split(":", 1)[1].strip()
36
+ if s:
37
+ return s
38
+ raise SystemExit(
39
+ "缺少 AHOLO API Key:公网调用请在工具参数 api_key 传入,"
40
+ "本地可设置环境变量 AHOLO_API_KEY 或填 secrets.local.json")
41
+
42
+
43
+ def create_task(key: str, prompt: str, version: str = "G1-Turbo",
44
+ face_count: int = 100000, style: str | None = None,
45
+ output_format: list | None = None,
46
+ image: str | None = None) -> int:
47
+ body = {
48
+ "prompt": prompt,
49
+ "version": version,
50
+ "faceCount": face_count,
51
+ "outputFormat": output_format or ["glb"],
52
+ "enablePbr": False, # 白模(仅 G1-Turbo 生效)
53
+ "aiPredictSize": False, # 尺寸策略:本地确定性缩放
54
+ }
55
+ if style:
56
+ body["style"] = style
57
+ if image: # 可选参考图(图+文同时输入):URL 直传,本地文件先走 Asset 上传
58
+ body["img"] = image if image.startswith(("http://", "https://")) else upload_mesh(image, key=key)
59
+ for attempt in range(3):
60
+ r = requests.post(f"{BASE}/lux3d/v1/generate/text-to-3d/task/create",
61
+ headers={"Authorization": key}, json=body, timeout=30)
62
+ d = r.json()
63
+ if d.get("c") == "0":
64
+ return int(d["d"])
65
+ code = str(d.get("c", ""))
66
+ print(f"[create] attempt{attempt} c={code} m={d.get('m')}", flush=True)
67
+ if "CONCURRENCY" in code:
68
+ time.sleep(30)
69
+ continue
70
+ if r.status_code >= 500:
71
+ time.sleep(10)
72
+ continue
73
+ raise SystemExit(f"create failed: {d}")
74
+ raise SystemExit("create: 重试耗尽")
75
+
76
+
77
+ def poll_task(key: str, taskid: int) -> dict:
78
+ t0 = time.time()
79
+ last = None
80
+ while time.time() - t0 < TIMEOUT_SEC:
81
+ r = requests.get(f"{BASE}/lux3d/v1/generate/task/get",
82
+ headers={"Authorization": key},
83
+ params={"taskid": taskid}, timeout=30)
84
+ d = r.json()
85
+ if d.get("c") != "0":
86
+ print(f"[poll] c={d.get('c')} m={d.get('m')}", flush=True)
87
+ time.sleep(POLL_SEC)
88
+ continue
89
+ data = d["d"]
90
+ st = data["status"]
91
+ if st != last:
92
+ print(f"[poll] task={taskid} status={st} elapsed={time.time()-t0:.0f}s", flush=True)
93
+ last = st
94
+ if st in (3, 4, 6):
95
+ data["elapsed_sec"] = round(time.time() - t0)
96
+ return data
97
+ time.sleep(POLL_SEC)
98
+ raise SystemExit(f"task {taskid} 轮询超时")
99
+
100
+
101
+ def download_glbs(outputs: list, name: str,
102
+ out_dir: Path | None = None) -> list[Path]:
103
+ out_dir = Path(out_dir) if out_dir else OUT_DIR
104
+ out_dir.mkdir(parents=True, exist_ok=True)
105
+ paths = []
106
+ for o in outputs:
107
+ url = o.get("content") or ""
108
+ if not url.startswith("http"):
109
+ continue # 跳过 NOT_REQUESTED 等
110
+ ext = url.split("?")[0].rsplit(".", 1)[-1].lower() or "glb"
111
+ fn = out_dir / f"{name}_{len(paths)}.{ext}" # 按真实扩展名命名(G1 会同时给 ZIP+GLB)
112
+ with requests.get(url, stream=True, timeout=120) as r:
113
+ r.raise_for_status()
114
+ with open(fn, "wb") as f:
115
+ for chunk in r.iter_content(1 << 16):
116
+ f.write(chunk)
117
+ paths.append(fn)
118
+ print(f"[download] {fn.name} {fn.stat().st_size/1e6:.1f}MB", flush=True)
119
+ return paths
120
+
121
+
122
+ # ---------- Asset 上传(本地文件 -> 公网 URL;参考图上传用) ----------
123
+
124
+ def _client_config(key: str | None = None):
125
+ """SDK v1.9+:create_*_client(config),config 显式带 key 与 region。"""
126
+ from manycore.aholo_sdk_core import AholoClientConfig
127
+ return AholoClientConfig(api_key=load_key(key), region="cn")
128
+
129
+
130
+ def upload_mesh(path, key: str | None = None) -> str:
131
+ """本地文件 -> 公网 URL(官方 Asset SDK,内置分块上传)。"""
132
+ from manycore.aholo_sdk_asset import create_asset_client
133
+ asset = create_asset_client(_client_config(key))
134
+ res = asset.upload_file(str(path))
135
+ return res.url
136
+
137
+
138
+ # ---------- 群核能力扩展(概念图)——官方 OpenAPI schema 直译,已实测(v0.3.1),未接入交付链 ----------
139
+
140
+ def _wait_and_download(key: str, tid: int, dest_dir, want_ext: set) -> list:
141
+ """Lux3D 任务通用:轮询至终态 + 按扩展名下载。"""
142
+ data = poll_task(key, tid)
143
+ if data["status"] != 3:
144
+ raise RuntimeError(f"task {tid} 未成功: status={data['status']}")
145
+ out_dir = Path(dest_dir)
146
+ out_dir.mkdir(parents=True, exist_ok=True)
147
+ paths = []
148
+ for o in data.get("outputs", []):
149
+ u = (o or {}).get("content") or ""
150
+ if not u.startswith("http"):
151
+ continue
152
+ ext = u.split("?")[0].rsplit(".", 1)[-1].lower()
153
+ if ext not in want_ext:
154
+ continue
155
+ dest = out_dir / f"{tid}_{len(paths)}.{ext}"
156
+ with requests.get(u, stream=True, timeout=180) as r:
157
+ r.raise_for_status()
158
+ with open(dest, "wb") as f:
159
+ for chunk in r.iter_content(1 << 16):
160
+ f.write(chunk)
161
+ paths.append(dest)
162
+ return paths
163
+
164
+
165
+ def concept_image(key: str, prompt: str, image: str | None = None,
166
+ dest_dir="output/concept") -> Path:
167
+ """multimodal-to-image:概念图(纯文案或图文)。返回图片路径。"""
168
+ body = {"prompt": prompt}
169
+ if image:
170
+ body["img"] = image if image.startswith(("http://", "https://")) else upload_mesh(image, key=key)
171
+ r = requests.post(f"{BASE}/lux3d/v1/generate/multimodal-to-image/task/create",
172
+ headers={"Authorization": key}, json=body, timeout=30)
173
+ d = r.json()
174
+ if d.get("c") != "0":
175
+ raise RuntimeError(f"concept create failed: {d}")
176
+ paths = _wait_and_download(key, int(d["d"]), dest_dir, {"png", "jpg", "jpeg", "webp"})
177
+ if not paths:
178
+ raise RuntimeError("concept: no image url in outputs")
179
+ return paths[0]