ima2-gen 1.1.6 → 1.1.7

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 (41) hide show
  1. package/.env.example +5 -0
  2. package/README.md +3 -0
  3. package/config.js +58 -0
  4. package/docs/FAQ.ko.md +20 -0
  5. package/docs/FAQ.md +20 -0
  6. package/docs/README.ko.md +3 -0
  7. package/docs/README.zh-CN.md +3 -0
  8. package/integrations/comfyui/ima2_gen_bridge/README.md +88 -0
  9. package/integrations/comfyui/ima2_gen_bridge/__init__.py +3 -0
  10. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  11. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  12. package/integrations/comfyui/ima2_gen_bridge/nodes.py +238 -0
  13. package/lib/canvasVersionStore.js +181 -0
  14. package/lib/cardNewsPlannerClient.js +4 -2
  15. package/lib/comfyBridge.js +214 -0
  16. package/lib/db.js +14 -0
  17. package/lib/historyList.js +4 -0
  18. package/lib/imageMetadata.js +4 -0
  19. package/lib/imageModels.js +20 -0
  20. package/lib/oauthProxy.js +88 -38
  21. package/lib/pngInfo.js +26 -0
  22. package/lib/promptImport/errors.js +16 -0
  23. package/lib/promptImport/githubSource.js +205 -0
  24. package/lib/promptImport/parsePromptCandidates.js +140 -0
  25. package/package.json +3 -2
  26. package/routes/annotations.js +95 -0
  27. package/routes/canvasVersions.js +64 -0
  28. package/routes/comfy.js +39 -0
  29. package/routes/edit.js +73 -4
  30. package/routes/generate.js +16 -2
  31. package/routes/index.js +8 -0
  32. package/routes/multimode.js +18 -1
  33. package/routes/nodes.js +25 -3
  34. package/routes/promptImport.js +175 -0
  35. package/ui/dist/assets/index-DARPdT4Q.css +1 -0
  36. package/ui/dist/assets/index-ht80GMq4.js +31 -0
  37. package/ui/dist/assets/index-ht80GMq4.js.map +1 -0
  38. package/ui/dist/index.html +2 -2
  39. package/ui/dist/assets/index-3X-6VjbF.css +0 -1
  40. package/ui/dist/assets/index-DPSq9qEs.js +0 -31
  41. package/ui/dist/assets/index-DPSq9qEs.js.map +0 -1
package/.env.example CHANGED
@@ -31,6 +31,11 @@
31
31
  # IMA2_GRAPH_MAX_NODES=500
32
32
  # IMA2_GRAPH_MAX_EDGES=1000
33
33
 
34
+ # ── ComfyUI bridge ────────────────────────────────────────────────────
35
+ # IMA2_COMFY_URL=http://127.0.0.1:8188
36
+ # IMA2_COMFY_UPLOAD_TIMEOUT_MS=30000
37
+ # IMA2_COMFY_MAX_UPLOAD_BYTES=52428800
38
+
34
39
  # ── IDs & TTLs ─────────────────────────────────────────────────────────
35
40
  # IMA2_GENERATED_HEX_BYTES=4
36
41
  # IMA2_NODE_HEX_BYTES=5
