janction-render 0.4.97__tar.gz → 0.4.99__tar.gz
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.
- {janction_render-0.4.97 → janction_render-0.4.99}/PKG-INFO +1 -1
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/__init__.py +1 -1
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/cli.py +19 -7
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/client.py +103 -13
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/mcp_server.py +7 -4
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/PKG-INFO +1 -1
- {janction_render-0.4.97 → janction_render-0.4.99}/pyproject.toml +1 -1
- {janction_render-0.4.97 → janction_render-0.4.99}/LICENSE +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/README.md +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/brief.py +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/hookup.py +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/links.py +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/mcp_app.html +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/mcp_common.py +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/prompts.py +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render/tooldocs.py +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/SOURCES.txt +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/dependency_links.txt +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/entry_points.txt +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/requires.txt +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/top_level.txt +0 -0
- {janction_render-0.4.97 → janction_render-0.4.99}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: janction-render
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.99
|
|
4
4
|
Summary: Cloud GPU rendering for Blender scenes and 3D models (GLB, FBX, USD, OBJ, STL), built for AI agents: MCP server (remote + stdio), HTTP API and CLI. Product shots, 360-degree turntables, animations and stills; preview in seconds with a critic; no local Blender or GPU. A 100 JPY welcome credit for a new key (one per network every 30 days) and 400 JPY more for saving a card that is not charged, then 0.1 JPY per GPU-second.
|
|
5
5
|
Author-email: "JasmyLab Inc." <contact@jasmylab.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -66,6 +66,11 @@ def _frames(s: str) -> tuple[int, int]:
|
|
|
66
66
|
raise argparse.ArgumentTypeError("frames must look like 1-240 or 12")
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
def _say_err(msg: str) -> None:
|
|
70
|
+
"""受け取りの進み具合(10/14: 5 GB の受け取りが 4 分黙っていた)。標準出力は結果の JSON だけにしておく。"""
|
|
71
|
+
print(msg, file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
|
|
69
74
|
def _progress(j: dict[str, Any]) -> None:
|
|
70
75
|
p = j["progress"]
|
|
71
76
|
print(f" {j['status']}: {p['frames_done']}/{p['frames_total']} frames"
|
|
@@ -119,7 +124,7 @@ def cmd_preview(c: Client, args: argparse.Namespace) -> int:
|
|
|
119
124
|
print(_j({k: j.get(k) for k in ("job_id", "status", "error", "log_tail", "warnings")}))
|
|
120
125
|
return 1
|
|
121
126
|
out = Path(args.out) if args.out else Path("render_out") / j["job_id"]
|
|
122
|
-
paths = c.download(j["job_id"], out)
|
|
127
|
+
paths = c.download(j["job_id"], out, on_progress=_say_err)
|
|
123
128
|
print(_j({"job_id": j["job_id"], "scene_id": sid, "frames": frames, "files": [str(p) for p in paths],
|
|
124
129
|
"warnings": j.get("warnings") or [], "gpu_seconds": j["gpu_seconds"], "device": j["device"],
|
|
125
130
|
"expires_at": j["expires_at"]}))
|
|
@@ -146,7 +151,7 @@ def cmd_render(c: Client, args: argparse.Namespace) -> int:
|
|
|
146
151
|
print(_j({k: j.get(k) for k in ("job_id", "status", "error", "log_tail")}))
|
|
147
152
|
return 1
|
|
148
153
|
out = Path(args.out) if args.out else Path("render_out") / j["job_id"]
|
|
149
|
-
paths = c.download(j["job_id"], out)
|
|
154
|
+
paths = c.download(j["job_id"], out, on_progress=_say_err)
|
|
150
155
|
print(_j({"job_id": j["job_id"], "scene_id": sid, "files": [str(p) for p in paths],
|
|
151
156
|
"warnings": j.get("warnings") or [], "gpu_seconds": j["gpu_seconds"], "device": j["device"],
|
|
152
157
|
"expires_at": j["expires_at"]}))
|
|
@@ -161,7 +166,7 @@ def cmd_status(c: Client, args: argparse.Namespace) -> int:
|
|
|
161
166
|
def cmd_download(c: Client, args: argparse.Namespace) -> int:
|
|
162
167
|
j = c.wait(args.job, timeout=args.wait, on_progress=_progress) if args.wait else c.job(args.job)
|
|
163
168
|
out = Path(args.out) if args.out else Path("render_out") / args.job
|
|
164
|
-
paths = c.download(args.job, out)
|
|
169
|
+
paths = c.download(args.job, out, on_progress=_say_err)
|
|
165
170
|
print(_j({"job_id": args.job, "status": j["status"], "files": [str(p) for p in paths]}))
|
|
166
171
|
return 0 if j["status"] == "done" else 1
|
|
167
172
|
|
|
@@ -383,7 +388,7 @@ def _outcome(c: Client, args: argparse.Namespace, name: str, options: dict[str,
|
|
|
383
388
|
print(_j({k: j.get(k) for k in ("job_id", "status", "error", "log_tail")}))
|
|
384
389
|
return 1
|
|
385
390
|
out = Path(args.out) if args.out else Path("render_out") / j["job_id"]
|
|
386
|
-
paths = c.download(j["job_id"], out, only=only)
|
|
391
|
+
paths = c.download(j["job_id"], out, only=only, on_progress=_say_err)
|
|
387
392
|
print(_j({"job_id": j["job_id"], "scene_id": sid, "files": [str(p) for p in paths], "gpu_seconds": j["gpu_seconds"],
|
|
388
393
|
"expires_at": j["expires_at"]}))
|
|
389
394
|
return 0
|
|
@@ -408,7 +413,7 @@ def cmd_try(c: Client, args: argparse.Namespace) -> int:
|
|
|
408
413
|
print(_j({k: j.get(k) for k in ("job_id", "status", "error", "log_tail")}))
|
|
409
414
|
return 1
|
|
410
415
|
out = Path(args.out) if args.out else Path("render_out") / j["job_id"]
|
|
411
|
-
paths = c.download(j["job_id"], out)
|
|
416
|
+
paths = c.download(j["job_id"], out, on_progress=_say_err)
|
|
412
417
|
print(f"done: {j['gpu_seconds']} GPU seconds on {j.get('device') or 'GPU'} -> " + ", ".join(str(p) for p in paths))
|
|
413
418
|
try:
|
|
414
419
|
me = c.me()
|
|
@@ -606,8 +611,15 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
606
611
|
pay = exc.payment()
|
|
607
612
|
if pay:
|
|
608
613
|
print(f"payment required: {exc.detail}", file=sys.stderr)
|
|
609
|
-
|
|
610
|
-
|
|
614
|
+
order = (pay.get("resume") or {}).get("order_id")
|
|
615
|
+
if order:
|
|
616
|
+
# 10/14: 払うと預かった仕事が自動で始まる。送り直すと同じ仕事が返る(2 本は描かない)
|
|
617
|
+
print(f"open this page in a browser and pay: {pay['checkout_url']}", file=sys.stderr)
|
|
618
|
+
print(f"the render then starts by itself (nothing to run again); follow it with: janction-render status {order}",
|
|
619
|
+
file=sys.stderr)
|
|
620
|
+
else:
|
|
621
|
+
print(f"open this page in a browser and pay, then run the command again: {pay['checkout_url']}",
|
|
622
|
+
file=sys.stderr)
|
|
611
623
|
print(_j(pay))
|
|
612
624
|
return 3
|
|
613
625
|
print(f"error: {exc}", file=sys.stderr)
|
|
@@ -25,6 +25,9 @@ CACHE = Path.home() / ".janction-render.json"
|
|
|
25
25
|
SCENE_SUFFIXES = (".blend", ".py", ".fbx", ".glb", ".gltf", ".obj", ".stl", ".ply", ".usd", ".usda", ".usdc", ".usdz", ".abc")
|
|
26
26
|
ONLY_CHOICES = ("all", "mp4", "frames", "sheet", "output", "video")
|
|
27
27
|
ARCHIVE_MIN_FILES = 8 # これ以上の数のファイルは zip で 1 回で取る(1 枚ずつだと入口の回数制限 429 に当たる、10/9)
|
|
28
|
+
# zip は約 1 GiB ずつの区切りで取る(10/14 受け取り側の 5 GB 試験: 1 本の zip は途中から取り直せず、切れると全部を 1 枚ずつ取り直していた)
|
|
29
|
+
ARCHIVE_PART_BYTES = 1 << 30
|
|
30
|
+
DOWNLOAD_TRIES = 3
|
|
28
31
|
# 公開の受付(render.janction.jp)の前に立つ Cloudflare は、1 回の送信を 100 MB までしか通さない(無料プラン)。超えると受付に
|
|
29
32
|
# 届かず HTML の 413 になるので、送る前に断ってリンク(scene_url / asset_urls)で渡すよう言う。素材は 100 MB ずつに分けて送る
|
|
30
33
|
# (10/12)。手元や自前の受付には掛けない。JANCTION_RENDER_MAX_UPLOAD_MB で変えられる(0 で見ない)
|
|
@@ -37,6 +40,15 @@ ASSET_LINK_MAX_BYTES = 128 * 1024 * 1024 # 受付の素材 1 つの上限(JR
|
|
|
37
40
|
_FRAME_NAME_RE = re.compile(r"^(part-\d+_)?frame_")
|
|
38
41
|
|
|
39
42
|
|
|
43
|
+
def _have(dest: Path, a: dict[str, Any]) -> bool:
|
|
44
|
+
"""dest にもう同じ大きさのファイルがあるか(大きさの分からない古い受付では、無いものとして取り直す)。"""
|
|
45
|
+
n = a.get("size")
|
|
46
|
+
try:
|
|
47
|
+
return n is not None and dest.is_file() and dest.stat().st_size == int(n)
|
|
48
|
+
except (OSError, ValueError):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
40
52
|
def select_artifacts(artifacts: list[dict[str, Any]], only: Optional[str] = None) -> list[dict[str, Any]]:
|
|
41
53
|
"""成果物の絞り込み: all / mp4(output.mp4 だけ)/ frames(PNG のコマだけ)/ sheet(4 コマ並べだけ)/ output(mp4 か sheet)。"""
|
|
42
54
|
o = (only or "all").strip().lower()
|
|
@@ -273,7 +285,8 @@ class ClientError(RuntimeError):
|
|
|
273
285
|
out[k] = self.extra[k]
|
|
274
286
|
# ようこそクレジット(docs/46)の受付なら付いてくる。card_offer はカード登録(0 円)で無料の GPU 時間が足される鍵の案内、
|
|
275
287
|
# topup_options はチャージの選択肢(docs/62 P0-1: REST の 402 と同じものを AI に渡す)
|
|
276
|
-
|
|
288
|
+
# resume: 402 で預かった注文(o_…)。払うとその仕事が自動で始まり、render_status(o_…) で追える(10/14)
|
|
289
|
+
for k in ("welcome", "first_topup_bonus", "card_offer", "topup_options", "topup_credit_yen", "resume"):
|
|
277
290
|
if self.extra.get(k):
|
|
278
291
|
out[k] = self.extra[k]
|
|
279
292
|
if self.extra.get("accepts"):
|
|
@@ -840,8 +853,11 @@ class Client:
|
|
|
840
853
|
return j
|
|
841
854
|
time.sleep(interval)
|
|
842
855
|
|
|
843
|
-
def download(self, job_id: str, out_dir: str | Path, only: Optional[list[str] | str] = None
|
|
844
|
-
|
|
856
|
+
def download(self, job_id: str, out_dir: str | Path, only: Optional[list[str] | str] = None,
|
|
857
|
+
on_progress: Optional[Callable[[str], None]] = None) -> list[Path]:
|
|
858
|
+
"""成果物を out_dir に落とす。only は名前の一覧か、'mp4' / 'frames' / 'sheet' / 'output' / 'all'。
|
|
859
|
+
on_progress は進み具合の 1 行(CLI は標準エラーに出す。10/14: 5 GB の受け取りが 4 分黙っていた)。"""
|
|
860
|
+
self._say = on_progress or (lambda _msg: None)
|
|
845
861
|
j = self.job(job_id)
|
|
846
862
|
out = Path(out_dir)
|
|
847
863
|
out.mkdir(parents=True, exist_ok=True)
|
|
@@ -854,40 +870,114 @@ class Client:
|
|
|
854
870
|
got = self._download_archive(job_id, out, wanted)
|
|
855
871
|
if got is not None:
|
|
856
872
|
return got
|
|
857
|
-
|
|
873
|
+
self._say("the zip did not come through; fetching the files one by one")
|
|
874
|
+
last = time.monotonic()
|
|
875
|
+
for i, a in enumerate(wanted, 1):
|
|
876
|
+
dest = out / a["name"]
|
|
877
|
+
if _have(dest, a): # 前の回(切れた zip・打ち直し)で届いている
|
|
878
|
+
paths.append(dest)
|
|
879
|
+
continue
|
|
880
|
+
if len(wanted) > 1 and time.monotonic() - last > 10:
|
|
881
|
+
last = time.monotonic()
|
|
882
|
+
self._say(f"downloading {i}/{len(wanted)} files")
|
|
858
883
|
# 受付が返す URL は公開 URL の土台で組まれている。自分がつないだ先(SSH トンネルなど)でも
|
|
859
884
|
# 落とせるように、パスだけを取り出して自分の server に付け直す
|
|
860
885
|
parts = urlsplit(a["url"])
|
|
861
886
|
path = parts.path + (f"?{parts.query}" if parts.query else "")
|
|
862
887
|
url = f"{self.server}{path}" if parts.path.startswith("/v1/") else a["url"]
|
|
863
|
-
|
|
864
|
-
self._raise(r)
|
|
865
|
-
dest = out / a["name"]
|
|
866
|
-
with dest.open("wb") as fh:
|
|
867
|
-
for chunk in r.iter_content(1024 * 1024):
|
|
868
|
-
fh.write(chunk)
|
|
888
|
+
self._download_file(url, dest)
|
|
869
889
|
paths.append(dest)
|
|
870
890
|
return paths
|
|
871
891
|
|
|
892
|
+
def _download_file(self, url: str, dest: Path) -> None:
|
|
893
|
+
"""1 つのファイルを dest.part に受け取り、切れたら Range で続きから(10/14。受付の FileResponse は Range に答える)。"""
|
|
894
|
+
part = dest.with_name(dest.name + ".part")
|
|
895
|
+
for attempt in range(DOWNLOAD_TRIES):
|
|
896
|
+
have = part.stat().st_size if part.exists() else 0
|
|
897
|
+
headers = {**self._headers(), **({"Range": f"bytes={have}-"} if have else {})}
|
|
898
|
+
try:
|
|
899
|
+
with self.s.get(url, headers=headers, stream=True, timeout=600) as r:
|
|
900
|
+
if r.status_code == 416: # もう全部ある
|
|
901
|
+
break
|
|
902
|
+
self._raise(r)
|
|
903
|
+
with part.open("ab" if r.status_code == 206 else "wb") as fh:
|
|
904
|
+
for chunk in r.iter_content(1024 * 1024):
|
|
905
|
+
fh.write(chunk)
|
|
906
|
+
break
|
|
907
|
+
except requests.RequestException:
|
|
908
|
+
if attempt == DOWNLOAD_TRIES - 1:
|
|
909
|
+
raise
|
|
910
|
+
time.sleep(2.0 * (attempt + 1))
|
|
911
|
+
part.replace(dest)
|
|
912
|
+
|
|
872
913
|
def _download_archive(self, job_id: str, out: Path, wanted: list[dict[str, Any]]) -> Optional[list[Path]]:
|
|
873
|
-
"""まとめて zip(GET /v1/jobs/{id}/archive)で取り、欲しい名前だけを out
|
|
874
|
-
|
|
914
|
+
"""まとめて zip(GET /v1/jobs/{id}/archive)で取り、欲しい名前だけを out に出す。大きいときは名前の順に約 1 GiB ずつの
|
|
915
|
+
区切り(first・last)で取り、届いた区切りは残して、切れた区切りだけを取り直す(10/14)。受付が古くて口が無い・取り直しても
|
|
916
|
+
届かない・中身が足りないときは None(呼び手が 1 枚ずつに戻り、届いているファイルは飛ばす)。"""
|
|
917
|
+
ordered = sorted(wanted, key=lambda a: a["name"])
|
|
918
|
+
groups: list[list[dict[str, Any]]] = []
|
|
919
|
+
size = 0
|
|
920
|
+
for a in ordered:
|
|
921
|
+
n = int(a.get("size") or 0)
|
|
922
|
+
if not groups or (size + n > ARCHIVE_PART_BYTES and groups[-1]):
|
|
923
|
+
groups.append([])
|
|
924
|
+
size = 0
|
|
925
|
+
groups[-1].append(a)
|
|
926
|
+
size += n
|
|
927
|
+
total_mb = sum(int(a.get("size") or 0) for a in wanted) / 1048576
|
|
928
|
+
if len(groups) == 1:
|
|
929
|
+
self._say(f"downloading {len(wanted)} files in one zip ({total_mb:.0f} MB)")
|
|
930
|
+
return self._fetch_archive(job_id, out, wanted)
|
|
931
|
+
self._say(f"downloading {len(wanted)} files ({total_mb / 1024:.1f} GB) in {len(groups)} zips of about 1 GB")
|
|
932
|
+
paths: list[Path] = []
|
|
933
|
+
for gi, g in enumerate(groups, 1):
|
|
934
|
+
if all(_have(out / a["name"], a) for a in g):
|
|
935
|
+
paths += [out / a["name"] for a in g]
|
|
936
|
+
continue
|
|
937
|
+
got: Any = None
|
|
938
|
+
for attempt in range(DOWNLOAD_TRIES):
|
|
939
|
+
self._say(f"zip {gi}/{len(groups)} ({len(g)} files, {sum(int(a.get('size') or 0) for a in g) / 1048576:.0f} MB)"
|
|
940
|
+
+ (f", again (try {attempt + 1})" if attempt else ""))
|
|
941
|
+
got = self._fetch_archive(job_id, out, g, (g[0]["name"], g[-1]["name"]))
|
|
942
|
+
if got is not None:
|
|
943
|
+
break
|
|
944
|
+
time.sleep(2.0 * (attempt + 1))
|
|
945
|
+
if got == "unsliced": # 区切りを知らない受付: 前のとおり 1 本で
|
|
946
|
+
return self._fetch_archive(job_id, out, wanted)
|
|
947
|
+
if got is None:
|
|
948
|
+
return None
|
|
949
|
+
paths += got
|
|
950
|
+
return paths
|
|
951
|
+
|
|
952
|
+
def _fetch_archive(self, job_id: str, out: Path, wanted: list[dict[str, Any]],
|
|
953
|
+
span: Optional[tuple[str, str]] = None) -> Any:
|
|
954
|
+
"""zip を 1 本取って欲しい名前を out に出す。span(最初と最後の名前)を渡すと、その一続きだけを頼む。
|
|
955
|
+
区切りを頼んだのに受付が X-Archive-Range を返さなければ "unsliced"。"""
|
|
875
956
|
import shutil
|
|
876
957
|
import tempfile
|
|
877
958
|
import zipfile
|
|
959
|
+
from urllib.parse import urlencode
|
|
878
960
|
|
|
879
961
|
names = {a["name"] for a in wanted}
|
|
880
962
|
exts = sorted({Path(n).suffix.lstrip(".").lower() for n in names if Path(n).suffix})
|
|
881
|
-
|
|
963
|
+
q = {**({"only": ",".join(exts)} if exts else {}), **({"first": span[0], "last": span[1]} if span else {})}
|
|
964
|
+
url = f"{self.server}/v1/jobs/{job_id}/archive" + (f"?{urlencode(q)}" if q else "")
|
|
882
965
|
tmp: Optional[str] = None
|
|
883
966
|
try:
|
|
884
967
|
with self.s.get(url, headers=self._headers(), stream=True, timeout=1800) as r:
|
|
885
968
|
if r.status_code != 200:
|
|
886
969
|
return None
|
|
970
|
+
if span and not r.headers.get("X-Archive-Range"):
|
|
971
|
+
return "unsliced"
|
|
887
972
|
fd, tmp = tempfile.mkstemp(suffix=".zip", dir=out)
|
|
973
|
+
got_b, last = 0, time.monotonic()
|
|
888
974
|
with os.fdopen(fd, "wb") as fh:
|
|
889
975
|
for chunk in r.iter_content(1024 * 1024):
|
|
890
976
|
fh.write(chunk)
|
|
977
|
+
got_b += len(chunk)
|
|
978
|
+
if time.monotonic() - last > 15:
|
|
979
|
+
last = time.monotonic()
|
|
980
|
+
getattr(self, "_say", lambda _m: None)(f" {got_b // 1048576} MB")
|
|
891
981
|
paths: list[Path] = []
|
|
892
982
|
with zipfile.ZipFile(tmp) as zf:
|
|
893
983
|
if not names <= set(zf.namelist()):
|
|
@@ -236,17 +236,20 @@ def _payment_block(exc: ClientError) -> Optional[dict[str, Any]]:
|
|
|
236
236
|
p = exc.payment()
|
|
237
237
|
if p is None:
|
|
238
238
|
return None
|
|
239
|
+
# 払ったあとは預かり(resume.order_id)から自動で始まる受付(0.4.98〜)なら、送り直させない(10/14)
|
|
240
|
+
after = ("after they paid (or registered the card) the render starts by itself: tell them so and follow it with "
|
|
241
|
+
"render_status(job_id=resume.order_id); do not upload or submit it again (a resend returns the same job)"
|
|
242
|
+
if p.get("resume") else "after they paid, call billing() to confirm the balance, then submit the same job again")
|
|
239
243
|
if p.get("card_offer"):
|
|
240
244
|
# 0 円のカード登録で無料の GPU 時間が足される鍵には、その道を先に(docs/62 P0-1)。チャージは代わりの道
|
|
241
245
|
p["next"] = ("first tell the user card_offer.tell_user in their own language (registering a card costs nothing and adds free "
|
|
242
|
-
"GPU time; they open card_offer.link in a browser), and offer the top-up at checkout_url as the other way; after
|
|
243
|
-
"either is done, call billing() to confirm, then submit the same job again")
|
|
246
|
+
"GPU time; they open card_offer.link in a browser), and offer the top-up at checkout_url as the other way; " + after)
|
|
244
247
|
return p
|
|
245
248
|
p["next"] = (("relay tell_user to the user in their own language (what this render costs and what the top-up buys) "
|
|
246
249
|
"with checkout_url " if p.get("tell_user") else
|
|
247
250
|
"tell the user the job goes past the free GPU time left and what the top-up costs; show checkout_url ")
|
|
248
|
-
+ "and ask them to pay in a browser (Stripe; the rest stays as credit);
|
|
249
|
-
"
|
|
251
|
+
+ "and ask them to pay in a browser (Stripe; the rest stays as credit); " + after
|
|
252
|
+
+ " (or render a smaller job that fits the free time)")
|
|
250
253
|
return p
|
|
251
254
|
|
|
252
255
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: janction-render
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.99
|
|
4
4
|
Summary: Cloud GPU rendering for Blender scenes and 3D models (GLB, FBX, USD, OBJ, STL), built for AI agents: MCP server (remote + stdio), HTTP API and CLI. Product shots, 360-degree turntables, animations and stills; preview in seconds with a critic; no local Blender or GPU. A 100 JPY welcome credit for a new key (one per network every 30 days) and 400 JPY more for saving a card that is not charged, then 0.1 JPY per GPU-second.
|
|
5
5
|
Author-email: "JasmyLab Inc." <contact@jasmylab.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "janction-render"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.99"
|
|
8
8
|
description = "Cloud GPU rendering for Blender scenes and 3D models (GLB, FBX, USD, OBJ, STL), built for AI agents: MCP server (remote + stdio), HTTP API and CLI. Product shots, 360-degree turntables, animations and stills; preview in seconds with a critic; no local Blender or GPU. A 100 JPY welcome credit for a new key (one per network every 30 days) and 400 JPY more for saving a card that is not charged, then 0.1 JPY per GPU-second."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{janction_render-0.4.97 → janction_render-0.4.99}/janction_render.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|