janction-render 0.3.0__tar.gz → 0.3.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janction-render
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Cloud GPU render farm for Blender, for AI agents: MCP server (remote + stdio) and CLI. Render Blender without a GPU from Claude, ChatGPT, Claude Code, Codex, Cursor.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -25,7 +25,12 @@ without a GPU, or when rendering locally is slow.
25
25
 
26
26
  - Input: a `.blend` file (textures travel with it), a **bpy Python script that builds the scene** (no local Blender
27
27
  needed), or a **3D file** (glTF/GLB, FBX, USD, OBJ, STL, PLY, Alembic) that is imported into an empty scene.
28
- - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render.
28
+ - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render;
29
+ `environment="compare"` previews the same frame under all four presets in one labelled image, so the agent can pick one.
30
+ - A `.gltf` or `.obj` brings its `.bin` / `.mtl` / textures along automatically (from the same folder or the same URL folder).
31
+ - `orbit=True` turns any scene or imported model into a turntable: an orbit camera circles it once (`orbit_frames`, default 24;
32
+ a preview shows 0/90/180/270 degrees, a final render without `frame_end` gives the whole turn as an MP4).
33
+ - While a job renders, `render_status` counts finished frames inside the running chunk, so the ETA updates every few seconds.
29
34
  - `scene_info` reads the scene without rendering (cameras, frame range, missing files).
30
35
  - `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
31
36
  - `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
@@ -107,10 +112,11 @@ Then, in Claude Code:
107
112
  | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?)` | PNG or MP4; returns job_id + estimate |
108
113
  | `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id, only="mp4" / "frames" / "all")` files or links; `render_cancel(job_id)` |
109
114
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
110
- | `render_info()` | workers online, queue, expected wait, supported inputs, environment presets, Blender versions |
115
+ | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
111
116
 
112
- Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`; `environment_strength`,
113
- `environment_visible=False` for a flat grey backdrop with HDRI lighting), `blender` (`"5.2"`), and `assets` (stdio: local files
117
+ Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`, `compare` on previews; `environment_strength`,
118
+ `environment_visible=False` for a flat grey backdrop with HDRI lighting), `orbit` / `orbit_frames` / `orbit_elevation` (turntable),
119
+ `blender` (`"5.2"`), and `assets` (stdio: local files
114
120
  sent with the scene, found in the script through `os.environ["JR_ASSETS_DIR"]`; a `.blend`'s external files are collected
115
121
  automatically with `pip install janction-render[blend]`) or `asset_urls` (remote).
116
122
 
@@ -143,7 +149,7 @@ POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scen
143
149
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
144
150
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
145
151
  POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
146
- environment, environment_strength, environment_visible, blender}
152
+ environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
147
153
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
148
154
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
149
155
  POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
@@ -8,7 +8,12 @@ without a GPU, or when rendering locally is slow.
8
8
 
9
9
  - Input: a `.blend` file (textures travel with it), a **bpy Python script that builds the scene** (no local Blender
10
10
  needed), or a **3D file** (glTF/GLB, FBX, USD, OBJ, STL, PLY, Alembic) that is imported into an empty scene.
11
- - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render.
11
+ - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render;
12
+ `environment="compare"` previews the same frame under all four presets in one labelled image, so the agent can pick one.
13
+ - A `.gltf` or `.obj` brings its `.bin` / `.mtl` / textures along automatically (from the same folder or the same URL folder).
14
+ - `orbit=True` turns any scene or imported model into a turntable: an orbit camera circles it once (`orbit_frames`, default 24;
15
+ a preview shows 0/90/180/270 degrees, a final render without `frame_end` gives the whole turn as an MP4).
16
+ - While a job renders, `render_status` counts finished frames inside the running chunk, so the ETA updates every few seconds.
12
17
  - `scene_info` reads the scene without rendering (cameras, frame range, missing files).
13
18
  - `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
14
19
  - `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
@@ -90,10 +95,11 @@ Then, in Claude Code:
90
95
  | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?)` | PNG or MP4; returns job_id + estimate |
91
96
  | `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id, only="mp4" / "frames" / "all")` files or links; `render_cancel(job_id)` |
92
97
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
93
- | `render_info()` | workers online, queue, expected wait, supported inputs, environment presets, Blender versions |
98
+ | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
94
99
 
95
- Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`; `environment_strength`,
96
- `environment_visible=False` for a flat grey backdrop with HDRI lighting), `blender` (`"5.2"`), and `assets` (stdio: local files
100
+ Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`, `compare` on previews; `environment_strength`,
101
+ `environment_visible=False` for a flat grey backdrop with HDRI lighting), `orbit` / `orbit_frames` / `orbit_elevation` (turntable),
102
+ `blender` (`"5.2"`), and `assets` (stdio: local files
97
103
  sent with the scene, found in the script through `os.environ["JR_ASSETS_DIR"]`; a `.blend`'s external files are collected
98
104
  automatically with `pip install janction-render[blend]`) or `asset_urls` (remote).
