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 +179 -0
- mcp_server.py +541 -0
- pipeline.py +210 -0
- printguard.py +833 -0
- printguard_aholo-0.5.0.dist-info/METADATA +125 -0
- printguard_aholo-0.5.0.dist-info/RECORD +10 -0
- printguard_aholo-0.5.0.dist-info/WHEEL +5 -0
- printguard_aholo-0.5.0.dist-info/entry_points.txt +3 -0
- printguard_aholo-0.5.0.dist-info/licenses/LICENSE +21 -0
- printguard_aholo-0.5.0.dist-info/top_level.txt +4 -0
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]
|