janction-render 0.1.0__tar.gz → 0.2.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.
- janction_render-0.2.0/PKG-INFO +153 -0
- janction_render-0.2.0/README.md +138 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render/__init__.py +1 -1
- janction_render-0.2.0/janction_render/brief.py +55 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render/client.py +7 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render/mcp_server.py +52 -42
- janction_render-0.2.0/janction_render.egg-info/PKG-INFO +153 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render.egg-info/SOURCES.txt +1 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/pyproject.toml +2 -2
- janction_render-0.1.0/PKG-INFO +0 -118
- janction_render-0.1.0/README.md +0 -103
- janction_render-0.1.0/janction_render.egg-info/PKG-INFO +0 -118
- {janction_render-0.1.0 → janction_render-0.2.0}/LICENSE +0 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render/cli.py +0 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render.egg-info/dependency_links.txt +0 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render.egg-info/entry_points.txt +0 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render.egg-info/requires.txt +0 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/janction_render.egg-info/top_level.txt +0 -0
- {janction_render-0.1.0 → janction_render-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: janction-render
|
|
3
|
+
Version: 0.2.0
|
|
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
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: requests>=2.31
|
|
9
|
+
Requires-Dist: mcp>=1.10
|
|
10
|
+
Provides-Extra: mcp
|
|
11
|
+
Requires-Dist: mcp>=1.10; extra == "mcp"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# JANCTION Render
|
|
17
|
+
|
|
18
|
+
A cloud GPU render farm for Blender, built for AI agents. Render Blender scenes on JANCTION GPUs from **Claude, ChatGPT,
|
|
19
|
+
Claude Code, Codex, Cursor or any MCP client**: an MCP server (remote and stdio) plus a CLI. Use it to render Blender
|
|
20
|
+
without a GPU, or when rendering locally is slow.
|
|
21
|
+
|
|
22
|
+
- Input: a `.blend` file, or a **bpy Python script that builds the scene** (no local Blender needed).
|
|
23
|
+
- `scene_info` reads the scene without rendering (cameras, frame range, missing files).
|
|
24
|
+
- `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
|
|
25
|
+
- `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
|
|
26
|
+
- `render_final` renders the frames on GPUs and joins them into an MP4; `render_status` reports the remaining time.
|
|
27
|
+
- Free beta: no charges. Each key gets 10 GPU-minutes per day; a final render is up to 240 frames at 1080p.
|
|
28
|
+
Paid plans (per GPU second, prepaid credit) will be announced on the service page before they start.
|
|
29
|
+
- Inputs and results are deleted 24 hours after last use and are never used for training.
|
|
30
|
+
|
|
31
|
+
## Status
|
|
32
|
+
|
|
33
|
+
Free beta. Service page: `https://render.janction.jp` (`/v1/health`, `/llms.txt`). Blender 5.0, Cycles on GPU.
|
|
34
|
+
|
|
35
|
+
## Connect (remote MCP, nothing to install)
|
|
36
|
+
|
|
37
|
+
MCP server URL: **`https://render.janction.jp/mcp`** (Streamable HTTP). Auth is OAuth 2.1 with dynamic client
|
|
38
|
+
registration: pressing "Connect" opens a page that creates a free API key (or takes one you already have). A raw API key
|
|
39
|
+
also works as `Authorization: Bearer jr_...`.
|
|
40
|
+
|
|
41
|
+
| client | how |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Claude.ai (web, desktop, mobile) | Settings → Connectors → Add custom connector → paste the URL → Connect |
|
|
44
|
+
| ChatGPT | Settings → Connectors → Advanced → Developer mode → Create → paste the URL (OAuth) |
|
|
45
|
+
| Claude Code | `claude mcp add --transport http janction-render https://render.janction.jp/mcp`, then `/mcp` to authenticate |
|
|
46
|
+
| Cursor, Windsurf, other MCP clients | Streamable HTTP at the URL above (OAuth, or a Bearer API key header) |
|
|
47
|
+
|
|
48
|
+
Remote tools take `scene_script` (bpy code as text), `scene_url` (an https link to a `.blend` or `.py`) or `scene_id`;
|
|
49
|
+
results come back as an inline image plus download links that need no key and work for about 24 hours.
|
|
50
|
+
|
|
51
|
+
Claude Code plugin (the remote connector plus a skill with the workflow):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
/plugin marketplace add KJdayo/janction-render
|
|
55
|
+
/plugin install janction-render@janction-render
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Install (stdio MCP, sends local files)
|
|
59
|
+
|
|
60
|
+
The package is on PyPI as `janction-render`.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
# Claude Code (uvx runs it without a global install)
|
|
64
|
+
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- uvx --from janction-render janction-render-mcp
|
|
65
|
+
|
|
66
|
+
# or with pip / pipx
|
|
67
|
+
pip install janction-render
|
|
68
|
+
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- janction-render-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Codex: add to `~/.codex/config.toml`
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[mcp_servers.janction-render]
|
|
75
|
+
command = "uvx"
|
|
76
|
+
args = ["--from", "janction-render", "janction-render-mcp"]
|
|
77
|
+
env = { JANCTION_RENDER_SERVER = "https://render.janction.jp" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A temporary API key is issued automatically on first use and cached in `~/.janction-render.json`; set
|
|
81
|
+
`JANCTION_RENDER_API_KEY` to pin one (quota and, later, credit belong to the key). The same key can be used from the
|
|
82
|
+
remote connector: paste it on the connect page.
|
|
83
|
+
|
|
84
|
+
Then, in Claude Code:
|
|
85
|
+
|
|
86
|
+
> Build a small street scene in Blender with a camera fly-through and render a preview with janction-render.
|
|
87
|
+
|
|
88
|
+
## Tools
|
|
89
|
+
|
|
90
|
+
| tool | what |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `scene_info(scene_script | scene_url | scene_path | scene_id)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
|
|
93
|
+
| `render_preview(..., frames="1-24")` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
|
|
94
|
+
| `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 |
|
|
95
|
+
| `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output)` | PNG or MP4; returns job_id + estimate |
|
|
96
|
+
| `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id)` files or links; `render_cancel(job_id)` |
|
|
97
|
+
| `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
|
|
98
|
+
| `render_info()` | workers online, queue and expected wait |
|
|
99
|
+
|
|
100
|
+
CLI: `janction-render inspect|preview|render|status|download|cancel|jobs|balance|topup|info`.
|
|
101
|
+
|
|
102
|
+
## Writing a scene script
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import bpy, math
|
|
106
|
+
for ob in list(bpy.data.objects):
|
|
107
|
+
bpy.data.objects.remove(ob, do_unlink=True)
|
|
108
|
+
scene = bpy.context.scene
|
|
109
|
+
scene.frame_start, scene.frame_end = 1, 24
|
|
110
|
+
bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1))
|
|
111
|
+
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
|
112
|
+
scene.collection.objects.link(cam); scene.camera = cam
|
|
113
|
+
cam.location = (6, -6, 4); cam.rotation_euler = (math.radians(60), 0, math.radians(45))
|
|
114
|
+
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
|
115
|
+
scene.collection.objects.link(sun)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
|
|
119
|
+
Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
|
|
120
|
+
|
|
121
|
+
## HTTP API
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
POST /v1/keys -> {api_key} (header X-API-Key afterwards)
|
|
125
|
+
POST /v1/files multipart "file" (.blend|.py) -> {scene_id}
|
|
126
|
+
POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
|
|
127
|
+
POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output}
|
|
128
|
+
GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
|
|
129
|
+
GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
|
|
130
|
+
POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
|
|
131
|
+
GET /.well-known/oauth-protected-resource/mcp OAuth discovery
|
|
132
|
+
POST /v1/billing/checkout {amount_yen} -> {checkout_url} POST /v1/billing/sync GET /v1/ledger GET /v1/me
|
|
133
|
+
GET /llms.txt /terms /privacy /legal /security
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
During the free beta a `429 quota_exceeded` response carries `resets_at`; a `400 beta_limit` means the job is too big
|
|
137
|
+
(split it). Once paid plans start, a `402 payment_required` response carries `checkout_url`.
|
|
138
|
+
|
|
139
|
+
## Self-hosting
|
|
140
|
+
|
|
141
|
+
The server (FastAPI + SQLite, with the remote MCP endpoint) and the worker (Blender in disposable Docker containers,
|
|
142
|
+
`--network none --cap-drop ALL`) live in the internal repository and are not part of this package yet. This repository
|
|
143
|
+
holds the client side: stdio MCP server, CLI, HTTP client, samples and the Claude Code plugin.
|
|
144
|
+
|
|
145
|
+
## MCP registry
|
|
146
|
+
|
|
147
|
+
This server is listed in the official MCP registry as `io.github.KJdayo/janction-render` (remote: `https://render.janction.jp/mcp`).
|
|
148
|
+
|
|
149
|
+
mcp-name: io.github.KJdayo/janction-render
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT (see LICENSE). Operated by JasmyLab Inc.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# JANCTION Render
|
|
2
|
+
|
|
3
|
+
A cloud GPU render farm for Blender, built for AI agents. Render Blender scenes on JANCTION GPUs from **Claude, ChatGPT,
|
|
4
|
+
Claude Code, Codex, Cursor or any MCP client**: an MCP server (remote and stdio) plus a CLI. Use it to render Blender
|
|
5
|
+
without a GPU, or when rendering locally is slow.
|
|
6
|
+
|
|
7
|
+
- Input: a `.blend` file, or a **bpy Python script that builds the scene** (no local Blender needed).
|
|
8
|
+
- `scene_info` reads the scene without rendering (cameras, frame range, missing files).
|
|
9
|
+
- `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
|
|
10
|
+
- `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
|
|
11
|
+
- `render_final` renders the frames on GPUs and joins them into an MP4; `render_status` reports the remaining time.
|
|
12
|
+
- Free beta: no charges. Each key gets 10 GPU-minutes per day; a final render is up to 240 frames at 1080p.
|
|
13
|
+
Paid plans (per GPU second, prepaid credit) will be announced on the service page before they start.
|
|
14
|
+
- Inputs and results are deleted 24 hours after last use and are never used for training.
|
|
15
|
+
|
|
16
|
+
## Status
|
|
17
|
+
|
|
18
|
+
Free beta. Service page: `https://render.janction.jp` (`/v1/health`, `/llms.txt`). Blender 5.0, Cycles on GPU.
|
|
19
|
+
|
|
20
|
+
## Connect (remote MCP, nothing to install)
|
|
21
|
+
|
|
22
|
+
MCP server URL: **`https://render.janction.jp/mcp`** (Streamable HTTP). Auth is OAuth 2.1 with dynamic client
|
|
23
|
+
registration: pressing "Connect" opens a page that creates a free API key (or takes one you already have). A raw API key
|
|
24
|
+
also works as `Authorization: Bearer jr_...`.
|
|
25
|
+
|
|
26
|
+
| client | how |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Claude.ai (web, desktop, mobile) | Settings → Connectors → Add custom connector → paste the URL → Connect |
|
|
29
|
+
| ChatGPT | Settings → Connectors → Advanced → Developer mode → Create → paste the URL (OAuth) |
|
|
30
|
+
| Claude Code | `claude mcp add --transport http janction-render https://render.janction.jp/mcp`, then `/mcp` to authenticate |
|
|
31
|
+
| Cursor, Windsurf, other MCP clients | Streamable HTTP at the URL above (OAuth, or a Bearer API key header) |
|
|
32
|
+
|
|
33
|
+
Remote tools take `scene_script` (bpy code as text), `scene_url` (an https link to a `.blend` or `.py`) or `scene_id`;
|
|
34
|
+
results come back as an inline image plus download links that need no key and work for about 24 hours.
|
|
35
|
+
|
|
36
|
+
Claude Code plugin (the remote connector plus a skill with the workflow):
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
/plugin marketplace add KJdayo/janction-render
|
|
40
|
+
/plugin install janction-render@janction-render
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Install (stdio MCP, sends local files)
|
|
44
|
+
|
|
45
|
+
The package is on PyPI as `janction-render`.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
# Claude Code (uvx runs it without a global install)
|
|
49
|
+
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- uvx --from janction-render janction-render-mcp
|
|
50
|
+
|
|
51
|
+
# or with pip / pipx
|
|
52
|
+
pip install janction-render
|
|
53
|
+
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- janction-render-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Codex: add to `~/.codex/config.toml`
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[mcp_servers.janction-render]
|
|
60
|
+
command = "uvx"
|
|
61
|
+
args = ["--from", "janction-render", "janction-render-mcp"]
|
|
62
|
+
env = { JANCTION_RENDER_SERVER = "https://render.janction.jp" }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A temporary API key is issued automatically on first use and cached in `~/.janction-render.json`; set
|
|
66
|
+
`JANCTION_RENDER_API_KEY` to pin one (quota and, later, credit belong to the key). The same key can be used from the
|
|
67
|
+
remote connector: paste it on the connect page.
|
|
68
|
+
|
|
69
|
+
Then, in Claude Code:
|
|
70
|
+
|
|
71
|
+
> Build a small street scene in Blender with a camera fly-through and render a preview with janction-render.
|
|
72
|
+
|
|
73
|
+
## Tools
|
|
74
|
+
|
|
75
|
+
| tool | what |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `scene_info(scene_script | scene_url | scene_path | scene_id)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
|
|
78
|
+
| `render_preview(..., frames="1-24")` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
|
|
79
|
+
| `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 |
|
|
80
|
+
| `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output)` | PNG or MP4; returns job_id + estimate |
|
|
81
|
+
| `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id)` files or links; `render_cancel(job_id)` |
|
|
82
|
+
| `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
|
|
83
|
+
| `render_info()` | workers online, queue and expected wait |
|
|
84
|
+
|
|
85
|
+
CLI: `janction-render inspect|preview|render|status|download|cancel|jobs|balance|topup|info`.
|
|
86
|
+
|
|
87
|
+
## Writing a scene script
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import bpy, math
|
|
91
|
+
for ob in list(bpy.data.objects):
|
|
92
|
+
bpy.data.objects.remove(ob, do_unlink=True)
|
|
93
|
+
scene = bpy.context.scene
|
|
94
|
+
scene.frame_start, scene.frame_end = 1, 24
|
|
95
|
+
bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1))
|
|
96
|
+
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
|
97
|
+
scene.collection.objects.link(cam); scene.camera = cam
|
|
98
|
+
cam.location = (6, -6, 4); cam.rotation_euler = (math.radians(60), 0, math.radians(45))
|
|
99
|
+
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
|
100
|
+
scene.collection.objects.link(sun)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
|
|
104
|
+
Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
|
|
105
|
+
|
|
106
|
+
## HTTP API
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
POST /v1/keys -> {api_key} (header X-API-Key afterwards)
|
|
110
|
+
POST /v1/files multipart "file" (.blend|.py) -> {scene_id}
|
|
111
|
+
POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
|
|
112
|
+
POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output}
|
|
113
|
+
GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
|
|
114
|
+
GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
|
|
115
|
+
POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
|
|
116
|
+
GET /.well-known/oauth-protected-resource/mcp OAuth discovery
|
|
117
|
+
POST /v1/billing/checkout {amount_yen} -> {checkout_url} POST /v1/billing/sync GET /v1/ledger GET /v1/me
|
|
118
|
+
GET /llms.txt /terms /privacy /legal /security
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
During the free beta a `429 quota_exceeded` response carries `resets_at`; a `400 beta_limit` means the job is too big
|
|
122
|
+
(split it). Once paid plans start, a `402 payment_required` response carries `checkout_url`.
|
|
123
|
+
|
|
124
|
+
## Self-hosting
|
|
125
|
+
|
|
126
|
+
The server (FastAPI + SQLite, with the remote MCP endpoint) and the worker (Blender in disposable Docker containers,
|
|
127
|
+
`--network none --cap-drop ALL`) live in the internal repository and are not part of this package yet. This repository
|
|
128
|
+
holds the client side: stdio MCP server, CLI, HTTP client, samples and the Claude Code plugin.
|
|
129
|
+
|
|
130
|
+
## MCP registry
|
|
131
|
+
|
|
132
|
+
This server is listed in the official MCP registry as `io.github.KJdayo/janction-render` (remote: `https://render.janction.jp/mcp`).
|
|
133
|
+
|
|
134
|
+
mcp-name: io.github.KJdayo/janction-render
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT (see LICENSE). Operated by JasmyLab Inc.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""仕事の状態を、エージェントが読みやすい短い形にする。stdio の MCP(mcp_server.py)とリモート MCP(server/mcp_remote.py)で共通。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def eta_brief(e: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
8
|
+
"""受付の eta(作業時間の見積もり)から、伝える価値のある項目だけ。"""
|
|
9
|
+
if not e:
|
|
10
|
+
return None
|
|
11
|
+
keep = ("human", "remaining_seconds", "queue_wait_seconds", "remaining_gpu_seconds", "elapsed_seconds",
|
|
12
|
+
"running_for_seconds", "took_seconds", "workers_online", "basis")
|
|
13
|
+
return {k: e[k] for k in keep if e.get(k) is not None}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def estimate_brief(e: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
17
|
+
if not e:
|
|
18
|
+
return None
|
|
19
|
+
keep = ("human", "seconds", "wall_seconds", "queue_wait_seconds", "per_frame_seconds", "frames", "chunks",
|
|
20
|
+
"workers_online", "basis", "basis_note", "note", "quota", "cost")
|
|
21
|
+
return {k: e[k] for k in keep if e.get(k) is not None}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def brief(j: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
p = j["progress"]
|
|
26
|
+
out: dict[str, Any] = {
|
|
27
|
+
"job_id": j["job_id"],
|
|
28
|
+
"kind": j["kind"],
|
|
29
|
+
"status": j["status"],
|
|
30
|
+
"scene_id": j["scene_id"],
|
|
31
|
+
"frames": j["frames"] if j.get("frames") else f"{j['frame_start']}-{j['frame_end']}",
|
|
32
|
+
"progress": f"{p['frames_done']}/{p['frames_total']} frames, {p['chunks_done']}/{p['chunks_total']} chunks",
|
|
33
|
+
"queue_ahead": p["queue_ahead"],
|
|
34
|
+
"gpu_seconds": j["gpu_seconds"],
|
|
35
|
+
"device": j["device"],
|
|
36
|
+
"expires_at": j["expires_at"],
|
|
37
|
+
}
|
|
38
|
+
if j.get("eta"):
|
|
39
|
+
out["eta"] = eta_brief(j["eta"])
|
|
40
|
+
if j.get("estimate"):
|
|
41
|
+
out["estimate"] = estimate_brief(j["estimate"])
|
|
42
|
+
if j.get("error"):
|
|
43
|
+
out["error"] = j["error"]
|
|
44
|
+
if j.get("log_tail"):
|
|
45
|
+
out["log_tail"] = j["log_tail"][-1500:]
|
|
46
|
+
if j.get("warnings"):
|
|
47
|
+
out["warnings"] = j["warnings"]
|
|
48
|
+
if j.get("output"):
|
|
49
|
+
out["output"] = j["output"]["name"]
|
|
50
|
+
if j.get("cost"):
|
|
51
|
+
c = j["cost"]
|
|
52
|
+
out["cost"] = ("free" if c["free"] else
|
|
53
|
+
(f"{c['charged_yen']} yen charged" if c["settled"] else f"up to {c['reserved_yen']} yen reserved"))
|
|
54
|
+
out["artifacts"] = [a["name"] for a in j["artifacts"]][:50]
|
|
55
|
+
return out
|
|
@@ -185,6 +185,13 @@ class Client:
|
|
|
185
185
|
body.update({k: v for k, v in params.items() if v is not None})
|
|
186
186
|
return self._req("POST", "/v1/jobs", json=body)
|
|
187
187
|
|
|
188
|
+
def estimate(self, scene_id: Optional[str] = None, **params: Any) -> dict[str, Any]:
|
|
189
|
+
"""入れずに見積もりだけ(GPU 秒・列の待ち・壁時計の目安・無料枠に収まるか)。"""
|
|
190
|
+
body: dict[str, Any] = {k: v for k, v in params.items() if v is not None}
|
|
191
|
+
if scene_id:
|
|
192
|
+
body["scene_id"] = scene_id
|
|
193
|
+
return self._req("POST", "/v1/estimate", json=body)
|
|
194
|
+
|
|
188
195
|
def job(self, job_id: str) -> dict[str, Any]:
|
|
189
196
|
return self._req("GET", f"/v1/jobs/{job_id}")
|
|
190
197
|
|
|
@@ -26,6 +26,7 @@ try:
|
|
|
26
26
|
except ImportError: # mcp 1.x
|
|
27
27
|
from mcp.server.fastmcp import FastMCP as MCPServer, Image # type: ignore
|
|
28
28
|
|
|
29
|
+
from janction_render.brief import brief as _brief, estimate_brief
|
|
29
30
|
from janction_render.client import Client, ClientError, parse_frames
|
|
30
31
|
|
|
31
32
|
PREVIEW_WAIT_S = 240.0
|
|
@@ -34,12 +35,13 @@ INFO_WAIT_S = 150.0
|
|
|
34
35
|
mcp = MCPServer(
|
|
35
36
|
name="janction-render",
|
|
36
37
|
instructions=(
|
|
37
|
-
"Render Blender scenes on JANCTION GPUs from the terminal. Use it when the
|
|
38
|
-
"building 3DCG with Blender (a .blend file or a bpy Python script) and has no GPU or "
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"render_download.
|
|
38
|
+
"Render Blender scenes on JANCTION GPUs (a cloud render farm for AI agents) from the terminal. Use it when the "
|
|
39
|
+
"user is building 3DCG with Blender (a .blend file or a bpy Python script) and has no GPU or rendering locally "
|
|
40
|
+
"is slow. Flow: scene_info (cameras, frame range, missing files; no render) -> render_preview (1-4 fast frames "
|
|
41
|
+
"in one image; look at it, fix the scene, repeat) -> ask the user 'is this OK?' -> render_estimate (tell the "
|
|
42
|
+
"user how long it takes and whether it fits today's free quota) -> render_final (frames or MP4) -> "
|
|
43
|
+
"render_status / render_download. Always tell the user the time estimate (estimate.human / eta.human). "
|
|
44
|
+
"Blender 5.0 with Cycles on GPU. Inputs and results are deleted 24 hours after last use."
|
|
43
45
|
),
|
|
44
46
|
)
|
|
45
47
|
|
|
@@ -72,36 +74,6 @@ def _quota_block(exc: ClientError) -> Optional[dict[str, Any]]:
|
|
|
72
74
|
"lower resolution or samples) may still fit"}
|
|
73
75
|
|
|
74
76
|
|
|
75
|
-
def _brief(j: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
-
p = j["progress"]
|
|
77
|
-
out = {
|
|
78
|
-
"job_id": j["job_id"],
|
|
79
|
-
"kind": j["kind"],
|
|
80
|
-
"status": j["status"],
|
|
81
|
-
"scene_id": j["scene_id"],
|
|
82
|
-
"frames": j["frames"] if j.get("frames") else f"{j['frame_start']}-{j['frame_end']}",
|
|
83
|
-
"progress": f"{p['frames_done']}/{p['frames_total']} frames, {p['chunks_done']}/{p['chunks_total']} chunks",
|
|
84
|
-
"queue_ahead": p["queue_ahead"],
|
|
85
|
-
"gpu_seconds": j["gpu_seconds"],
|
|
86
|
-
"device": j["device"],
|
|
87
|
-
"expires_at": j["expires_at"],
|
|
88
|
-
}
|
|
89
|
-
if j.get("error"):
|
|
90
|
-
out["error"] = j["error"]
|
|
91
|
-
if j.get("log_tail"):
|
|
92
|
-
out["log_tail"] = j["log_tail"][-1500:]
|
|
93
|
-
if j.get("warnings"):
|
|
94
|
-
out["warnings"] = j["warnings"]
|
|
95
|
-
if j.get("output"):
|
|
96
|
-
out["output"] = j["output"]["name"]
|
|
97
|
-
if j.get("cost"):
|
|
98
|
-
c = j["cost"]
|
|
99
|
-
out["cost"] = ("free" if c["free"] else
|
|
100
|
-
(f"{c['charged_yen']} yen charged" if c["settled"] else f"up to {c['reserved_yen']} yen reserved"))
|
|
101
|
-
out["artifacts"] = [a["name"] for a in j["artifacts"]][:50]
|
|
102
|
-
return out
|
|
103
|
-
|
|
104
|
-
|
|
105
77
|
def _payment_block(exc: ClientError) -> Optional[dict[str, Any]]:
|
|
106
78
|
"""残高不足(402)なら、決済ページのリンクと次の手順を返す。"""
|
|
107
79
|
p = exc.payment()
|
|
@@ -218,6 +190,37 @@ def render_preview(
|
|
|
218
190
|
return result
|
|
219
191
|
|
|
220
192
|
|
|
193
|
+
@mcp.tool()
|
|
194
|
+
def render_estimate(scene_id: str = "", kind: str = "final", frame_start: int = 1, frame_end: int = 1,
|
|
195
|
+
frames: str = "", width: int = 1920, height: int = 1080, samples: int = 128) -> str:
|
|
196
|
+
"""Estimate how long a render will take BEFORE starting it (no GPU time used): GPU seconds, queue wait,
|
|
197
|
+
wall-clock time as a human-readable string ('about 3 minutes'), and whether it fits today's free quota and
|
|
198
|
+
the size limits. kind is 'final' (frame_start..frame_end at width x height, samples) or 'preview' (frames
|
|
199
|
+
like '1-24'). Pass scene_id when you have one: the estimate then uses this scene's own measured render
|
|
200
|
+
times. Tell the user the result before calling render_final."""
|
|
201
|
+
c = _client()
|
|
202
|
+
try:
|
|
203
|
+
params: dict[str, Any] = {"kind": kind, "width": width, "height": height, "samples": samples}
|
|
204
|
+
if kind == "preview":
|
|
205
|
+
params["frames"] = parse_frames(frames)
|
|
206
|
+
else:
|
|
207
|
+
params.update({"frame_start": frame_start, "frame_end": frame_end})
|
|
208
|
+
est = c.estimate(scene_id or None, **params)
|
|
209
|
+
except (ClientError, ValueError) as exc:
|
|
210
|
+
return _j({"ok": False, "error": str(exc)})
|
|
211
|
+
out = estimate_brief(est) or {}
|
|
212
|
+
out["kind"] = kind
|
|
213
|
+
q = est.get("quota") or {}
|
|
214
|
+
if q and not q.get("fits_today", True):
|
|
215
|
+
out["next"] = ("this does not fit today's remaining free GPU time; propose fewer frames, lower resolution or "
|
|
216
|
+
"samples, or wait until resets_at")
|
|
217
|
+
elif q and q.get("fits_size") is False:
|
|
218
|
+
out["next"] = f"too big for the free beta (max {q.get('max_frames_per_job')} frames and 1920x1080 per job); split it"
|
|
219
|
+
else:
|
|
220
|
+
out["next"] = "tell the user the estimate and ask before calling render_final"
|
|
221
|
+
return _j(out)
|
|
222
|
+
|
|
223
|
+
|
|
221
224
|
@mcp.tool()
|
|
222
225
|
def render_final(
|
|
223
226
|
scene_path: str = "",
|
|
@@ -253,20 +256,26 @@ def render_final(
|
|
|
253
256
|
except (ValueError, FileNotFoundError) as exc:
|
|
254
257
|
return _j({"ok": False, "error": str(exc)})
|
|
255
258
|
brief = _brief(j)
|
|
256
|
-
|
|
257
|
-
brief["next"] = "
|
|
259
|
+
est = j.get("estimate") or {}
|
|
260
|
+
brief["next"] = (f"tell the user it will take {est.get('human', 'a few minutes')}; then render_status(job_id) "
|
|
261
|
+
"until status is done, then render_download(job_id)")
|
|
258
262
|
return _j(brief)
|
|
259
263
|
|
|
260
264
|
|
|
261
265
|
@mcp.tool()
|
|
262
266
|
def render_status(job_id: str) -> str:
|
|
263
267
|
"""Check a JANCTION render job: status (queued/running/done/failed/canceled), frames done,
|
|
264
|
-
how many chunks are queued ahead,
|
|
265
|
-
tail if it failed. Use render_download
|
|
268
|
+
how many chunks are queued ahead, the ETA (eta.human = remaining time including queue wait),
|
|
269
|
+
artifacts ready, Blender warnings, and the error plus log tail if it failed. Use render_download
|
|
270
|
+
once status is done."""
|
|
266
271
|
try:
|
|
267
|
-
|
|
272
|
+
j = _client().job(job_id)
|
|
268
273
|
except ClientError as exc:
|
|
269
274
|
return _j({"ok": False, "error": str(exc)})
|
|
275
|
+
b = _brief(j)
|
|
276
|
+
if j["status"] in ("queued", "running"):
|
|
277
|
+
b["next"] = f"tell the user: {((j.get('eta') or {}).get('human') or 'still working')}; check again in a while"
|
|
278
|
+
return _j(b)
|
|
270
279
|
|
|
271
280
|
|
|
272
281
|
@mcp.tool()
|
|
@@ -346,7 +355,8 @@ def render_info() -> str:
|
|
|
346
355
|
except (ClientError, Exception) as exc: # noqa: BLE001
|
|
347
356
|
return _j({"ok": False, "server": c.server, "error": str(exc)})
|
|
348
357
|
return _j({"ok": True, "server": c.server, "workers": health["workers"], "queue": health["queue"],
|
|
349
|
-
"key": me, "downloads_go_to": str(_out_root())
|
|
358
|
+
"key": me, "downloads_go_to": str(_out_root()),
|
|
359
|
+
"remote_mcp": f"{c.server}/mcp (Streamable HTTP; for Claude.ai, ChatGPT, Cursor: add this URL as a connector)"})
|
|
350
360
|
|
|
351
361
|
|
|
352
362
|
def main() -> None:
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: janction-render
|
|
3
|
+
Version: 0.2.0
|
|
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
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: requests>=2.31
|
|
9
|
+
Requires-Dist: mcp>=1.10
|
|
10
|
+
Provides-Extra: mcp
|
|
11
|
+
Requires-Dist: mcp>=1.10; extra == "mcp"
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# JANCTION Render
|
|
17
|
+
|
|
18
|
+
A cloud GPU render farm for Blender, built for AI agents. Render Blender scenes on JANCTION GPUs from **Claude, ChatGPT,
|
|
19
|
+
Claude Code, Codex, Cursor or any MCP client**: an MCP server (remote and stdio) plus a CLI. Use it to render Blender
|
|
20
|
+
without a GPU, or when rendering locally is slow.
|
|
21
|
+
|
|
22
|
+
- Input: a `.blend` file, or a **bpy Python script that builds the scene** (no local Blender needed).
|
|
23
|
+
- `scene_info` reads the scene without rendering (cameras, frame range, missing files).
|
|
24
|
+
- `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
|
|
25
|
+
- `render_estimate` says how long a render will take ("about 3 minutes") and whether it fits today's free quota.
|
|
26
|
+
- `render_final` renders the frames on GPUs and joins them into an MP4; `render_status` reports the remaining time.
|
|
27
|
+
- Free beta: no charges. Each key gets 10 GPU-minutes per day; a final render is up to 240 frames at 1080p.
|
|
28
|
+
Paid plans (per GPU second, prepaid credit) will be announced on the service page before they start.
|
|
29
|
+
- Inputs and results are deleted 24 hours after last use and are never used for training.
|
|
30
|
+
|
|
31
|
+
## Status
|
|
32
|
+
|
|
33
|
+
Free beta. Service page: `https://render.janction.jp` (`/v1/health`, `/llms.txt`). Blender 5.0, Cycles on GPU.
|
|
34
|
+
|
|
35
|
+
## Connect (remote MCP, nothing to install)
|
|
36
|
+
|
|
37
|
+
MCP server URL: **`https://render.janction.jp/mcp`** (Streamable HTTP). Auth is OAuth 2.1 with dynamic client
|
|
38
|
+
registration: pressing "Connect" opens a page that creates a free API key (or takes one you already have). A raw API key
|
|
39
|
+
also works as `Authorization: Bearer jr_...`.
|
|
40
|
+
|
|
41
|
+
| client | how |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Claude.ai (web, desktop, mobile) | Settings → Connectors → Add custom connector → paste the URL → Connect |
|
|
44
|
+
| ChatGPT | Settings → Connectors → Advanced → Developer mode → Create → paste the URL (OAuth) |
|
|
45
|
+
| Claude Code | `claude mcp add --transport http janction-render https://render.janction.jp/mcp`, then `/mcp` to authenticate |
|
|
46
|
+
| Cursor, Windsurf, other MCP clients | Streamable HTTP at the URL above (OAuth, or a Bearer API key header) |
|
|
47
|
+
|
|
48
|
+
Remote tools take `scene_script` (bpy code as text), `scene_url` (an https link to a `.blend` or `.py`) or `scene_id`;
|
|
49
|
+
results come back as an inline image plus download links that need no key and work for about 24 hours.
|
|
50
|
+
|
|
51
|
+
Claude Code plugin (the remote connector plus a skill with the workflow):
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
/plugin marketplace add KJdayo/janction-render
|
|
55
|
+
/plugin install janction-render@janction-render
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Install (stdio MCP, sends local files)
|
|
59
|
+
|
|
60
|
+
The package is on PyPI as `janction-render`.
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
# Claude Code (uvx runs it without a global install)
|
|
64
|
+
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- uvx --from janction-render janction-render-mcp
|
|
65
|
+
|
|
66
|
+
# or with pip / pipx
|
|
67
|
+
pip install janction-render
|
|
68
|
+
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- janction-render-mcp
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Codex: add to `~/.codex/config.toml`
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[mcp_servers.janction-render]
|
|
75
|
+
command = "uvx"
|
|
76
|
+
args = ["--from", "janction-render", "janction-render-mcp"]
|
|
77
|
+
env = { JANCTION_RENDER_SERVER = "https://render.janction.jp" }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A temporary API key is issued automatically on first use and cached in `~/.janction-render.json`; set
|
|
81
|
+
`JANCTION_RENDER_API_KEY` to pin one (quota and, later, credit belong to the key). The same key can be used from the
|
|
82
|
+
remote connector: paste it on the connect page.
|
|
83
|
+
|
|
84
|
+
Then, in Claude Code:
|
|
85
|
+
|
|
86
|
+
> Build a small street scene in Blender with a camera fly-through and render a preview with janction-render.
|
|
87
|
+
|
|
88
|
+
## Tools
|
|
89
|
+
|
|
90
|
+
| tool | what |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `scene_info(scene_script | scene_url | scene_path | scene_id)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
|
|
93
|
+
| `render_preview(..., frames="1-24")` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
|
|
94
|
+
| `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 |
|
|
95
|
+
| `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output)` | PNG or MP4; returns job_id + estimate |
|
|
96
|
+
| `render_status(job_id)` | progress and ETA (`eta.human`); `render_download(job_id)` files or links; `render_cancel(job_id)` |
|
|
97
|
+
| `billing()` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
|
|
98
|
+
| `render_info()` | workers online, queue and expected wait |
|
|
99
|
+
|
|
100
|
+
CLI: `janction-render inspect|preview|render|status|download|cancel|jobs|balance|topup|info`.
|
|
101
|
+
|
|
102
|
+
## Writing a scene script
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import bpy, math
|
|
106
|
+
for ob in list(bpy.data.objects):
|
|
107
|
+
bpy.data.objects.remove(ob, do_unlink=True)
|
|
108
|
+
scene = bpy.context.scene
|
|
109
|
+
scene.frame_start, scene.frame_end = 1, 24
|
|
110
|
+
bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1))
|
|
111
|
+
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
|
112
|
+
scene.collection.objects.link(cam); scene.camera = cam
|
|
113
|
+
cam.location = (6, -6, 4); cam.rotation_euler = (math.radians(60), 0, math.radians(45))
|
|
114
|
+
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
|
115
|
+
scene.collection.objects.link(sun)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
|
|
119
|
+
Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
|
|
120
|
+
|
|
121
|
+
## HTTP API
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
POST /v1/keys -> {api_key} (header X-API-Key afterwards)
|
|
125
|
+
POST /v1/files multipart "file" (.blend|.py) -> {scene_id}
|
|
126
|
+
POST /v1/estimate {kind, frames|frame_start/frame_end, width, height, samples, scene_id?} -> seconds, wall_seconds, human, quota
|
|
127
|
+
POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output}
|
|
128
|
+
GET /v1/jobs/{id} status, progress, eta, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
|
|
129
|
+
GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
|
|
130
|
+
POST /mcp remote MCP (Streamable HTTP; Bearer api key or OAuth)
|
|
131
|
+
GET /.well-known/oauth-protected-resource/mcp OAuth discovery
|
|
132
|
+
POST /v1/billing/checkout {amount_yen} -> {checkout_url} POST /v1/billing/sync GET /v1/ledger GET /v1/me
|
|
133
|
+
GET /llms.txt /terms /privacy /legal /security
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
During the free beta a `429 quota_exceeded` response carries `resets_at`; a `400 beta_limit` means the job is too big
|
|
137
|
+
(split it). Once paid plans start, a `402 payment_required` response carries `checkout_url`.
|
|
138
|
+
|
|
139
|
+
## Self-hosting
|
|
140
|
+
|
|
141
|
+
The server (FastAPI + SQLite, with the remote MCP endpoint) and the worker (Blender in disposable Docker containers,
|
|
142
|
+
`--network none --cap-drop ALL`) live in the internal repository and are not part of this package yet. This repository
|
|
143
|
+
holds the client side: stdio MCP server, CLI, HTTP client, samples and the Claude Code plugin.
|
|
144
|
+
|
|
145
|
+
## MCP registry
|
|
146
|
+
|
|
147
|
+
This server is listed in the official MCP registry as `io.github.KJdayo/janction-render` (remote: `https://render.janction.jp/mcp`).
|
|
148
|
+
|
|
149
|
+
mcp-name: io.github.KJdayo/janction-render
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT (see LICENSE). Operated by JasmyLab Inc.
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "janction-render"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
|
11
11
|
dependencies = ["requests>=2.31", "mcp>=1.10"]
|
janction_render-0.1.0/PKG-INFO
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: janction-render
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Render Blender scenes on JANCTION GPUs straight from terminal AI agents (Claude Code, Codex): MCP server + CLI
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Dist: requests>=2.31
|
|
9
|
-
Requires-Dist: mcp>=1.10
|
|
10
|
-
Provides-Extra: mcp
|
|
11
|
-
Requires-Dist: mcp>=1.10; extra == "mcp"
|
|
12
|
-
Provides-Extra: dev
|
|
13
|
-
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
-
Dynamic: license-file
|
|
15
|
-
|
|
16
|
-
# JANCTION Render
|
|
17
|
-
|
|
18
|
-
Render Blender scenes on JANCTION GPUs straight from a terminal AI agent (Claude Code, Codex, or any MCP client).
|
|
19
|
-
Use it when you are building 3DCG with Blender and have no GPU, or rendering locally is slow.
|
|
20
|
-
|
|
21
|
-
- Input: a `.blend` file, or a **bpy Python script that builds the scene** (no local Blender needed).
|
|
22
|
-
- `scene_info` reads the scene without rendering (cameras, frame range, missing files).
|
|
23
|
-
- `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
|
|
24
|
-
- `render_final` renders the frames on GPUs and joins them into an MP4.
|
|
25
|
-
- Free beta: no charges. Each key gets 10 GPU-minutes per day; a final render is up to 240 frames at 1080p.
|
|
26
|
-
Paid plans (per GPU second, prepaid credit) will be announced on the service page before they start.
|
|
27
|
-
- Inputs and results are deleted 24 hours after last use and are never used for training.
|
|
28
|
-
|
|
29
|
-
## Status
|
|
30
|
-
|
|
31
|
-
Free beta. The public endpoint is `https://render.janction.jp` (opening; check `/v1/health`). Until it answers, you can
|
|
32
|
-
self-host the server and worker from the internal package (see Self-hosting) or wait for the announcement in `/llms.txt`.
|
|
33
|
-
|
|
34
|
-
## Install
|
|
35
|
-
|
|
36
|
-
Until the package is on PyPI, install straight from this repository:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
# Claude Code
|
|
40
|
-
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- uvx --from git+https://github.com/KJdayo/janction-render janction-render-mcp
|
|
41
|
-
|
|
42
|
-
# or with pipx
|
|
43
|
-
pipx install git+https://github.com/KJdayo/janction-render
|
|
44
|
-
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- janction-render-mcp
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Codex: add to `~/.codex/config.toml`
|
|
48
|
-
|
|
49
|
-
```toml
|
|
50
|
-
[mcp_servers.janction-render]
|
|
51
|
-
command = "uvx"
|
|
52
|
-
args = ["--from", "git+https://github.com/KJdayo/janction-render", "janction-render-mcp"]
|
|
53
|
-
env = { JANCTION_RENDER_SERVER = "https://render.janction.jp" }
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
A temporary API key is issued automatically on first use and cached in `~/.janction-render.json`; set
|
|
57
|
-
`JANCTION_RENDER_API_KEY` to pin one (quota and, later, credit belong to the key).
|
|
58
|
-
|
|
59
|
-
Then, in Claude Code:
|
|
60
|
-
|
|
61
|
-
> Build a small street scene in Blender with a camera fly-through and render a preview with janction-render.
|
|
62
|
-
|
|
63
|
-
## Tools
|
|
64
|
-
|
|
65
|
-
| tool | what |
|
|
66
|
-
|---|---|
|
|
67
|
-
| `scene_info(scene_path | scene_script | scene_id)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
|
|
68
|
-
| `render_preview(..., frames="1-24")` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
|
|
69
|
-
| `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output)` | PNG or MP4; returns job_id + estimate |
|
|
70
|
-
| `render_status(job_id)` / `render_download(job_id)` / `render_cancel(job_id)` | progress, files, cancel |
|
|
71
|
-
| `billing(topup_yen=0)` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
|
|
72
|
-
| `render_info()` | server, workers online, queue |
|
|
73
|
-
|
|
74
|
-
CLI: `janction-render inspect|preview|render|status|download|cancel|jobs|balance|topup|info`.
|
|
75
|
-
|
|
76
|
-
## Writing a scene script
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
import bpy, math
|
|
80
|
-
for ob in list(bpy.data.objects):
|
|
81
|
-
bpy.data.objects.remove(ob, do_unlink=True)
|
|
82
|
-
scene = bpy.context.scene
|
|
83
|
-
scene.frame_start, scene.frame_end = 1, 24
|
|
84
|
-
bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1))
|
|
85
|
-
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
|
86
|
-
scene.collection.objects.link(cam); scene.camera = cam
|
|
87
|
-
cam.location = (6, -6, 4); cam.rotation_euler = (math.radians(60), 0, math.radians(45))
|
|
88
|
-
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
|
89
|
-
scene.collection.objects.link(sun)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
|
|
93
|
-
Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
|
|
94
|
-
|
|
95
|
-
## HTTP API
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
POST /v1/keys -> {api_key} (header X-API-Key afterwards)
|
|
99
|
-
POST /v1/files multipart "file" (.blend|.py) -> {scene_id}
|
|
100
|
-
POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output}
|
|
101
|
-
GET /v1/jobs/{id} status, progress, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
|
|
102
|
-
GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
|
|
103
|
-
POST /v1/billing/checkout {amount_yen} -> {checkout_url} POST /v1/billing/sync GET /v1/ledger GET /v1/me
|
|
104
|
-
GET /llms.txt /terms /privacy /tokushoho
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
During the free beta a `429 quota_exceeded` response carries `resets_at`; a `400 beta_limit` means the job is too big
|
|
108
|
-
(split it). Once paid plans start, a `402 payment_required` response carries `checkout_url`.
|
|
109
|
-
|
|
110
|
-
## Self-hosting
|
|
111
|
-
|
|
112
|
-
The server (FastAPI + SQLite) and the worker (Blender in disposable Docker containers, `--network none --cap-drop ALL`)
|
|
113
|
-
live in the internal repository and are not part of this package yet. This repository holds the client side: MCP server, CLI,
|
|
114
|
-
HTTP client and samples.
|
|
115
|
-
|
|
116
|
-
## License
|
|
117
|
-
|
|
118
|
-
MIT (see LICENSE). Operated by JasmyLab Inc.
|
janction_render-0.1.0/README.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# JANCTION Render
|
|
2
|
-
|
|
3
|
-
Render Blender scenes on JANCTION GPUs straight from a terminal AI agent (Claude Code, Codex, or any MCP client).
|
|
4
|
-
Use it when you are building 3DCG with Blender and have no GPU, or rendering locally is slow.
|
|
5
|
-
|
|
6
|
-
- Input: a `.blend` file, or a **bpy Python script that builds the scene** (no local Blender needed).
|
|
7
|
-
- `scene_info` reads the scene without rendering (cameras, frame range, missing files).
|
|
8
|
-
- `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
|
|
9
|
-
- `render_final` renders the frames on GPUs and joins them into an MP4.
|
|
10
|
-
- Free beta: no charges. Each key gets 10 GPU-minutes per day; a final render is up to 240 frames at 1080p.
|
|
11
|
-
Paid plans (per GPU second, prepaid credit) will be announced on the service page before they start.
|
|
12
|
-
- Inputs and results are deleted 24 hours after last use and are never used for training.
|
|
13
|
-
|
|
14
|
-
## Status
|
|
15
|
-
|
|
16
|
-
Free beta. The public endpoint is `https://render.janction.jp` (opening; check `/v1/health`). Until it answers, you can
|
|
17
|
-
self-host the server and worker from the internal package (see Self-hosting) or wait for the announcement in `/llms.txt`.
|
|
18
|
-
|
|
19
|
-
## Install
|
|
20
|
-
|
|
21
|
-
Until the package is on PyPI, install straight from this repository:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
# Claude Code
|
|
25
|
-
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- uvx --from git+https://github.com/KJdayo/janction-render janction-render-mcp
|
|
26
|
-
|
|
27
|
-
# or with pipx
|
|
28
|
-
pipx install git+https://github.com/KJdayo/janction-render
|
|
29
|
-
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- janction-render-mcp
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Codex: add to `~/.codex/config.toml`
|
|
33
|
-
|
|
34
|
-
```toml
|
|
35
|
-
[mcp_servers.janction-render]
|
|
36
|
-
command = "uvx"
|
|
37
|
-
args = ["--from", "git+https://github.com/KJdayo/janction-render", "janction-render-mcp"]
|
|
38
|
-
env = { JANCTION_RENDER_SERVER = "https://render.janction.jp" }
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
A temporary API key is issued automatically on first use and cached in `~/.janction-render.json`; set
|
|
42
|
-
`JANCTION_RENDER_API_KEY` to pin one (quota and, later, credit belong to the key).
|
|
43
|
-
|
|
44
|
-
Then, in Claude Code:
|
|
45
|
-
|
|
46
|
-
> Build a small street scene in Blender with a camera fly-through and render a preview with janction-render.
|
|
47
|
-
|
|
48
|
-
## Tools
|
|
49
|
-
|
|
50
|
-
| tool | what |
|
|
51
|
-
|---|---|
|
|
52
|
-
| `scene_info(scene_path | scene_script | scene_id)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
|
|
53
|
-
| `render_preview(..., frames="1-24")` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
|
|
54
|
-
| `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output)` | PNG or MP4; returns job_id + estimate |
|
|
55
|
-
| `render_status(job_id)` / `render_download(job_id)` / `render_cancel(job_id)` | progress, files, cancel |
|
|
56
|
-
| `billing(topup_yen=0)` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
|
|
57
|
-
| `render_info()` | server, workers online, queue |
|
|
58
|
-
|
|
59
|
-
CLI: `janction-render inspect|preview|render|status|download|cancel|jobs|balance|topup|info`.
|
|
60
|
-
|
|
61
|
-
## Writing a scene script
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
import bpy, math
|
|
65
|
-
for ob in list(bpy.data.objects):
|
|
66
|
-
bpy.data.objects.remove(ob, do_unlink=True)
|
|
67
|
-
scene = bpy.context.scene
|
|
68
|
-
scene.frame_start, scene.frame_end = 1, 24
|
|
69
|
-
bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1))
|
|
70
|
-
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
|
71
|
-
scene.collection.objects.link(cam); scene.camera = cam
|
|
72
|
-
cam.location = (6, -6, 4); cam.rotation_euler = (math.radians(60), 0, math.radians(45))
|
|
73
|
-
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
|
74
|
-
scene.collection.objects.link(sun)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
|
|
78
|
-
Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
|
|
79
|
-
|
|
80
|
-
## HTTP API
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
POST /v1/keys -> {api_key} (header X-API-Key afterwards)
|
|
84
|
-
POST /v1/files multipart "file" (.blend|.py) -> {scene_id}
|
|
85
|
-
POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output}
|
|
86
|
-
GET /v1/jobs/{id} status, progress, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
|
|
87
|
-
GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
|
|
88
|
-
POST /v1/billing/checkout {amount_yen} -> {checkout_url} POST /v1/billing/sync GET /v1/ledger GET /v1/me
|
|
89
|
-
GET /llms.txt /terms /privacy /tokushoho
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
During the free beta a `429 quota_exceeded` response carries `resets_at`; a `400 beta_limit` means the job is too big
|
|
93
|
-
(split it). Once paid plans start, a `402 payment_required` response carries `checkout_url`.
|
|
94
|
-
|
|
95
|
-
## Self-hosting
|
|
96
|
-
|
|
97
|
-
The server (FastAPI + SQLite) and the worker (Blender in disposable Docker containers, `--network none --cap-drop ALL`)
|
|
98
|
-
live in the internal repository and are not part of this package yet. This repository holds the client side: MCP server, CLI,
|
|
99
|
-
HTTP client and samples.
|
|
100
|
-
|
|
101
|
-
## License
|
|
102
|
-
|
|
103
|
-
MIT (see LICENSE). Operated by JasmyLab Inc.
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: janction-render
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Render Blender scenes on JANCTION GPUs straight from terminal AI agents (Claude Code, Codex): MCP server + CLI
|
|
5
|
-
Requires-Python: >=3.11
|
|
6
|
-
Description-Content-Type: text/markdown
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Dist: requests>=2.31
|
|
9
|
-
Requires-Dist: mcp>=1.10
|
|
10
|
-
Provides-Extra: mcp
|
|
11
|
-
Requires-Dist: mcp>=1.10; extra == "mcp"
|
|
12
|
-
Provides-Extra: dev
|
|
13
|
-
Requires-Dist: pytest>=8; extra == "dev"
|
|
14
|
-
Dynamic: license-file
|
|
15
|
-
|
|
16
|
-
# JANCTION Render
|
|
17
|
-
|
|
18
|
-
Render Blender scenes on JANCTION GPUs straight from a terminal AI agent (Claude Code, Codex, or any MCP client).
|
|
19
|
-
Use it when you are building 3DCG with Blender and have no GPU, or rendering locally is slow.
|
|
20
|
-
|
|
21
|
-
- Input: a `.blend` file, or a **bpy Python script that builds the scene** (no local Blender needed).
|
|
22
|
-
- `scene_info` reads the scene without rendering (cameras, frame range, missing files).
|
|
23
|
-
- `render_preview` returns 1-4 low-cost frames tiled in one image within seconds, so the agent can look, fix, and repeat.
|
|
24
|
-
- `render_final` renders the frames on GPUs and joins them into an MP4.
|
|
25
|
-
- Free beta: no charges. Each key gets 10 GPU-minutes per day; a final render is up to 240 frames at 1080p.
|
|
26
|
-
Paid plans (per GPU second, prepaid credit) will be announced on the service page before they start.
|
|
27
|
-
- Inputs and results are deleted 24 hours after last use and are never used for training.
|
|
28
|
-
|
|
29
|
-
## Status
|
|
30
|
-
|
|
31
|
-
Free beta. The public endpoint is `https://render.janction.jp` (opening; check `/v1/health`). Until it answers, you can
|
|
32
|
-
self-host the server and worker from the internal package (see Self-hosting) or wait for the announcement in `/llms.txt`.
|
|
33
|
-
|
|
34
|
-
## Install
|
|
35
|
-
|
|
36
|
-
Until the package is on PyPI, install straight from this repository:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
# Claude Code
|
|
40
|
-
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- uvx --from git+https://github.com/KJdayo/janction-render janction-render-mcp
|
|
41
|
-
|
|
42
|
-
# or with pipx
|
|
43
|
-
pipx install git+https://github.com/KJdayo/janction-render
|
|
44
|
-
claude mcp add janction-render -e JANCTION_RENDER_SERVER=https://render.janction.jp -- janction-render-mcp
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Codex: add to `~/.codex/config.toml`
|
|
48
|
-
|
|
49
|
-
```toml
|
|
50
|
-
[mcp_servers.janction-render]
|
|
51
|
-
command = "uvx"
|
|
52
|
-
args = ["--from", "git+https://github.com/KJdayo/janction-render", "janction-render-mcp"]
|
|
53
|
-
env = { JANCTION_RENDER_SERVER = "https://render.janction.jp" }
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
A temporary API key is issued automatically on first use and cached in `~/.janction-render.json`; set
|
|
57
|
-
`JANCTION_RENDER_API_KEY` to pin one (quota and, later, credit belong to the key).
|
|
58
|
-
|
|
59
|
-
Then, in Claude Code:
|
|
60
|
-
|
|
61
|
-
> Build a small street scene in Blender with a camera fly-through and render a preview with janction-render.
|
|
62
|
-
|
|
63
|
-
## Tools
|
|
64
|
-
|
|
65
|
-
| tool | what |
|
|
66
|
-
|---|---|
|
|
67
|
-
| `scene_info(scene_path | scene_script | scene_id)` | cameras, frame range, fps, resolution, objects, lights, missing files. No render. |
|
|
68
|
-
| `render_preview(..., frames="1-24")` | up to 4 frames (720p budget) tiled with frame labels; returns the image inline |
|
|
69
|
-
| `render_final(scene_id, frame_start, frame_end, width, height, samples, fps, output)` | PNG or MP4; returns job_id + estimate |
|
|
70
|
-
| `render_status(job_id)` / `render_download(job_id)` / `render_cancel(job_id)` | progress, files, cancel |
|
|
71
|
-
| `billing(topup_yen=0)` | free-beta quota (used today, daily limit, reset time); later balance and top-up link |
|
|
72
|
-
| `render_info()` | server, workers online, queue |
|
|
73
|
-
|
|
74
|
-
CLI: `janction-render inspect|preview|render|status|download|cancel|jobs|balance|topup|info`.
|
|
75
|
-
|
|
76
|
-
## Writing a scene script
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
import bpy, math
|
|
80
|
-
for ob in list(bpy.data.objects):
|
|
81
|
-
bpy.data.objects.remove(ob, do_unlink=True)
|
|
82
|
-
scene = bpy.context.scene
|
|
83
|
-
scene.frame_start, scene.frame_end = 1, 24
|
|
84
|
-
bpy.ops.mesh.primitive_monkey_add(location=(0, 0, 1))
|
|
85
|
-
cam = bpy.data.objects.new("Camera", bpy.data.cameras.new("Camera"))
|
|
86
|
-
scene.collection.objects.link(cam); scene.camera = cam
|
|
87
|
-
cam.location = (6, -6, 4); cam.rotation_euler = (math.radians(60), 0, math.radians(45))
|
|
88
|
-
sun = bpy.data.objects.new("Sun", bpy.data.lights.new("Sun", "SUN"))
|
|
89
|
-
scene.collection.objects.link(sun)
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The service sets engine (Cycles), resolution, samples and denoising; your script sets the scene, camera and frame range.
|
|
93
|
-
Scripts run in an isolated container with no network. See `samples/cube_scene.py`.
|
|
94
|
-
|
|
95
|
-
## HTTP API
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
POST /v1/keys -> {api_key} (header X-API-Key afterwards)
|
|
99
|
-
POST /v1/files multipart "file" (.blend|.py) -> {scene_id}
|
|
100
|
-
POST /v1/jobs {scene_id, kind: info|preview|final, frames|frame_start/frame_end, width, height, samples, camera, fps, output}
|
|
101
|
-
GET /v1/jobs/{id} status, progress, artifacts[], cost, warnings, info DELETE /v1/jobs/{id} cancel
|
|
102
|
-
GET /v1/jobs/{id}/artifacts/{name} PNG / MP4
|
|
103
|
-
POST /v1/billing/checkout {amount_yen} -> {checkout_url} POST /v1/billing/sync GET /v1/ledger GET /v1/me
|
|
104
|
-
GET /llms.txt /terms /privacy /tokushoho
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
During the free beta a `429 quota_exceeded` response carries `resets_at`; a `400 beta_limit` means the job is too big
|
|
108
|
-
(split it). Once paid plans start, a `402 payment_required` response carries `checkout_url`.
|
|
109
|
-
|
|
110
|
-
## Self-hosting
|
|
111
|
-
|
|
112
|
-
The server (FastAPI + SQLite) and the worker (Blender in disposable Docker containers, `--network none --cap-drop ALL`)
|
|
113
|
-
live in the internal repository and are not part of this package yet. This repository holds the client side: MCP server, CLI,
|
|
114
|
-
HTTP client and samples.
|
|
115
|
-
|
|
116
|
-
## License
|
|
117
|
-
|
|
118
|
-
MIT (see LICENSE). Operated by JasmyLab Inc.
|
|
File without changes
|
|
File without changes
|
{janction_render-0.1.0 → janction_render-0.2.0}/janction_render.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|