99
105
 
@@ -126,7 +132,7 @@ POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scen
126
132
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
127
133
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
128
134
  POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
129
- environment, environment_strength, environment_visible, blender}
135
+ environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
130
136
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
131
137
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
132
138
  POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
@@ -1,3 +1,3 @@
1
1
  """JANCTION Render — ターミナルの AI エージェントから GPU で 3DCG を描く。"""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
@@ -39,6 +39,53 @@ def select_artifacts(artifacts: list[dict[str, Any]], only: Optional[str] = None
39
39
  raise ValueError("only must be one of: " + ", ".join(ONLY_CHOICES))
40
40
 
41
41
 
42
+ def companion_refs(kind: str, text: str) -> list[str]:
43
+ """glTF(JSON)か OBJ の中で参照している外部ファイルの相対パス(data: URI と絶対パスは除く)。
44
+ OBJ は mtllib の .mtl だけを返す(.mtl の中の画像は mtl_refs で)。"""
45
+ import json as _json
46
+ import re as _re
47
+
48
+ out: list[str] = []
49
+
50
+ def add(ref: str) -> None:
51
+ ref = (ref or "").strip().replace("\\", "/")
52
+ if not ref or ref.startswith(("data:", "http://", "https://", "/")) or ".." in ref.split("/"):
53
+ return
54
+ if ref not in out:
55
+ out.append(ref)
56
+
57
+ if kind == "gltf":
58
+ try:
59
+ doc = _json.loads(text)
60
+ except ValueError:
61
+ return out
62
+ for key in ("buffers", "images"):
63
+ for item in doc.get(key) or []:
64
+ if isinstance(item, dict) and isinstance(item.get("uri"), str):
65
+ add(item["uri"])
66
+ return out
67
+ if kind == "obj":
68
+ for line in text.splitlines():
69
+ parts = line.strip().split(None, 1)
70
+ if len(parts) == 2 and parts[0].lower() == "mtllib":
71
+ for name in parts[1].split():
72
+ add(name)
73
+ return out
74
+ if kind == "mtl":
75
+ for line in text.splitlines():
76
+ parts = line.strip().split()
77
+ if len(parts) >= 2 and parts[0].lower() in ("map_kd", "map_ks", "map_ka", "map_ns", "map_d", "map_bump", "bump",
78
+ "disp", "decal", "norm", "map_ke", "refl"):
79
+ add(parts[-1])
80
+ return out
81
+ return out
82
+
83
+
84
+ def companion_kind(name: str) -> str:
85
+ ext = Path(name).suffix.lower()
86
+ return {".gltf": "gltf", ".obj": "obj", ".mtl": "mtl"}.get(ext, "")
87
+
88
+
42
89
  def sha256_of(path: Path) -> str:
43
90
  import hashlib
44
91
 
@@ -263,6 +310,25 @@ class Client:
263
310
  missing: list[str] = []
264
311
  if p.suffix.lower() == ".blend" and trace_assets:
265
312
  items, missing = trace_blend_assets(p)
313
+ elif companion_kind(p.name) in ("gltf", "obj") and trace_assets:
314
+ # glTF の .bin と画像、OBJ の .mtl とその画像を、ファイルの横から集める
315
+ base = p.parent
316
+ queue = companion_refs(companion_kind(p.name), p.read_text(encoding="utf-8", errors="replace"))
317
+ seen: set[str] = set()
318
+ while queue:
319
+ rel = queue.pop(0)
320
+ if rel in seen:
321
+ continue
322
+ seen.add(rel)
323
+ fp = base / rel
324
+ if not fp.is_file():
325
+ missing.append(rel)
326
+ continue
327
+ items.append((rel, fp))
328
+ if companion_kind(rel) == "mtl":
329
+ folder = Path(rel).parent.as_posix()
330
+ for sub in companion_refs("mtl", fp.read_text(encoding="utf-8", errors="replace")):
331
+ queue.append(sub if folder in ("", ".") else f"{folder}/{sub}")
266
332
  for a in assets or []:
267
333
  ap = Path(a)
268
334
  if not ap.is_file():
@@ -51,8 +51,12 @@ mcp = MCPServer(
51
51
  ),
52
52
  )
