janction-render 0.4.63__tar.gz → 0.4.65__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.
Files changed (22) hide show
  1. {janction_render-0.4.63 → janction_render-0.4.65}/PKG-INFO +1 -1
  2. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/__init__.py +1 -1
  3. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/mcp_common.py +105 -1
  4. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/mcp_server.py +7 -69
  5. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render.egg-info/PKG-INFO +1 -1
  6. {janction_render-0.4.63 → janction_render-0.4.65}/pyproject.toml +1 -1
  7. {janction_render-0.4.63 → janction_render-0.4.65}/LICENSE +0 -0
  8. {janction_render-0.4.63 → janction_render-0.4.65}/README.md +0 -0
  9. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/brief.py +0 -0
  10. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/cli.py +0 -0
  11. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/client.py +0 -0
  12. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/hookup.py +0 -0
  13. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/links.py +0 -0
  14. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/mcp_app.html +0 -0
  15. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/prompts.py +0 -0
  16. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render/tooldocs.py +0 -0
  17. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render.egg-info/SOURCES.txt +0 -0
  18. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render.egg-info/dependency_links.txt +0 -0
  19. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render.egg-info/entry_points.txt +0 -0
  20. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render.egg-info/requires.txt +0 -0
  21. {janction_render-0.4.63 → janction_render-0.4.65}/janction_render.egg-info/top_level.txt +0 -0
  22. {janction_render-0.4.63 → janction_render-0.4.65}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janction-render
3
- Version: 0.4.63
3
+ Version: 0.4.65
4
4
  Summary: Cloud GPU rendering for Blender scenes and 3D models (GLB, FBX, USD, OBJ, STL), built for AI agents: MCP server (remote + stdio), HTTP API and CLI. Product shots, 360-degree turntables, animations and stills; preview in seconds with a critic; no local Blender or GPU. A 100 JPY welcome credit for a new key (one per network every 30 days) and 400 JPY more for saving a card that is not charged, then 0.1 JPY per GPU-second.
5
5
  Author-email: "JasmyLab Inc." <contact@jasmylab.com>
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """JANCTION Render — ターミナルの AI エージェントから GPU で 3DCG を描く。"""
2
2
 