package/README.md CHANGED
@@ -193,6 +193,9 @@ Start `ima2 serve`, then check `~/.ima2/server.json`. You can also run `ima2 pin
193
193
  **OAuth login does not work**
194
194
  Run `npx @openai/codex login`, confirm `ima2 status`, then restart `ima2 serve`.
195
195
 
196
+ **`fetch failed` repeats on a proxy/VPN network**
197
+ Check that the local OAuth proxy is reachable. On networks that require a proxy, enable your proxy client's TUN/TURN-style mode, then retry `npx openai-oauth --port 10531`. If it still fails, set `HTTP_PROXY` and `HTTPS_PROXY` in the same terminal that runs `ima2 serve` or `openai-oauth`.
198
+
196
199
  **Images fail with `APIKEY_DISABLED`**
197
200
  Use OAuth for generation. API-key image generation is intentionally disabled in this build.
198
201
 
package/config.js CHANGED
@@ -50,6 +50,10 @@ function pickInt(envVal, fileVal, fallback) {
50
50
  const n = Number(candidate);
51
51
  return Number.isFinite(n) ? n : fallback;
52
52
  }
53
+ function pickPositiveInt(envVal, fileVal, fallback) {
54
+ const n = pickInt(envVal, fileVal, fallback);
55
+ return Number.isFinite(n) && n > 0 ? n : fallback;
56
+ }
53
57
  function pickStr(envVal, fileVal, fallback) {
54
58
  return firstDefined(envVal, fileVal) ?? fallback;
55
59
  }
@@ -83,6 +87,41 @@ export const config = {
83
87
  maxParallel: pickInt(env.IMA2_MAX_PARALLEL, fileCfg.limits?.maxParallel, 8),
84
88
  graphMaxNodes: pickInt(env.IMA2_GRAPH_MAX_NODES, fileCfg.limits?.graphMaxNodes, 500),
85
89
  graphMaxEdges: pickInt(env.IMA2_GRAPH_MAX_EDGES, fileCfg.limits?.graphMaxEdges, 1000),
90
+ promptImportMaxFileBytes: pickInt(
91
+ env.IMA2_PROMPT_IMPORT_MAX_FILE_BYTES,
92
+ fileCfg.limits?.promptImportMaxFileBytes,
93
+ 512 * 1024,
94
+ ),
95
+ promptImportMaxCandidatesPerFile: pickInt(
96
+ env.IMA2_PROMPT_IMPORT_MAX_CANDIDATES_PER_FILE,
97
+ fileCfg.limits?.promptImportMaxCandidatesPerFile,
98
+ 100,
99
+ ),
100
+ promptImportMaxCandidatesPerImport: pickInt(
101
+ env.IMA2_PROMPT_IMPORT_MAX_CANDIDATES_PER_IMPORT,
102
+ fileCfg.limits?.promptImportMaxCandidatesPerImport,
103
+ 100,
104
+ ),
105
+ promptImportFetchTimeoutMs: pickInt(
106
+ env.IMA2_PROMPT_IMPORT_FETCH_TIMEOUT_MS,
107
+ fileCfg.limits?.promptImportFetchTimeoutMs,
108
+ 8000,
109
+ ),
110
+ promptImportMaxCandidateChars: pickInt(
111
+ env.IMA2_PROMPT_IMPORT_MAX_CANDIDATE_CHARS,
112
+ fileCfg.limits?.promptImportMaxCandidateChars,
113
+ 12000,
114
+ ),
115
+ promptImportMinCandidateChars: pickInt(
116
+ env.IMA2_PROMPT_IMPORT_MIN_CANDIDATE_CHARS,
117
+ fileCfg.limits?.promptImportMinCandidateChars,
118
+ 40,
119
+ ),
120
+ promptImportMaxSourceCharsScanned: pickInt(
121
+ env.IMA2_PROMPT_IMPORT_MAX_SOURCE_CHARS_SCANNED,
122
+ fileCfg.limits?.promptImportMaxSourceCharsScanned,
123
+ 512 * 1024,
124
+ ),
86
125
  },
87
126
  history: {
88
127
  defaultPageSize: pickInt(
@@ -146,6 +185,12 @@ export const config = {
146
185
  default: pickStr(env.IMA2_IMAGE_MODEL_DEFAULT, fileCfg.imageModels?.default, "gpt-5.4-mini"),
147
186
  valid: new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]),
148
187
  unsupported: new Set(["gpt-5.3-codex-spark"]),
188
+ reasoningEffort: pickStr(
189
+ env.IMA2_REASONING_EFFORT,
190
+ fileCfg.imageModels?.reasoningEffort,
191
+ "medium",
192
+ ),
193
+ validReasoningEfforts: new Set(["low", "medium", "high", "xhigh"]),
149
194
  },
150
195
  log: {
151
196
  level: pickStr(env.IMA2_LOG_LEVEL, fileCfg.log?.level, defaultLogLevelForEnv(env)),
@@ -164,6 +209,19 @@ export const config = {
164
209
  false,
165
210
  ),
166
211
  },
212
+ comfy: {
213
+ defaultUrl: pickStr(env.IMA2_COMFY_URL, fileCfg.comfy?.defaultUrl, "http://127.0.0.1:8188"),
214
+ uploadTimeoutMs: pickPositiveInt(
215
+ env.IMA2_COMFY_UPLOAD_TIMEOUT_MS,
216
+ fileCfg.comfy?.uploadTimeoutMs,
217
+ 30_000,
218
+ ),
219
+ maxUploadBytes: pickPositiveInt(
220
+ env.IMA2_COMFY_MAX_UPLOAD_BYTES,
221
+ fileCfg.comfy?.maxUploadBytes,
222
+ 50 * 1024 * 1024,
223
+ ),
224
+ },
167
225
  dev: {
168
226
  viteDevMode: pickBool(env.VITE_IMA2_DEV, fileCfg.dev?.viteDevMode, false),
169
227
  },
package/docs/FAQ.ko.md CHANGED
@@ -17,6 +17,7 @@ English version: [FAQ.md](FAQ.md)
17
17
  | `gpt-5.5`만 실패함 | Codex CLI를 업데이트하고, 안정 대안으로 `gpt-5.4`를 사용합니다. |
18
18
  | 레퍼런스 업로드 실패 | JPEG/PNG로 변환하고 해상도를 낮춰 보세요. 레퍼런스는 최대 5장입니다. |
19
19
  | Windows에서 `10531` 포트 관련 OAuth/proxy 오류가 남 | `ima2 doctor`를 실행하고, 필요하면 `IMA2_OAUTH_PROXY_PORT=11531 ima2 serve`로 시작하세요. |
20
+ | 프록시/VPN 환경에서 `fetch failed`가 반복됨 | 프록시 클라이언트의 TUN/TURN류 모드를 켜거나, 같은 터미널에 `HTTP_PROXY` / `HTTPS_PROXY`를 설정하세요. |
20
21
 
21
22
  ## 설치와 업데이트
22
23
 
@@ -224,6 +225,25 @@ ima2 ping
224
225
 
225
226
  필요하면 `ima2 serve`를 다시 시작합니다.
226
227
 
228
+ ### 프록시나 VPN 뒤에서 `fetch failed`가 계속 나면 어떻게 하나요?
229
+
230
+ 대부분 로컬 OAuth 프록시가 현재 네트워크 경로로 upstream 서비스에 닿지 못하는 상황입니다. `openai-oauth`는 보통 `10531` 포트의 localhost 프록시로 실행됩니다.
231
+
232
+ 먼저 시도하세요.
233
+
234
+ ```bash
235
+ npx openai-oauth --port 10531
236
+ ```
237
+
238
+ 프록시가 필요한 네트워크라면 터미널 프로세스도 프록시를 타도록 프록시 클라이언트의 TUN/TURN류 모드를 켜세요. 그래도 안 되면 `openai-oauth` 또는 `ima2 serve`를 실행하는 같은 터미널에 프록시 환경 변수를 설정합니다.
239
+
240
+ ```bash
241
+ export HTTP_PROXY=http://127.0.0.1:7890
242
+ export HTTPS_PROXY=http://127.0.0.1:7890
243
+ ```
244
+
245
+ 호스트와 포트는 사용하는 프록시 클라이언트 값에 맞춰 바꾸세요. 로컬 OAuth 프록시가 접근 가능한 상태에서도 `ima2-gen`이 계속 실패하면, 새 이슈를 열 때 실행 명령, OS, 프록시 설정, 터미널 오류를 함께 남겨 주세요.
246
+
227
247
  ### 회사 컴퓨터에서는 무엇을 확인해야 하나요?
228
248
 
229
249
  OAuth는 OpenAI와 ChatGPT/Codex 관련 호스트 접근이 필요할 수 있습니다. 회사 방화벽, TLS 검사, VPN, 프록시가 흐름을 깨뜨릴 수 있습니다. 로그인 실패와 `failed to fetch`가 반복되면 다른 네트워크에서도 시도해 보세요.
package/docs/FAQ.md CHANGED
@@ -17,6 +17,7 @@ For Korean, see [FAQ.ko.md](FAQ.ko.md).
17
17
  | `gpt-5.5` fails | Update Codex CLI first, then try `gpt-5.4` as the stable fallback. |
18
18
  | Reference upload fails | Use JPEG/PNG, lower the resolution, and keep references to 5 images or fewer. |
19
19
  | Windows reports OAuth/proxy failures around port `10531` | Run `ima2 doctor`; if needed start with `IMA2_OAUTH_PROXY_PORT=11531 ima2 serve`. |
20
+ | `fetch failed` repeats on a proxy/VPN network | Enable proxy TUN/TURN-style mode, or set `HTTP_PROXY` / `HTTPS_PROXY` in the same terminal. |
20
21
 
21
22
  ## Install and update
22
23
 
@@ -232,6 +233,25 @@ ima2 ping
232
233
 
233
234
  Then restart `ima2 serve` if needed.
234
235
 
236
+ ### What if `fetch failed` keeps happening behind a proxy or VPN?
237
+
238
+ This usually means the local OAuth proxy cannot reach the upstream service through your network path. `openai-oauth` runs as a local localhost proxy, commonly on port `10531`.
239
+
240
+ Try:
241
+
242
+ ```bash
243
+ npx openai-oauth --port 10531
244
+ ```
245
+
246
+ If your network requires a proxy, enable your proxy client's TUN/TURN-style mode so terminal processes can use it. If that is not enough, set the proxy variables in the same terminal that runs `openai-oauth` or `ima2 serve`:
247
+
248
+ ```bash
249
+ export HTTP_PROXY=http://127.0.0.1:7890
250
+ export HTTPS_PROXY=http://127.0.0.1:7890
251
+ ```
252
+
253
+ Use the host and port from your proxy client. If `ima2-gen` still fails after the local OAuth proxy is reachable, collect the exact command, OS, proxy setup, and terminal error before opening a new issue.
254
+
235
255
  ### What should I check on a company computer?
236
256
 
237
257
  OAuth may require access to OpenAI and ChatGPT/Codex-related hosts. A corporate firewall, TLS inspection, VPN, or proxy can break the flow. Try a different network if login and `failed to fetch` errors keep repeating.
package/docs/README.ko.md CHANGED
@@ -160,6 +160,9 @@ environment variables > ~/.ima2/config.json > built-in defaults
160
160
  **OAuth 로그인이 안 돼요**
161
161
  `npx @openai/codex login`을 실행하고, `ima2 status`를 확인한 뒤 `ima2 serve`를 다시 시작하세요.
162
162
 
163
+ **프록시/VPN 환경에서 `fetch failed`가 반복돼요**
164
+ 로컬 OAuth 프록시가 접근 가능한지 확인하세요. 프록시가 필요한 네트워크라면 프록시 클라이언트의 TUN/TURN류 모드를 켠 뒤 `npx openai-oauth --port 10531`을 다시 시도하세요. 그래도 실패하면 `ima2 serve` 또는 `openai-oauth`를 실행하는 같은 터미널에 `HTTP_PROXY`와 `HTTPS_PROXY`를 설정하세요.
165
+
163
166
  **이미지 생성이 `APIKEY_DISABLED`로 실패해요**
164
167
  현재 빌드에서는 OAuth로 생성해야 합니다. API-key 이미지 생성은 의도적으로 비활성화되어 있습니다.
165
168
 
@@ -153,6 +153,9 @@ environment variables > ~/.ima2/config.json > built-in defaults
153
153
  **OAuth 登录失败**
154
154
  运行 `npx @openai/codex login`,用 `ima2 status` 确认状态,然后重启 `ima2 serve`。
155
155
 
156
+ **在代理/VPN 网络下反复出现 `fetch failed`**
157
+ 请先确认本地 OAuth proxy 可以访问。如果你的网络需要代理,请在代理客户端里开启 TUN/TURN 类似的转发模式,然后重试 `npx openai-oauth --port 10531`。如果仍然失败,请在运行 `ima2 serve` 或 `openai-oauth` 的同一个终端里设置 `HTTP_PROXY` 和 `HTTPS_PROXY`。
158
+
156
159
  **生成图片时返回 `APIKEY_DISABLED`**
157
160
  当前 build 需要使用 OAuth 生成。API-key image generation 是有意关闭的。
158
161
 
@@ -0,0 +1,88 @@
1
+ # ima2-gen ComfyUI Bridge
2
+
3
+ This custom node lets ComfyUI call a running local `ima2-gen` server and return
4
+ the generated image as a ComfyUI `IMAGE`.
5
+
6
+ ## Install
7
+
8
+ Copy or symlink this folder into your ComfyUI custom nodes directory:
9
+
10
+ ```bash
11
+ ComfyUI/custom_nodes/ima2_gen_bridge
12
+ ```
13
+
14
+ For development from this repository:
15
+
16
+ ```bash
17
+ ln -s /path/to/ima2-gen/integrations/comfyui/ima2_gen_bridge \
18
+ /path/to/ComfyUI/custom_nodes/ima2_gen_bridge
19
+ ```
20
+
21
+ Restart ComfyUI after installing.
22
+
23
+ ## Prerequisite
24
+
25
+ Start the ima2 server before queueing the node:
26
+
27
+ ```bash
28
+ ima2 serve
29
+ ```
30
+
31
+ The node never asks for OpenAI credentials. It only calls the local ima2 HTTP
32
+ server you already started.
33
+
34
+ ## Node
35
+
36
+ Add the node from:
37
+
38
+ ```text
39
+ Add Node -> ima2-gen -> Ima2 Generate
40
+ ```
41
+
42
+ Inputs:
43
+
44
+ | Input | Description |
45
+ | --- | --- |
46
+ | `prompt` | Text prompt sent to ima2. |
47
+ | `server_url` | Optional loopback ima2 server origin. Leave empty for auto-discovery. |
48
+ | `quality` | `low`, `medium`, or `high`. |
49
+ | `size` | Image size string such as `1024x1024`. |
50
+ | `moderation` | `low` or `auto`. |
51
+ | `timeout` | Request timeout in seconds. |
52
+ | `model` | Optional ima2 image model override. |
53
+ | `mode` | `auto` or `direct` prompt mode. |
54
+ | `web_search` | Maps to ima2 `webSearchEnabled`. |
55
+
56
+ Outputs:
57
+
58
+ | Output | Description |
59
+ | --- | --- |
60
+ | `image` | Generated ComfyUI `IMAGE`. |
61
+ | `filename` | Saved ima2 generated filename. |
62
+ | `metadata` | JSON metadata string with request details. |
63
+
64
+ ## Server Discovery
65
+
66
+ When `server_url` is empty, the node checks:
67
+
68
+ 1. `IMA2_SERVER`
69
+ 2. `IMA2_ADVERTISE_FILE`
70
+ 3. `IMA2_CONFIG_DIR/server.json`
71
+ 4. `~/.ima2/server.json`
72
+ 5. `http://127.0.0.1:3333`
73
+
74
+ Only loopback HTTP origins are accepted:
75
+
76
+ ```text
77
+ http://127.0.0.1:3333
78
+ http://localhost:3333
79
+ http://[::1]:3333
80
+ ```
81
+
82
+ Remote hosts, credentials, paths, queries, fragments, and HTTPS URLs are
83
+ rejected.
84
+
85
+ ## Scope
86
+
87
+ This PR2 node is text-to-image only. Image input, edit, reference generation,
88
+ and workflow automation are later follow-ups.
@@ -0,0 +1,3 @@
1
+ from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
2
+
3
+ __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
@@ -0,0 +1,238 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ import os
6
+ from io import BytesIO
7
+ from pathlib import Path
8
+ from urllib.error import HTTPError, URLError
9
+ from urllib.parse import urlparse
10
+ from urllib.request import Request, urlopen
11
+
12
+ import numpy as np
13
+ import torch
14
+ from PIL import Image
15
+
16
+
17
+ DEFAULT_SERVER_URL = "http://127.0.0.1:3333"
18
+ IMA2_CLIENT_HEADER = "comfyui/bridge"
19
+ ALLOWED_LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"}
20
+ MODEL_OPTIONS = ["", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]
21
+
22
+
23
+ class Ima2BridgeError(Exception):
24
+ pass
25
+
26
+
27
+ def _normalize_loopback_url(raw):
28
+ value = str(raw or "").strip()
29
+ if not value:
30
+ raise Ima2BridgeError("ima2 server URL is required")
31
+
32
+ parsed = urlparse(value)
33
+ if parsed.scheme != "http":
34
+ raise Ima2BridgeError("ima2 server URL must use http")
35
+ if parsed.username or parsed.password:
36
+ raise Ima2BridgeError("ima2 server URL must not include credentials")
37
+ if parsed.path not in ("", "/") or parsed.params or parsed.query or parsed.fragment:
38
+ raise Ima2BridgeError("ima2 server URL must be an origin only")
39
+
40
+ hostname = (parsed.hostname or "").lower()
41
+ if hostname not in ALLOWED_LOOPBACK_HOSTS:
42
+ raise Ima2BridgeError("ima2 server URL must point to a loopback host")
43
+
44
+ try:
45
+ port = parsed.port
46
+ except ValueError as exc:
47
+ raise Ima2BridgeError("ima2 server URL port is invalid") from exc
48
+ if port is None:
49
+ raise Ima2BridgeError("ima2 server URL must include a port")
50
+
51
+ host = f"[{hostname}]" if hostname == "::1" else hostname
52
+ return f"http://{host}:{port}"
53
+
54
+
55
+ def _read_json_file(path):
56
+ try:
57
+ if not path or not Path(path).is_file():
58
+ return None
59
+ return json.loads(Path(path).read_text(encoding="utf-8"))
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ def _read_advertise_file():
65
+ explicit = os.environ.get("IMA2_ADVERTISE_FILE")
66
+ if explicit:
67
+ data = _read_json_file(explicit)
68
+ if data:
69
+ return data
70
+
71
+ config_dir = os.environ.get("IMA2_CONFIG_DIR")
72
+ candidates = []
73
+ if config_dir:
74
+ candidates.append(Path(config_dir) / "server.json")
75
+ candidates.append(Path.home() / ".ima2" / "server.json")
76
+
77
+ for path in candidates:
78
+ data = _read_json_file(path)
79
+ if data:
80
+ return data
81
+ return None
82
+
83
+
84
+ def _candidate_server_urls(server_url):
85
+ if str(server_url or "").strip():
86
+ return [server_url]
87
+
88
+ candidates = []
89
+ env_url = os.environ.get("IMA2_SERVER")
90
+ if env_url:
91
+ candidates.append(env_url)
92
+
93
+ advertised = _read_advertise_file()
94
+ if isinstance(advertised, dict):
95
+ backend = advertised.get("backend")
96
+ if isinstance(backend, dict) and backend.get("url"):
97
+ candidates.append(backend["url"])
98
+ if advertised.get("url"):
99
+ candidates.append(advertised["url"])
100
+ if advertised.get("port"):
101
+ candidates.append(f"http://127.0.0.1:{advertised['port']}")
102
+
103
+ candidates.append(DEFAULT_SERVER_URL)
104
+ return candidates
105
+
106
+
107
+ def _resolve_server_url(server_url=""):
108
+ candidates = _candidate_server_urls(server_url)
109
+ if str(server_url or "").strip():
110
+ return _normalize_loopback_url(candidates[0])
111
+
112
+ for candidate in candidates:
113
+ try:
114
+ return _normalize_loopback_url(candidate)
115
+ except Ima2BridgeError:
116
+ continue
117
+ raise Ima2BridgeError("No valid local ima2 server URL was found")
118
+
119
+
120
+ def _post_generate(base_url, payload, timeout):
121
+ body = json.dumps(payload).encode("utf-8")
122
+ request = Request(
123
+ f"{base_url}/api/generate",
124
+ data=body,
125
+ method="POST",
126
+ headers={
127
+ "Content-Type": "application/json",
128
+ "X-ima2-client": IMA2_CLIENT_HEADER,
129
+ },
130
+ )
131
+ try:
132
+ with urlopen(request, timeout=timeout) as response:
133
+ text = response.read().decode("utf-8")
134
+ except HTTPError as exc:
135
+ text = exc.read().decode("utf-8", errors="replace")
136
+ raise Ima2BridgeError(f"ima2 server returned HTTP {exc.code}: {text}") from exc
137
+ except URLError as exc:
138
+ raise Ima2BridgeError(f"ima2 server is unreachable: {exc.reason}") from exc
139
+
140
+ try:
141
+ return json.loads(text)
142
+ except json.JSONDecodeError as exc:
143
+ raise Ima2BridgeError("ima2 server returned invalid JSON") from exc
144
+
145
+
146
+ def _decode_data_url_to_tensor(data_url):
147
+ if not isinstance(data_url, str) or "," not in data_url:
148
+ raise Ima2BridgeError("ima2 response did not include an image data URL")
149
+ header, encoded = data_url.split(",", 1)
150
+ if not header.startswith("data:image/") or ";base64" not in header:
151
+ raise Ima2BridgeError("ima2 response image must be a base64 image data URL")
152
+
153
+ try:
154
+ raw = base64.b64decode(encoded, validate=True)
155
+ except Exception as exc:
156
+ raise Ima2BridgeError("ima2 response image base64 is invalid") from exc
157
+
158
+ image = Image.open(BytesIO(raw)).convert("RGB")
159
+ array = np.asarray(image).astype(np.float32) / 255.0
160
+ return torch.from_numpy(array)[None,]
161
+
162
+
163
+ class Ima2Generate:
164
+ CATEGORY = "ima2-gen"
165
+ RETURN_TYPES = ("IMAGE", "STRING", "STRING")
166
+ RETURN_NAMES = ("image", "filename", "metadata")
167
+ FUNCTION = "generate"
168
+
169
+ @classmethod
170
+ def INPUT_TYPES(cls):
171
+ return {
172
+ "required": {
173
+ "prompt": ("STRING", {"multiline": True, "default": ""}),
174
+ "server_url": ("STRING", {"default": ""}),
175
+ "quality": (["low", "medium", "high"], {"default": "medium"}),
176
+ "size": ("STRING", {"default": "1024x1024"}),
177
+ "moderation": (["low", "auto"], {"default": "low"}),
178
+ "timeout": ("INT", {"default": 180, "min": 5, "max": 600}),
179
+ },
180
+ "optional": {
181
+ "model": (MODEL_OPTIONS,),
182
+ "mode": (["auto", "direct"], {"default": "auto"}),
183
+ "web_search": ("BOOLEAN", {"default": True}),
184
+ },
185
+ }
186
+
187
+ def generate(
188
+ self,
189
+ prompt,
190
+ server_url="",
191
+ quality="medium",
192
+ size="1024x1024",
193
+ moderation="low",
194
+ timeout=180,
195
+ model="",
196
+ mode="auto",
197
+ web_search=True,
198
+ ):
199
+ clean_prompt = str(prompt or "").strip()
200
+ if not clean_prompt:
201
+ raise Ima2BridgeError("prompt is required")
202
+
203
+ base_url = _resolve_server_url(server_url)
204
+ payload = {
205
+ "prompt": clean_prompt,
206
+ "quality": quality,
207
+ "size": size,
208
+ "n": 1,
209
+ "format": "png",
210
+ "moderation": moderation,
211
+ "mode": mode,
212
+ "webSearchEnabled": bool(web_search),
213
+ }
214
+ if model:
215
+ payload["model"] = model
216
+
217
+ response = _post_generate(base_url, payload, int(timeout))
218
+ image_data = response.get("image")
219
+ filename = response.get("filename") or ""
220
+ image = _decode_data_url_to_tensor(image_data)
221
+ metadata = {
222
+ "filename": filename,
223
+ "requestId": response.get("requestId"),
224
+ "elapsed": response.get("elapsed"),
225
+ "serverUrl": base_url,
226
+ "model": response.get("model") or model or None,
227
+ "size": response.get("size") or size,
228
+ }
229
+ return (image, filename, json.dumps(metadata, ensure_ascii=False, sort_keys=True))
230
+
231
+
232
+ NODE_CLASS_MAPPINGS = {
233
+ "Ima2Generate": Ima2Generate,
234
+ }
235
+
236
+ NODE_DISPLAY_NAME_MAPPINGS = {
237
+ "Ima2Generate": "Ima2 Generate",
238
+ }