janction-render 0.3.1__tar.gz → 0.3.3__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.1
3
+ Version: 0.3.3
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
@@ -28,6 +28,13 @@ without a GPU, or when rendering locally is slow.
28
28
  - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render;
29
29
  `environment="compare"` previews the same frame under all four presets in one labelled image, so the agent can pick one.
30
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). Flat floors and
33
+ walls are ignored when framing; `orbit_target` (object name or `x,y,z`) and `orbit_distance` (multiplier) adjust the shot.
34
+ - While the GPU is lent to another workload, tools answer right away with the queued job and a note instead of waiting.
35
+ - While a job renders, `render_status` counts finished frames inside the running chunk, so the ETA updates every few seconds.
36
+ - In hosts that support MCP Apps (Claude web and desktop, among others), previews, progress and download links also appear
37
+ as an interactive panel in the chat: the image, a progress bar, "Open MP4", and preset buttons after an `environment="compare"` preview.
31
38
  - `scene_info` reads the scene without rendering (cameras, frame range, missing files).
32
39
  - `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
33
40
  - `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
@@ -111,8 +118,9 @@ Then, in Claude Code:
111
118
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
112
119
  | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
113
120
 
114
- Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`; `environment_strength`,
115
- `environment_visible=False` for a flat grey backdrop with HDRI lighting), `blender` (`"5.2"`), and `assets` (stdio: local files
121
+ Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`, `compare` on previews; `environment_strength`,
122
+ `environment_visible=False` for a flat grey backdrop with HDRI lighting), `orbit` / `orbit_frames` / `orbit_elevation` (turntable),
123
+ `blender` (`"5.2"`), and `assets` (stdio: local files
116
124
  sent with the scene, found in the script through `os.environ["JR_ASSETS_DIR"]`; a `.blend`'s external files are collected
117
125
  automatically with `pip install janction-render[blend]`) or `asset_urls` (remote).
118
126
 
@@ -145,7 +153,7 @@ POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scen
145
153
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
146
154
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
147
155
  POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
148
- environment, environment_strength, environment_visible, blender}
156
+ environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
149
157
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
150
158
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
151
159
  POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
@@ -11,6 +11,13 @@ without a GPU, or when rendering locally is slow.
11
11
  - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render;
12
12
  `environment="compare"` previews the same frame under all four presets in one labelled image, so the agent can pick one.
13
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). Flat floors and
16
+ walls are ignored when framing; `orbit_target` (object name or `x,y,z`) and `orbit_distance` (multiplier) adjust the shot.
17
+ - While the GPU is lent to another workload, tools answer right away with the queued job and a note instead of waiting.
18
+ - While a job renders, `render_status` counts finished frames inside the running chunk, so the ETA updates every few seconds.
19
+ - In hosts that support MCP Apps (Claude web and desktop, among others), previews, progress and download links also appear
20
+ as an interactive panel in the chat: the image, a progress bar, "Open MP4", and preset buttons after an `environment="compare"` preview.
14
21
  - `scene_info` reads the scene without rendering (cameras, frame range, missing files).
15
22
  - `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
16
23
  - `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
@@ -94,8 +101,9 @@ Then, in Claude Code:
94
101
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
95
102
  | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
96
103
 
97
- Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`; `environment_strength`,
98
- `environment_visible=False` for a flat grey backdrop with HDRI lighting), `blender` (`"5.2"`), and `assets` (stdio: local files
104
+ Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`, `compare` on previews; `environment_strength`,
105
+ `environment_visible=False` for a flat grey backdrop with HDRI lighting), `orbit` / `orbit_frames` / `orbit_elevation` (turntable),
106
+ `blender` (`"5.2"`), and `assets` (stdio: local files
99
107
  sent with the scene, found in the script through `os.environ["JR_ASSETS_DIR"]`; a `.blend`'s external files are collected
100
108
  automatically with `pip install janction-render[blend]`) or `asset_urls` (remote).
101
109
 
@@ -128,7 +136,7 @@ POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scen
128
136
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
129
137
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
130
138
  POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
131
- environment, environment_strength, environment_visible, blender}
139
+ environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
132
140
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
133
141
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
134
142
  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.1"
3
+ __version__ = "0.3.3"
@@ -51,6 +51,10 @@ 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). Flat floors/walls are ignored when framing; orbit_target = an "
56
+ "object name or 'x,y,z' centres on that, orbit_distance scales the distance (1.0). A preview without frames "
57
+ "shows 0/90/180/270 deg; a final without frame_end renders the whole turn as an MP4. Best for imported models.")
54
58
  ENV_DOC = ("environment: '' keeps the scene's own world; 'studio' | 'sunset' | 'overcast' | 'night' replaces it with a "
55
59
  "bundled HDRI (good first render for scenes without lighting work); 'compare' (render_preview only) renders the "
56
60
  "first frame under all four presets in one 2x2 image with labels, so you can pick one; environment_strength scales it (1.0); "
@@ -72,6 +76,27 @@ def _out_root() -> Path:
72
76
  return Path(os.environ.get("JANCTION_RENDER_OUT") or (Path.home() / "janction-render"))
73
77
 
74
78
 
79
+ def _wait_unless_gated(c: Client, job: dict[str, Any], timeout: float) -> dict[str, Any]:
80
+ """仕事を待つ。ただし仕事を取れるワーカーが無い(GPU が貸し出し中)なら待たずに返し、hint で伝える。"""
81
+ if job["status"] in ("done", "failed", "canceled"):
82
+ return job
83
+ try:
84
+ q = c.health().get("queue") or {}
85
+ if int(q.get("workers_available") or 0) == 0 and job["status"] == "queued":
86
+ job["gated"] = True
87
+ return job
88
+ except Exception: # noqa: BLE001
89
+ pass
90
+ return c.wait(job["job_id"], timeout=timeout)
91
+
92
+
93
+ def _gated_hint(j: dict[str, Any], again: str) -> str:
94
+ if j.get("gated"):
95
+ return (f"the GPU is lent to another workload right now; the job stays queued and starts when the GPU returns. "
96
+ f"Tell the user and offer to check later with {again}; do not poll continuously")
97
+ return ""
98
+
99
+
75
100
  def _j(obj: Any) -> str:
76
101
  return json.dumps(obj, ensure_ascii=False, indent=2)
77
102
 
@@ -83,7 +108,7 @@ def _quota_block(exc: ClientError) -> Optional[dict[str, Any]]:
83
108
  x = exc.extra
84
109
  return {"quota_exceeded": True, "detail": exc.detail, "scope": x.get("scope"),
85
110
  "gpu_seconds_used_today": x.get("gpu_seconds_used_today"), "gpu_seconds_per_day": x.get("gpu_seconds_per_day"),
86
- "resets_at": x.get("resets_at"),
111
+ "resets_at": x.get("resets_at_iso") or x.get("resets_at"),
87
112
  "next": "tell the user today's free GPU time is used up and when it resets; a smaller job (fewer frames, "
88
113
  "lower resolution or samples) may still fit"}
89
114
 
@@ -171,12 +196,12 @@ def scene_info(scene_path: str = "", scene_id: str = "", scene_script: str = "",
171
196
  sid = _resolve_scene(c, scene_path, scene_id, scene_script, assets)
172
197
  j = c.submit(sid, kind="info", **_job_params("", 1.0, True, blender))
173
198
  if j["status"] != "done":
174
- j = c.wait(j["job_id"], timeout=INFO_WAIT_S)
199
+ j = _wait_unless_gated(c, j, INFO_WAIT_S)
175
200
  except (ClientError, ValueError, FileNotFoundError) as exc:
176
201
  return _j({"ok": False, "error": str(exc)})
177
202
  if j["status"] != "done":
178
203
  b = _brief(j)
179
- b["hint"] = "still queued (a GPU worker may be busy); call scene_info again in a moment" \
204
+ b["hint"] = (_gated_hint(j, "scene_info(scene_id)") or "still queued (a GPU worker may be busy); call scene_info again in a moment") \
180
205
  if j["status"] in ("queued", "running") else "fix the scene and try again"
181
206
  return _j(b)
182
207
  info = dict(j.get("info") or {})
@@ -209,6 +234,11 @@ def render_preview(
209
234
  environment_visible: bool = True,
210
235
  blender: str = "",
211
236
  assets: Optional[list[str]] = None,
237
+ orbit: bool = False,
238
+ orbit_frames: int = 24,
239
+ orbit_elevation: float = 18.0,
240
+ orbit_target: str = "",
241
+ orbit_distance: float = 1.0,
212
242
  ) -> list[Any]:
213
243
  """Render a fast, cheap preview of a Blender scene on a JANCTION GPU and show the image.
214
244
 
@@ -222,16 +252,18 @@ def render_preview(
222
252
  Look at the returned image, fix the scene, preview again; when it looks right, ask the user and
223
253
  call render_final. Returns scene_id (reuse it without re-uploading), job_id, saved PNG paths,
224
254
  Blender warnings (e.g. missing textures), GPU seconds, and the expiry time (24h after last use).
225
- """ + ENV_DOC
255
+ """ + ENV_DOC + ORBIT_DOC
226
256
  c = _client()
227
257
  try:
228
- frame_list = parse_frames(frames)
258
+ frame_list = parse_frames(frames) if (frames or not orbit) else None
229
259
  sid = _resolve_scene(c, scene_path, scene_id, scene_script, assets)
230
260
  j = c.submit(sid, kind="preview", frames=frame_list, camera=camera or None,
231
261
  width=width, height=height, samples=samples,
232
- **_job_params(environment, environment_strength, environment_visible, blender))
262
+ **_job_params(environment, environment_strength, environment_visible, blender),
263
+ **({"orbit": True, "orbit_frames": orbit_frames, "orbit_elevation": orbit_elevation,
264
+ "orbit_target": orbit_target, "orbit_distance": orbit_distance} if orbit else {}))
233
265
  _log(f"preview job {j['job_id']} submitted frames={frame_list}")
234
- j = c.wait(j["job_id"], timeout=PREVIEW_WAIT_S)
266
+ j = _wait_unless_gated(c, j, PREVIEW_WAIT_S)
235
267
  except ClientError as exc:
236
268
  block = _payment_block(exc) or _quota_block(exc)
237
269
  return [_j(block if block else {"ok": False, "error": str(exc)})]
@@ -239,7 +271,7 @@ def render_preview(
239
271
  return [_j({"ok": False, "error": str(exc)})]
240
272
  brief = _brief(j)
241
273
  if j["status"] != "done":
242
- brief["hint"] = ("still running: call render_status(job_id) and then render_download"
274
+ brief["hint"] = ((_gated_hint(j, "render_status(job_id)") or "still running: call render_status(job_id) and then render_download")
243
275
  if j["status"] in ("queued", "running")
244
276
  else "fix the scene and try again")
245
277
  return [_j(brief)]
@@ -305,6 +337,11 @@ def render_final(
305
337
  environment_visible: bool = True,
306
338
  blender: str = "",
307
339
  assets: Optional[list[str]] = None,
340
+ orbit: bool = False,
341
+ orbit_frames: int = 24,
342
+ orbit_elevation: float = 18.0,
343
+ orbit_target: str = "",
344
+ orbit_distance: float = 1.0,
308
345
  ) -> str:
309
346
  """Render the final frames (or a video) of a Blender scene on JANCTION GPUs.
310
347
 
@@ -314,14 +351,20 @@ def render_final(
314
351
  (output='png'; 'auto' picks). Use the same environment / blender as the approved preview. Returns job_id and a
315
352
  time estimate right away; the render runs in the background: poll with render_status, then fetch with
316
353
  render_download. Inputs and results are deleted 24 hours after last use, so download them.
317
- """ + ENV_DOC
354
+ """ + ENV_DOC + ORBIT_DOC
318
355
  c = _client()
319
356
  try:
320
357
  sid = _resolve_scene(c, scene_path, scene_id, scene_script, assets)
358
+ extra: dict[str, Any] = {}
359
+ if orbit:
360
+ extra.update({"orbit": True, "orbit_frames": orbit_frames, "orbit_elevation": orbit_elevation,
361
+ "orbit_target": orbit_target, "orbit_distance": orbit_distance})
362
+ if frame_end <= frame_start:
363
+ frame_end = None # type: ignore[assignment] # 1 周ぶんは受付が決める
321
364
  j = c.submit(sid, kind="final", frame_start=frame_start, frame_end=frame_end,
322
365
  width=width, height=height, samples=samples, fps=fps, output=output,
323
366
  camera=camera or None,
324
- **_job_params(environment, environment_strength, environment_visible, blender))
367
+ **_job_params(environment, environment_strength, environment_visible, blender), **extra)
325
368
  except ClientError as exc:
326
369
  block = _payment_block(exc) or _quota_block(exc)
327
370
  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.1
3
+ Version: 0.3.3
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
@@ -28,6 +28,13 @@ without a GPU, or when rendering locally is slow.
28
28
  - `environment="studio" | "sunset" | "overcast" | "night"` lights a scene with a bundled HDRI (CC0) for a good first render;
29
29
  `environment="compare"` previews the same frame under all four presets in one labelled image, so the agent can pick one.
30
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). Flat floors and
33
+ walls are ignored when framing; `orbit_target` (object name or `x,y,z`) and `orbit_distance` (multiplier) adjust the shot.
34
+ - While the GPU is lent to another workload, tools answer right away with the queued job and a note instead of waiting.
35
+ - While a job renders, `render_status` counts finished frames inside the running chunk, so the ETA updates every few seconds.
36
+ - In hosts that support MCP Apps (Claude web and desktop, among others), previews, progress and download links also appear
37
+ as an interactive panel in the chat: the image, a progress bar, "Open MP4", and preset buttons after an `environment="compare"` preview.
31
38
  - `scene_info` reads the scene without rendering (cameras, frame range, missing files).
32
39
  - `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
33
40
  - `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
@@ -111,8 +118,9 @@ Then, in Claude Code:
111
118
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
112
119
  | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
113
120
 
114
- Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`; `environment_strength`,
115
- `environment_visible=False` for a flat grey backdrop with HDRI lighting), `blender` (`"5.2"`), and `assets` (stdio: local files
121
+ Options on `render_preview` / `render_final`: `environment` (`studio`, `sunset`, `overcast`, `night`, `compare` on previews; `environment_strength`,
122
+ `environment_visible=False` for a flat grey backdrop with HDRI lighting), `orbit` / `orbit_frames` / `orbit_elevation` (turntable),
123
+ `blender` (`"5.2"`), and `assets` (stdio: local files
116
124
  sent with the scene, found in the script through `os.environ["JR_ASSETS_DIR"]`; a `.blend`'s external files are collected
117
125
  automatically with `pip install janction-render[blend]`) or `asset_urls` (remote).
118
126
 
@@ -145,7 +153,7 @@ POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scen
145
153
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
146
154
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
147
155
  POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
148
- environment, environment_strength, environment_visible, blender}
156
+ environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
149
157
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
150
158
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
151
159
  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.1"
7
+ version = "0.3.3"
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