3
- __version__ = "0.4.63"
3
+ __version__ = "0.4.65"
@@ -8,7 +8,7 @@
8
8
  受付の server/jobs.py もプリセットの名前はここから取る。"""
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Any
11
+ from typing import Any, Optional
12
12
 
13
13
  PURPOSE_PRESETS = ("product", "turntable", "animation")
14
14
  FINAL_TOOL_DEFAULTS = {"width": 1920, "height": 1080, "samples": 128, "output": "auto"}
@@ -60,6 +60,110 @@ def job_params(environment: str, environment_strength: float, environment_visibl
60
60
  return out
61
61
 
62
62
 
63
+ # ------------------------------------------------------------------ 見積もりとシーンの読み取りの返事(両方の道具で同じ)
64
+
65
+ def estimate_body(kind: str, scene_id: str, frames: str, frame_start: int, frame_end: int, width: int, height: int, samples: int,
66
+ cameras: Any, preset: str) -> tuple[dict[str, Any], bool]:
67
+ """render_estimate の引数から受付への本文を作る。戻り値は (本文, シーンの読み取りが先に要るか)。
68
+ preset='animation' でコマの指定が無ければ、呼ぶ側が scene_info を済ませてから見積もる。"""
69
+ from .client import parse_frames
70
+ body: dict[str, Any] = {"kind": kind, "width": width, "height": height, "samples": samples}
71
+ if scene_id:
72
+ body["scene_id"] = scene_id
73
+ if kind == "preview":
74
+ body["frames"] = parse_frames(frames) if (frames or not preset) else None
75
+ else:
76
+ body.update({"frame_start": frame_start, "frame_end": frame_end})
77
+ if cameras:
78
+ body["cameras"] = list(cameras)
79
+ if kind != "preview":
80
+ body.pop("frame_end", None)
81
+ needs_info = bool(preset == "animation" and scene_id
82
+ and (body.get("frames") is None if kind == "preview" else frame_end <= frame_start))
83
+ body = {k: v for k, v in with_preset(body, preset, kind).items() if v is not None}
84
+ return body, needs_info
85
+
86
+
87
+ def estimate_next(est: dict[str, Any]) -> str:
88
+ """見積もりの返事の next(エージェントへの次の一手)。上限の言い方は docs/60 BRAND-02 に合わせる。"""
89
+ q = est.get("quota") or {}
90
+ cost = est.get("cost") or {}
91
+ if q and q.get("fits_size") is False:
92
+ return (f"too big for one job: at most {q.get('max_frames_per_job')} frames per job part (1920x1080 on free time, 3840x2160 "
93
+ "on prepaid credit); the service splits longer instructions into parts under one job id, or lower the size")
94
+ if q and not q.get("fits_today", True) and q.get("mode") == "free_allowance":
95
+ # 有料モード: 無料枠を超えた分は残高から(10/8)。額と残高を言ってから
96
+ return (f"this goes past the free GPU time left: about {cost.get('estimated_yen')} JPY would be charged from credit "
97
+ f"(balance {cost.get('balance_yen')} JPY). Tell the user the time and the price and ask before "
98
+ "render_final; a smaller job (fewer frames, lower resolution or samples) may stay free")
99
+ if q and not q.get("fits_today", True):
100
+ return ("this does not fit today's remaining free GPU time; propose fewer frames, lower resolution or samples, "
101
+ "or wait until resets_at")
102
+ return "tell the user the estimate and ask before calling render_final"
103
+
104
+
105
+ def scene_info_reply(j: dict[str, Any], assets_param: str) -> dict[str, Any]:
106
+ """scene_info の返事(info に scene_id・エンジンの注意・警告・欠けたファイルの案内・次の一手)。assets_param は素材の渡し方の文
107
+ (remote: 'asset_urls'、stdio: 'assets=[...]')。"""
108
+ from .brief import scene_next, trim
109
+ info = dict(j.get("info") or {})
110
+ info["scene_id"] = j.get("scene_id")
111
+ if info.get("engine") and info["engine"] != "CYCLES":
112
+ info["engine_note"] = "JANCTION renders with CYCLES; the scene's engine setting is not used"
113
+ if j.get("warnings"):
114
+ info["blender_warnings"] = j["warnings"]
115
+ if info.get("missing_files"):
116
+ info["note"] = (f"missing_files will render pink/black; send them with {assets_param} (kept next to the scene), "
117
+ "pack them into the .blend (File > External Data > Pack) or fix the paths")
118
+ if isinstance(info.get("object_names"), list):
119
+ info["object_names"] = trim(info["object_names"], 30)
120
+ info.update(scene_next(info)) # 次の一手: シーンの縦横比に合わせた試し描き(10/9)
121
+ return info
122
+
123
+
124
+ def billing_reply(me: dict[str, Any], synced: Optional[dict[str, Any]] = None) -> dict[str, Any]:
125
+ """billing 道具の返事(GET /v1/me の形から)。無料ベータ / ようこそ / 毎日の無料枠 / 旧形 の 4 通りを両方の道具で同じに。"""
126
+ from .brief import plan_fields, welcome_billing
127
+ synced = synced or {}
128
+ q = me.get("quota") or {}
129
+ prefix = me.get("prefix")
130
+ if q and q.get("mode", "free_beta") == "free_beta":
131
+ return {"mode": "free_beta", "note": "no charges during the free beta; daily GPU-time quota per key",
132
+ "gpu_seconds_used_today": q["gpu_seconds_used_today"], "gpu_seconds_per_day": q["gpu_seconds_per_day"],
133
+ "gpu_seconds_left_today": max(0, q["gpu_seconds_per_day"] - q["gpu_seconds_used_today"]),
134
+ "resets_at": q["resets_at"], "max_frames_per_job": q["max_frames_per_job"], "max_pixels": q["max_pixels"],
135
+ **({"key_prefix": prefix} if prefix else {})}
136
+ if q and q.get("welcome"):
137
+ # ようこそクレジット(10/8 夜 本人の判断、docs/46): 残りの円と期限、期間の後の試し描き、最初のチャージの特典
138
+ return welcome_billing(q["welcome"], me, synced)
139
+ if q:
140
+ # 有料(10/8〜): 毎日の無料枠と、超えた分を払う残高の両方
141
+ yen = me["billing"]["yen_per_gpu_second"]
142
+ out = {"mode": "free_allowance",
143
+ "note": (f"the first {q['gpu_seconds_per_day'] // 60} GPU-minutes of each day are free; beyond that "
144
+ f"{yen} JPY per GPU-second from prepaid credit (only the part over the free time is charged)"),
145
+ "gpu_seconds_used_today": q["gpu_seconds_used_today"], "gpu_seconds_per_day": q["gpu_seconds_per_day"],
146
+ "gpu_seconds_left_today": max(0, q["gpu_seconds_per_day"] - q["gpu_seconds_used_today"]),
147
+ "resets_at": q["resets_at"], "max_frames_per_job": q["max_frames_per_job"], "max_pixels": q["max_pixels"],
148
+ "balance_yen": me["balance_yen"], "held_yen": me.get("held_yen", 0), "held_jobs": me.get("held_jobs", 0),
149
+ "yen_per_gpu_second": yen, "min_topup_yen": me["billing"]["min_topup_yen"],
150
+ "pending_checkout_url": me["billing"]["pending_checkout_url"], "charged_yen_total": me["charged_yen_total"],
151
+ "topup_options": (me["billing"].get("topup_options") or {}).get("options"),
152
+ "just_credited": synced.get("credited", []), **plan_fields(me)}
153
+ if me.get("usd_indicative") is not None:
154
+ out["usd_indicative"] = me["usd_indicative"]
155
+ if prefix:
156
+ out["key_prefix"] = prefix
157
+ return out
158
+ out = {"balance_yen": me["balance_yen"], "free_previews_left": me["free_previews_left"],
159
+ "yen_per_gpu_second": me["billing"]["yen_per_gpu_second"], "min_topup_yen": me["billing"]["min_topup_yen"],
160
+ "billing_enabled": me["billing"]["enabled"], "just_credited": synced.get("credited", []),
161
+ "pending_checkout_url": me["billing"]["pending_checkout_url"], "charged_yen_total": me["charged_yen_total"]}
162
+ if prefix:
163
+ out["key_prefix"] = prefix
164
+ return out
165
+
166
+
63
167
  # ------------------------------------------------------------------ 引数の説明(道具の description の末尾に足す)
64
168
 
65
169
  _ENGINE_DOC = (" engine: '' or 'cycles' (path tracing, the default) | 'eevee' (Blender's real-time rasterizer: about half the "
@@ -29,7 +29,7 @@ except ImportError: # mcp 1.x
29
29
  from mcp.server.fastmcp import FastMCP as MCPServer, Image # type: ignore
30
30
  from mcp.types import ToolAnnotations
31
31
 
32
- from janction_render.brief import plan_fields, review_reply, scene_next, welcome_billing
32
+ from janction_render.brief import review_reply
33
33
  from janction_render.brief import brief as _brief, estimate_brief, final_body_after_preview, followup_after_preview, \
34
34
  next_after_preview, trim, PRESENT_FIRST, preview_presentation, turntable_estimate
35
35
  from janction_render.client import Client, ClientError, parse_frames
@@ -390,18 +390,7 @@ def scene_info(scene_path: Annotated[str, D("scene_path", "")] = "",
390
390
  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") \
391
391
  if j["status"] in ("queued", "running") else "fix the scene and try again"
392
392
  return _j(b)
393
- info = dict(j.get("info") or {})
394
- info["scene_id"] = j["scene_id"]
395
- if info.get("engine") and info["engine"] != "CYCLES":
396
- info["engine_note"] = "JANCTION renders with CYCLES; the scene's engine setting is not used"
397
- if j.get("warnings"):
398
- info["blender_warnings"] = j["warnings"]
399
- if info.get("missing_files"):
400
- info["note"] = ("missing_files will render pink/black; send them with assets=[...] (kept next to the .blend), "
401
- "pack them into the .blend (File > External Data > Pack) or fix the paths")
402
- if isinstance(info.get("object_names"), list):
403
- info["object_names"] = trim(info["object_names"], 30)
404
- info.update(scene_next(info)) # 次の一手: シーンの縦横比に合わせた試し描き(10/9)
393
+ info = mcp_common.scene_info_reply(j, "assets=[...]")
405
394
  return _j(_attach_upload_notes(info))
406
395
 
407
396
 
@@ -579,37 +568,16 @@ def render_estimate(scene_id: Annotated[str, D("scene_id", "")] = "",
579
568
  c = _client()
580
569
  try:
581
570
  preset = _preset_of(preset)
582
- params: dict[str, Any] = {"kind": kind, "width": width, "height": height, "samples": samples}
583
- if kind == "preview":
584
- params["frames"] = parse_frames(frames) if (frames or not preset) else None
585
- else:
586
- params.update({"frame_start": frame_start, "frame_end": frame_end})
587
- if cameras:
588
- params["cameras"] = list(cameras)
589
- if kind != "preview":
590
- params.pop("frame_end", None)
591
- if preset == "animation" and scene_id and (params.get("frames") is None if kind == "preview" else frame_end <= frame_start):
571
+ params, needs_info = mcp_common.estimate_body(kind, scene_id, frames, frame_start, frame_end, width, height, samples, cameras, preset)
572
+ params.pop("scene_id", None)
573
+ if needs_info:
592
574
  _ensure_scene_info(c, scene_id)
593
- params = {k: v for k, v in _with_preset(params, preset, kind).items() if v is not None}
594
575
  est = c.estimate(scene_id or None, **params)
595
576
  except (ClientError, ValueError) as exc:
596
577
  return _j(_fail(exc))
597
578
  out = estimate_brief(est) or {}
598
579
  out["kind"] = kind
599
- q = est.get("quota") or {}
600
- cost = est.get("cost") or {}
601
- if q and q.get("fits_size") is False:
602
- out["next"] = f"too big for one job (max {q.get('max_frames_per_job')} frames and 1920x1080 per job); split it"
603
- elif q and not q.get("fits_today", True) and q.get("mode") == "free_allowance":
604
- # 有料モード(10/8〜): 無料枠を超えた分は残高から。額と残高を言ってから
605
- out["next"] = (f"this goes past the free GPU time left: about {cost.get('estimated_yen')} JPY would be charged from "
606
- f"credit (balance {cost.get('balance_yen')} JPY). Tell the user the time and the price and ask before "
607
- "render_final; a smaller job (fewer frames, lower resolution or samples) may stay free")
608
- elif q and not q.get("fits_today", True):
609
- out["next"] = ("this does not fit today's remaining free GPU time; propose fewer frames, lower resolution or "
610
- "samples, or wait until resets_at")
611
- else:
612
- out["next"] = "tell the user the estimate and ask before calling render_final"
580
+ out["next"] = mcp_common.estimate_next(est)
613
581
  return _j(out)
614
582
 
615
583
 
@@ -815,37 +783,7 @@ def billing(topup_yen: Annotated[int, D("topup_yen", 0)] = 0) -> str:
815
783
  if me["billing"]["enabled"]:
816
784
  synced = c.billing_sync()
817
785
  me = c.me()
818
- q = me.get("quota") or {}
819
- if q and q.get("mode", "free_beta") == "free_beta":
820
- return _j({"mode": "free_beta", "note": "no charges during the free beta; daily GPU-time quota per key",
821
- "gpu_seconds_used_today": q["gpu_seconds_used_today"], "gpu_seconds_per_day": q["gpu_seconds_per_day"],
822
- "gpu_seconds_left_today": max(0, q["gpu_seconds_per_day"] - q["gpu_seconds_used_today"]),
823
- "resets_at": q["resets_at"], "max_frames_per_job": q["max_frames_per_job"],
824
- "max_pixels": q["max_pixels"]})
825
- if q and q.get("welcome"):
826
- # ようこそクレジット(10/8 夜 本人の判断、docs/46): リモートの billing と同じ形
827
- return _j(welcome_billing(q["welcome"], me, synced))
828
- if q:
829
- # 有料(10/8〜): 毎日の無料枠と、超えた分を払う残高の両方
830
- yen = me["billing"]["yen_per_gpu_second"]
831
- return _j({"mode": "free_allowance",
832
- "note": (f"the first {q['gpu_seconds_per_day'] // 60} GPU-minutes of each day are free; beyond that "
833
- f"{yen} JPY per GPU-second from prepaid credit (only the part over the free time is charged)"),
834
- "gpu_seconds_used_today": q["gpu_seconds_used_today"], "gpu_seconds_per_day": q["gpu_seconds_per_day"],
835
- "gpu_seconds_left_today": max(0, q["gpu_seconds_per_day"] - q["gpu_seconds_used_today"]),
836
- "resets_at": q["resets_at"], "max_frames_per_job": q["max_frames_per_job"],
837
- "max_pixels": q["max_pixels"], "balance_yen": me["balance_yen"], "usd_indicative": me.get("usd_indicative"),
838
- "held_yen": me.get("held_yen", 0), "held_jobs": me.get("held_jobs", 0), "yen_per_gpu_second": yen,
839
- "min_topup_yen": me["billing"]["min_topup_yen"], "just_credited": synced.get("credited", []),
840
- "pending_checkout_url": me["billing"]["pending_checkout_url"],
841
- "topup_options": (me["billing"].get("topup_options") or {}).get("options"),
842
- "charged_yen_total": me["charged_yen_total"], **plan_fields(me)})
843
- return _j({"balance_yen": me["balance_yen"], "free_previews_left": me["free_previews_left"],
844
- "yen_per_gpu_second": me["billing"]["yen_per_gpu_second"],
845
- "min_topup_yen": me["billing"]["min_topup_yen"], "billing_enabled": me["billing"]["enabled"],
846
- "just_credited": synced.get("credited", []),
847
- "pending_checkout_url": me["billing"]["pending_checkout_url"],
848
- "charged_yen_total": me["charged_yen_total"]})
786
+ return _j(mcp_common.billing_reply(me, synced))
849
787
  except ClientError as exc:
850
788
  return _j(_fail(exc))
851
789
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: janction-render
3
- Version: 0.4.63
3
+ Version: 0.4.65
4
4
  Summary: Cloud GPU rendering for Blender scenes and 3D models (GLB, FBX, USD, OBJ, STL), built for AI agents: MCP server (remote + stdio), HTTP API and CLI. Product shots, 360-degree turntables, animations and stills; preview in seconds with a critic; no local Blender or GPU. A 100 JPY welcome credit for a new key (one per network every 30 days) and 400 JPY more for saving a card that is not charged, then 0.1 JPY per GPU-second.
5
5
  Author-email: "JasmyLab Inc." <contact@jasmylab.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "janction-render"
7
- version = "0.4.63"
7
+ version = "0.4.65"
8
8
  description = "Cloud GPU rendering for Blender scenes and 3D models (GLB, FBX, USD, OBJ, STL), built for AI agents: MCP server (remote + stdio), HTTP API and CLI. Product shots, 360-degree turntables, animations and stills; preview in seconds with a critic; no local Blender or GPU. A 100 JPY welcome credit for a new key (one per network every 30 days) and 400 JPY more for saving a card that is not charged, then 0.1 JPY per GPU-second."
9
9
  readme = "README.md"
10
10
  license = "MIT"