53
53
 
54
+ ORBIT_DOC = (" orbit=True: turntable; an orbit camera circles the scene once over orbit_frames frames (default 24, "
55
+ "elevation orbit_elevation deg, default 18). A preview without frames shows 0/90/180/270 deg; a final without "
56
+ "frame_end renders the whole turn as an MP4. Best for imported models (.glb/.fbx/.obj).")
54
57
  ENV_DOC = ("environment: '' keeps the scene's own world; 'studio' | 'sunset' | 'overcast' | 'night' replaces it with a "
55
- "bundled HDRI (good first render for scenes without lighting work); environment_strength scales it (1.0); "
58
+ "bundled HDRI (good first render for scenes without lighting work); 'compare' (render_preview only) renders the "
59
+ "first frame under all four presets in one 2x2 image with labels, so you can pick one; environment_strength scales it (1.0); "
56
60
  "environment_visible=False hides the HDRI from the camera (flat grey backdrop, HDRI lighting only). "
57
61
  "blender: '' (5.0) or '5.2' to render with Blender 5.2 when available. assets: local files (images, glTF "
58
62
  ".bin, ...) sent along with the scene; a .blend finds them through relative paths, a script through "
@@ -82,7 +86,7 @@ def _quota_block(exc: ClientError) -> Optional[dict[str, Any]]:
82
86
  x = exc.extra
83
87
  return {"quota_exceeded": True, "detail": exc.detail, "scope": x.get("scope"),
84
88
  "gpu_seconds_used_today": x.get("gpu_seconds_used_today"), "gpu_seconds_per_day": x.get("gpu_seconds_per_day"),
85
- "resets_at": x.get("resets_at"),
89
+ "resets_at": x.get("resets_at_iso") or x.get("resets_at"),
86
90
  "next": "tell the user today's free GPU time is used up and when it resets; a smaller job (fewer frames, "
87
91
  "lower resolution or samples) may still fit"}
88
92
 
@@ -208,6 +212,9 @@ def render_preview(
208
212
  environment_visible: bool = True,
209
213
  blender: str = "",
210
214
  assets: Optional[list[str]] = None,
215
+ orbit: bool = False,
216
+ orbit_frames: int = 24,
217
+ orbit_elevation: float = 18.0,
211
218
  ) -> list[Any]:
212
219
  """Render a fast, cheap preview of a Blender scene on a JANCTION GPU and show the image.
213
220
 
@@ -221,14 +228,15 @@ def render_preview(
221
228
  Look at the returned image, fix the scene, preview again; when it looks right, ask the user and
222
229
  call render_final. Returns scene_id (reuse it without re-uploading), job_id, saved PNG paths,
223
230
  Blender warnings (e.g. missing textures), GPU seconds, and the expiry time (24h after last use).
224
- """ + ENV_DOC
231
+ """ + ENV_DOC + ORBIT_DOC
225
232
  c = _client()
226
233
  try:
227
- frame_list = parse_frames(frames)
234
+ frame_list = parse_frames(frames) if (frames or not orbit) else None
228
235
  sid = _resolve_scene(c, scene_path, scene_id, scene_script, assets)
229
236
  j = c.submit(sid, kind="preview", frames=frame_list, camera=camera or None,
230
237
  width=width, height=height, samples=samples,
231
- **_job_params(environment, environment_strength, environment_visible, blender))
238
+ **_job_params(environment, environment_strength, environment_visible, blender),
239
+ **({"orbit": True, "orbit_frames": orbit_frames, "orbit_elevation": orbit_elevation} if orbit else {}))
232
240
  _log(f"preview job {j['job_id']} submitted frames={frame_list}")
233
241
  j = c.wait(j["job_id"], timeout=PREVIEW_WAIT_S)
234
242
  except ClientError as exc:
@@ -304,6 +312,9 @@ def render_final(
304
312
  environment_visible: bool = True,
305
313
  blender: str = "",
306
314
  assets: Optional[list[str]] = None,
315
+ orbit: bool = False,
316
+ orbit_frames: int = 24,
317
+ orbit_elevation: float = 18.0,
307
318
  ) -> str:
308
319
  """Render the final frames (or a video) of a Blender scene on JANCTION GPUs.
309
320
 
@@ -313,14 +324,19 @@ def render_final(
313
324
  (output='png'; 'auto' picks). Use the same environment / blender as the approved preview. Returns job_id and a
314
325
  time estimate right away; the render runs in the background: poll with render_status, then fetch with
315
326
  render_download. Inputs and results are deleted 24 hours after last use, so download them.
316
- """ + ENV_DOC
327
+ """ + ENV_DOC + ORBIT_DOC
317
328
  c = _client()
318
329
  try:
319
330
  sid = _resolve_scene(c, scene_path, scene_id, scene_script, assets)
331
+ extra: dict[str, Any] = {}
332
+ if orbit:
333
+ extra.update({"orbit": True, "orbit_frames": orbit_frames, "orbit_elevation": orbit_elevation})
334
+ if frame_end <= frame_start:
335
+ frame_end = None # type: ignore[assignment] # 1 周ぶんは受付が決める
320
336
  j = c.submit(sid, kind="final", frame_start=frame_start, frame_end=frame_end,
321
337
  width=width, height=height, samples=samples, fps=fps, output=output,
322
338
  camera=camera or None,
323
- **_job_params(environment, environment_strength, environment_visible, blender))
339
+ **_job_params(environment, environment_strength, environment_visible, blender), **extra)
324
340
  except ClientError as exc:
325
341
  block = _payment_block(exc) or _quota_block(exc)
326
342
  return _j(block if block else {"ok": False, "error": str(exc)})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janction-render
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Cloud GPU render farm for Blender, for AI agents: MCP server (remote + stdio) and CLI. Render Blender without a GPU from Claude, ChatGPT, Claude Code, Codex, Cursor.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -25,7 +25,12 @@ without a GPU, or when rendering locally is slow.
25
25
 
26
26
  - Input: a `.blend` file (textures travel with it), a **bpy Python script that builds the scene** (no local Blender
27
27
  needed), or a **3D file** (glTF/GLB, FBX, USD, OBJ, STL, PLY, Alembic) that is imported into an empty scene.
28
- - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render.
28
+ - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render;
29
+ `environment="compare"` previews the same frame under all four presets in one labelled image, so the agent can pick one.
30
+ - A `.gltf` or `.obj` brings its `.bin` / `.mtl` / textures along automatically (from the same folder or the same URL folder).
31
+ - `orbit=True` turns any scene or imported model into a turntable: an orbit camera circles it once (`orbit_frames`, default 24;
32
+ a preview shows 0/90/180/270 degrees, a final render without `frame_end` gives the whole turn as an MP4).
33
+ - While a job renders, `render_status` counts finished frames inside the running chunk, so the ETA updates every few seconds.
29
34
  - `scene_info` reads the scene without rendering (cameras, frame range, missing files).
30
35
  - `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
31
36
  - `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
@@ -107,10 +112,11 @@ Then, in Claude Code:
107
112
  | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?)` | PNG or MP4; returns job_id + estimate |
108
113
  | `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id, only="mp4" / "frames" / "all")` files or links; `render_cancel(job_id)` |
109
114
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
110
- | `render_info()` | workers online, queue, expected wait, supported inputs, environment presets, Blender versions |
115
+ | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
111
116
 
112
- Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`; `environment_strength`,
113
- `environment_visible=False` for a flat grey backdrop with HDRI lighting), `blender` (`"5.2"`), and `assets` (stdio: local files
117
+ Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`, `compare` on previews; `environment_strength`,
118
+ `environment_visible=False` for a flat grey backdrop with HDRI lighting), `orbit` / `orbit_frames` / `orbit_elevation` (turntable),
119
+ `blender` (`"5.2"`), and `assets` (stdio: local files
114
120
  sent with the scene, found in the script through `os.environ["JR_ASSETS_DIR"]`; a `.blend`'s external files are collected
115
121
  automatically with `pip install janction-render[blend]`) or `asset_urls` (remote).
116
122
 
@@ -143,7 +149,7 @@ POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scen
143
149
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
144
150
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
145
151
  POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
146
- environment, environment_strength, environment_visible, blender}
152
+ environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
147
153
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
148
154
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
149
155
  POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "janction-render"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Cloud GPU render farm for Blender, for AI agents: MCP server (remote + stdio) and CLI. Render Blender without a GPU from Claude, ChatGPT, Claude Code, Codex, Cursor."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes