janction-render 0.3.4__tar.gz → 0.4.0__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.4
3
+ Version: 0.4.0
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
@@ -114,7 +114,7 @@ Then, in Claude Code:
114
114
  | `scene_info(scene_script | scene_url | scene_path | scene_id, assets?)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
115
115
  | `render_preview(..., frames="1-24", environment?, blender?)` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
116
116
  | `render_estimate(scene_id, frame_start, frame_end, width, height, samples)` | GPU seconds, queue wait, "about N minutes", fits today's free quota? No GPU time used |
117
- | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?)` | PNG or MP4; returns job_id + estimate |
117
+ | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?, engine?)` | frames (png / exr) or video (mp4 / webm / prores); returns job_id + estimate |
118
118
  | `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id, only="mp4" / "frames" / "all")` files or links; `render_cancel(job_id)` |
119
119
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
120
120
  | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
@@ -143,7 +143,7 @@ sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
143
143
  scene.collection.objects.link(sun)
144
144
  ```
145
145
 
146
- The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
146
+ The service sets the engine (Cycles by default; `engine="eevee"` for cheaper animations: about half the per-frame cost after ~7 s of shader compilation per job), resolution, samples and denoising; your script sets the scene, camera and frame range. In Blender 5.0 materials always use nodes: set the Principled BSDF Base Color, not `diffuse_color`.
147
147
  Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
148
148
 
149
149
  ## HTTP API
@@ -153,7 +153,7 @@ POST /v1/keys -> {api_key} (header X-A
153
153
  POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scene_id}
154
154
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
155
155
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
156
- POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
156
+ POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output (png|exr|mp4|webm|prores), engine (cycles|eevee),
157
157
  environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
158
158
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
159
159
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
@@ -97,7 +97,7 @@ Then, in Claude Code:
97
97
  | `scene_info(scene_script | scene_url | scene_path | scene_id, assets?)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
98
98
  | `render_preview(..., frames="1-24", environment?, blender?)` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
99
99
  | `render_estimate(scene_id, frame_start, frame_end, width, height, samples)` | GPU seconds, queue wait, "about N minutes", fits today's free quota? No GPU time used |
100
- | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?)` | PNG or MP4; returns job_id + estimate |
100
+ | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?, engine?)` | frames (png / exr) or video (mp4 / webm / prores); returns job_id + estimate |
101
101
  | `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id, only="mp4" / "frames" / "all")` files or links; `render_cancel(job_id)` |
102
102
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
103
103
  | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
@@ -126,7 +126,7 @@ sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
126
126
  scene.collection.objects.link(sun)
127
127
  ```
128
128
 
129
- The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
129
+ The service sets the engine (Cycles by default; `engine="eevee"` for cheaper animations: about half the per-frame cost after ~7 s of shader compilation per job), resolution, samples and denoising; your script sets the scene, camera and frame range. In Blender 5.0 materials always use nodes: set the Principled BSDF Base Color, not `diffuse_color`.
130
130
  Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
131
131
 
132
132
  ## HTTP API
@@ -136,7 +136,7 @@ POST /v1/keys -> {api_key} (header X-A
136
136
  POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scene_id}
137
137
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
138
138
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
139
- POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
139
+ POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output (png|exr|mp4|webm|prores), engine (cycles|eevee),
140
140
  environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
141
141
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
142
142
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
@@ -1,3 +1,3 @@
1
1
  """JANCTION Render — ターミナルの AI エージェントから GPU で 3DCG を描く。"""
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.4.0"
@@ -79,7 +79,7 @@ def cmd_preview(c: Client, args: argparse.Namespace) -> int:
79
79
  w, h = args.size
80
80
  frames = parse_frames(args.frames, default=args.frame)
81
81
  j = c.submit(sid, kind="preview", frames=frames, camera=args.camera, width=w, height=h,
82
- samples=args.samples)
82
+ samples=args.samples, engine=args.engine)
83
83
  print(f"job {j['job_id']} queued: frames {frames} (estimate ~{j['estimate']['seconds']}s)", file=sys.stderr)
84
84
  j = c.wait(j["job_id"], timeout=args.timeout, on_progress=_progress)
85
85
  if j["status"] != "done":
@@ -98,7 +98,7 @@ def cmd_render(c: Client, args: argparse.Namespace) -> int:
98
98
  w, h = args.size
99
99
  fs, fe = args.frames
100
100
  j = c.submit(sid, kind="final", frame_start=fs, frame_end=fe, width=w, height=h,
101
- samples=args.samples, fps=args.fps, output=args.output, camera=args.camera)
101
+ samples=args.samples, fps=args.fps, output=args.output, camera=args.camera, engine=args.engine)
102
102
  print(f"job {j['job_id']} queued: {fe - fs + 1} frames in {j['progress']['chunks_total']} chunks, "
103
103
  f"estimate ~{j['estimate']['seconds']}s", file=sys.stderr)
104
104
  if not args.wait:
@@ -187,6 +187,7 @@ def build_parser() -> argparse.ArgumentParser:
187
187
  s.add_argument("--camera", default=None)
188
188
  s.add_argument("--size", type=_size, default=(1280, 720))
189
189
  s.add_argument("--samples", type=int, default=16)
190
+ s.add_argument("--engine", choices=["cycles", "eevee"], default=None, help="cycles (default) or eevee (cheaper drafts)")
190
191
  s.add_argument("--out", default=None)
191
192
  s.add_argument("--timeout", type=float, default=300)
192
193
  s.set_defaults(fn=cmd_preview)
@@ -197,8 +198,10 @@ def build_parser() -> argparse.ArgumentParser:
197
198
  s.add_argument("--frames", type=_frames, required=True, help="e.g. 1-240 or 12")
198
199
  s.add_argument("--size", type=_size, default=(1920, 1080))
199
200
  s.add_argument("--samples", type=int, default=128)
201
+ s.add_argument("--engine", choices=["cycles", "eevee"], default=None, help="cycles (default) or eevee (cheaper drafts)")
200
202
  s.add_argument("--fps", type=int, default=24)
201
- s.add_argument("--output", choices=["auto", "png", "mp4"], default="auto")
203
+ s.add_argument("--output", choices=["auto", "png", "exr", "mp4", "webm", "prores"], default="auto",
204
+ help="png/exr = frames, mp4/webm/prores = video (auto: mp4 for a range, png for one frame)")
202
205
  s.add_argument("--camera", default=None)
203
206
  s.add_argument("--out", default=None)
204
207
  s.add_argument("--wait", action="store_true", help="wait until done and download")
@@ -19,7 +19,7 @@ CACHE = Path.home() / ".janction-render.json"
19
19
 
20
20
  # 送れるシーン: .blend、bpy スクリプト、こちらの Blender が読み込める 3D ファイル
21
21
  SCENE_SUFFIXES = (".blend", ".py", ".fbx", ".glb", ".gltf", ".obj", ".stl", ".ply", ".usd", ".usda", ".usdc", ".usdz", ".abc")
22
- ONLY_CHOICES = ("all", "mp4", "frames", "sheet", "output")
22
+ ONLY_CHOICES = ("all", "mp4", "frames", "sheet", "output", "video")
23
23
 
24
24
 
25
25
  def select_artifacts(artifacts: list[dict[str, Any]], only: Optional[str] = None) -> list[dict[str, Any]]:
@@ -27,14 +27,14 @@ def select_artifacts(artifacts: list[dict[str, Any]], only: Optional[str] = None
27
27
  o = (only or "all").strip().lower()
28
28
  if o in ("", "all"):
29
29
  return list(artifacts)
30
- if o == "mp4":
31
- return [a for a in artifacts if a["name"].endswith(".mp4")]
30
+ if o in ("mp4", "video"):
31
+ return [a for a in artifacts if a["name"].endswith((".mp4", ".webm", ".mov"))]
32
32
  if o == "frames":
33
33
  return [a for a in artifacts if a["name"].startswith("frame_")]
34
34
  if o == "sheet":
35
35
  return [a for a in artifacts if a["name"] == "sheet.png"]
36
36
  if o == "output":
37
- picked = [a for a in artifacts if a["name"] in ("output.mp4", "sheet.png")]
37
+ picked = [a for a in artifacts if a["name"] in ("output.mp4", "output.webm", "output.mov", "sheet.png")]
38
38
  return picked or [a for a in artifacts if a["name"].startswith("frame_")][:1]
39
39
  raise ValueError("only must be one of: " + ", ".join(ONLY_CHOICES))
40
40
 
@@ -90,7 +90,7 @@ function render() {
90
90
  if (state.image) html += `<img class="sheet" src="${state.image}" alt="render preview">`;
91
91
  const out = job.output_url;
92
92
  const links = job.links || {};
93
- if (status === "done" && out && /\.mp4(\?|$)/.test(out)) {
93
+ if (status === "done" && out && /\.(mp4|webm|mov)(\?|$)/.test(out)) {
94
94
  html += `<video controls playsinline src="${esc(out)}"></video>`;
95
95
  }
96
96
  if (status === "done" && isCompare(job)) {
@@ -99,7 +99,7 @@ function render() {
99
99
  }
100
100
  html += `<div class="row">`;
101
101
  if (status === "queued" || status === "running") html += `<button data-act="refresh">Refresh</button>`;
102
- if (status === "done" && out) html += `<button class="primary" data-open="${esc(out)}">Open ${/\.mp4/.test(out) ? "MP4" : "image"}</button>`;
102
+ if (status === "done" && out) html += `<button class="primary" data-open="${esc(out)}">Open ${/\.(mp4|webm|mov)/.test(out) ? "video" : "image"}</button>`;
103
103
  const frameLinks = Object.entries(links).filter(([n]) => n.startsWith("frame_"));
104
104
  if (status === "done" && frameLinks.length) html += `<button data-open="${esc(frameLinks[0][1])}">Open first frame</button>`;
105
105
  if (status === "done" && job.kind === "preview") html += `<button data-say="This preview looks right. Please ask me about frames, resolution and samples, then render the final.">Looks good, go final</button><button data-say="Please adjust the scene and preview again.">Needs changes</button>`;
@@ -41,7 +41,8 @@ mcp = MCPServer(
41
41
  "in one image; look at it, fix the scene, repeat) -> ask the user 'is this OK?' -> render_estimate (tell the "
42
42
  "user how long it takes and whether it fits today's free quota) -> render_final (frames or MP4) -> "
43
43
  "render_status / render_download. Always tell the user the time estimate (estimate.human / eta.human). "
44
- "Blender 5.0 with Cycles on GPU (blender='5.2' selects Blender 5.2 when the worker has it). Inputs: a .blend, a bpy "
44
+ "Blender 5.0 with Cycles on GPU (blender='5.2' selects Blender 5.2 when the worker has it); engine='eevee' switches to EEVEE "
45
+ "(about half the per-frame cost on animations of a few dozen frames or more; each job pays ~7 s of shader compilation, so previews stay on Cycles); output='png'|'exr' returns frames, 'mp4'|'webm'|'prores' a video. Blender 5.0 API notes for scripts: materials always use nodes, so set a color on the Principled BSDF (mat.node_tree.nodes['Principled BSDF'].inputs['Base Color'].default_value = (r, g, b, 1)); mat.diffuse_color only affects the viewport. Animate with obj.keyframe_insert(); obj.animation_data.action.fcurves no longer exists (set bpy.context.preferences.edit.keyframe_new_interpolation_type='LINEAR' before inserting keyframes for constant-speed motion); do not set Material.use_nodes; leave resolution, samples and denoising to the service. Inputs: a .blend, a bpy "
45
46
  "script, or a 3D file (FBX, glTF/GLB, USD, OBJ, STL, PLY, Alembic) that is imported into an empty scene; images "
46
47
  "and other assets can be sent with the scene (assets=[...]). environment='studio'|'sunset'|'overcast'|'night' "
47
48
  "lights the scene with a bundled HDRI for a good-looking first render. Inputs and results are deleted 24 hours "
@@ -172,8 +173,11 @@ def _resolve_scene(c: Client, scene_path: str, scene_id: str, scene_script: str
172
173
  return up["scene_id"]
173
174
 
174
175
 
175
- def _job_params(environment: str, environment_strength: float, environment_visible: bool, blender: str) -> dict[str, Any]:
176
+ def _job_params(environment: str, environment_strength: float, environment_visible: bool, blender: str,
177
+ engine: str = "") -> dict[str, Any]:
176
178
  out: dict[str, Any] = {}
179
+ if engine:
180
+ out["engine"] = engine
177
181
  if environment:
178
182
  out.update({"environment": environment, "environment_strength": environment_strength,
179
183
  "environment_visible": environment_visible})
@@ -256,6 +260,7 @@ def render_preview(
256
260
  orbit_elevation: float = 18.0,
257
261
  orbit_target: str = "",
258
262
  orbit_distance: float = 1.0,
263
+ engine: str = "",
259
264
  ) -> list[Any]:
260
265
  """Render a fast, cheap preview of a Blender scene on a JANCTION GPU and show the image.
261
266
 
@@ -276,7 +281,7 @@ def render_preview(
276
281
  sid = _resolve_scene(c, scene_path, scene_id, scene_script, assets)
277
282
  j = c.submit(sid, kind="preview", frames=frame_list, camera=camera or None,
278
283
  width=width, height=height, samples=samples,
279
- **_job_params(environment, environment_strength, environment_visible, blender),
284
+ **_job_params(environment, environment_strength, environment_visible, blender, engine=engine),
280
285
  **({"orbit": True, "orbit_frames": orbit_frames, "orbit_elevation": orbit_elevation,
281
286
  "orbit_target": orbit_target, "orbit_distance": orbit_distance} if orbit else {}))
282
287
  _log(f"preview job {j['job_id']} submitted frames={frame_list}")
@@ -359,6 +364,7 @@ def render_final(
359
364
  orbit_elevation: float = 18.0,
360
365
  orbit_target: str = "",
361
366
  orbit_distance: float = 1.0,
367
+ engine: str = "",
362
368
  ) -> str:
363
369
  """Render the final frames (or a video) of a Blender scene on JANCTION GPUs.
364
370
 
@@ -381,7 +387,7 @@ def render_final(
381
387
  j = c.submit(sid, kind="final", frame_start=frame_start, frame_end=frame_end,
382
388
  width=width, height=height, samples=samples, fps=fps, output=output,
383
389
  camera=camera or None,
384
- **_job_params(environment, environment_strength, environment_visible, blender), **extra)
390
+ **_job_params(environment, environment_strength, environment_visible, blender, engine=engine), **extra)
385
391
  except ClientError as exc:
386
392
  block = _payment_block(exc) or _quota_block(exc)
387
393
  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.4
3
+ Version: 0.4.0
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
@@ -114,7 +114,7 @@ Then, in Claude Code:
114
114
  | `scene_info(scene_script | scene_url | scene_path | scene_id, assets?)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
115
115
  | `render_preview(..., frames="1-24", environment?, blender?)` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
116
116
  | `render_estimate(scene_id, frame_start, frame_end, width, height, samples)` | GPU seconds, queue wait, "about N minutes", fits today's free quota? No GPU time used |
117
- | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?)` | PNG or MP4; returns job_id + estimate |
117
+ | `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output, environment?, blender?, engine?)` | frames (png / exr) or video (mp4 / webm / prores); returns job_id + estimate |
118
118
  | `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id, only="mp4" / "frames" / "all")` files or links; `render_cancel(job_id)` |
119
119
  | `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
120
120
  | `render_info()` | workers online or gated (GPU lent to another workload), queue, expected wait, supported inputs, environment presets, Blender versions |
@@ -143,7 +143,7 @@ sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
143
143
  scene.collection.objects.link(sun)
144
144
  ```
145
145
 
146
- The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
146
+ The service sets the engine (Cycles by default; `engine="eevee"` for cheaper animations: about half the per-frame cost after ~7 s of shader compilation per job), resolution, samples and denoising; your script sets the scene, camera and frame range. In Blender 5.0 materials always use nodes: set the Principled BSDF Base Color, not `diffuse_color`.
147
147
  Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
148
148
 
149
149
  ## HTTP API
@@ -153,7 +153,7 @@ POST /v1/keys -> {api_key} (header X-A
153
153
  POST /v1/files multipart "file" (.blend|.py|.glb|.fbx|.usd|.obj|...) -> {scene_id}
154
154
  POST /v1/files/{id}/assets multipart "files" textures, glTF .bin ... GET /v1/files/lookup?sha256= reuse an upload
155
155
  POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
156
- POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output,
156
+ POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output (png|exr|mp4|webm|prores), engine (cycles|eevee),
157
157
  environment, environment_strength, environment_visible, blender, orbit, orbit_frames, orbit_elevation}
158
158
  GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
159
159
  GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "janction-render"
7
- version = "0.3.4"
7
+ version = "0.4.0"
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