ima2-gen 1.1.6 → 1.1.8
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.
- package/.env.example +5 -0
- package/README.md +3 -0
- package/assets/phase-a-bg-cleanup-test.png +0 -0
- package/config.js +111 -0
- package/docs/FAQ.ko.md +20 -0
- package/docs/FAQ.md +20 -0
- package/docs/README.ko.md +3 -0
- package/docs/README.zh-CN.md +3 -0
- package/integrations/comfyui/ima2_gen_bridge/README.md +88 -0
- package/integrations/comfyui/ima2_gen_bridge/__init__.py +3 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/nodes.py +238 -0
- package/lib/canvasVersionStore.js +181 -0
- package/lib/cardNewsPlannerClient.js +4 -2
- package/lib/comfyBridge.js +214 -0
- package/lib/db.js +14 -0
- package/lib/historyList.js +4 -0
- package/lib/imageMetadata.js +4 -0
- package/lib/imageModels.js +20 -0
- package/lib/localImportStore.js +111 -0
- package/lib/oauthProxy.js +88 -38
- package/lib/pngInfo.js +26 -0
- package/lib/promptImport/curatedSources.js +139 -0
- package/lib/promptImport/discoveryRegistry.js +236 -0
- package/lib/promptImport/errors.js +16 -0
- package/lib/promptImport/githubDiscovery.js +248 -0
- package/lib/promptImport/githubFolder.js +308 -0
- package/lib/promptImport/githubSource.js +239 -0
- package/lib/promptImport/gptImageHints.js +68 -0
- package/lib/promptImport/parsePromptCandidates.js +153 -0
- package/lib/promptImport/promptIndex.js +248 -0
- package/lib/promptImport/rankPromptCandidates.js +49 -0
- package/package.json +3 -2
- package/routes/annotations.js +95 -0
- package/routes/canvasVersions.js +64 -0
- package/routes/comfy.js +39 -0
- package/routes/edit.js +73 -4
- package/routes/generate.js +16 -2
- package/routes/imageImport.js +33 -0
- package/routes/index.js +10 -0
- package/routes/multimode.js +18 -1
- package/routes/nodes.js +25 -3
- package/routes/promptImport.js +354 -0
- package/ui/dist/assets/index-BDffwmLs.css +1 -0
- package/ui/dist/assets/index-D0fdHLkJ.js +31 -0
- package/ui/dist/assets/index-D0fdHLkJ.js.map +1 -0
- package/ui/dist/index.html +6 -3
- package/ui/dist/assets/index-3X-6VjbF.css +0 -1
- package/ui/dist/assets/index-DPSq9qEs.js +0 -31
- 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
|
|
|
Binary file
|
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,81 @@ 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
|
+
),
|
|
125
|
+
promptImportMaxRepoIndexFiles: pickInt(
|
|
126
|
+
env.IMA2_PROMPT_IMPORT_MAX_REPO_INDEX_FILES,
|
|
127
|
+
fileCfg.limits?.promptImportMaxRepoIndexFiles,
|
|
128
|
+
500,
|
|
129
|
+
),
|
|
130
|
+
promptImportCuratedSearchLimit: pickInt(
|
|
131
|
+
env.IMA2_PROMPT_IMPORT_CURATED_SEARCH_LIMIT,
|
|
132
|
+
fileCfg.limits?.promptImportCuratedSearchLimit,
|
|
133
|
+
50,
|
|
134
|
+
),
|
|
135
|
+
promptImportIndexCacheTtlMs: pickInt(
|
|
136
|
+
env.IMA2_PROMPT_IMPORT_INDEX_CACHE_TTL_MS,
|
|
137
|
+
fileCfg.limits?.promptImportIndexCacheTtlMs,
|
|
138
|
+
24 * 60 * 60 * 1000,
|
|
139
|
+
),
|
|
140
|
+
promptImportMaxFolderFiles: pickInt(
|
|
141
|
+
env.IMA2_PROMPT_IMPORT_MAX_FOLDER_FILES,
|
|
142
|
+
fileCfg.limits?.promptImportMaxFolderFiles,
|
|
143
|
+
100,
|
|
144
|
+
),
|
|
145
|
+
promptImportMaxFolderPreviewFiles: pickInt(
|
|
146
|
+
env.IMA2_PROMPT_IMPORT_MAX_FOLDER_PREVIEW_FILES,
|
|
147
|
+
fileCfg.limits?.promptImportMaxFolderPreviewFiles,
|
|
148
|
+
20,
|
|
149
|
+
),
|
|
150
|
+
promptImportDiscoverySearchLimit: pickInt(
|
|
151
|
+
env.IMA2_PROMPT_IMPORT_DISCOVERY_SEARCH_LIMIT,
|
|
152
|
+
fileCfg.limits?.promptImportDiscoverySearchLimit,
|
|
153
|
+
20,
|
|
154
|
+
),
|
|
155
|
+
promptImportDiscoveryCacheTtlMs: pickInt(
|
|
156
|
+
env.IMA2_PROMPT_IMPORT_DISCOVERY_CACHE_TTL_MS,
|
|
157
|
+
fileCfg.limits?.promptImportDiscoveryCacheTtlMs,
|
|
158
|
+
60 * 60 * 1000,
|
|
159
|
+
),
|
|
160
|
+
promptImportDiscoveryMaxQueries: pickInt(
|
|
161
|
+
env.IMA2_PROMPT_IMPORT_DISCOVERY_MAX_QUERIES,
|
|
162
|
+
fileCfg.limits?.promptImportDiscoveryMaxQueries,
|
|
163
|
+
5,
|
|
164
|
+
),
|
|
86
165
|
},
|
|
87
166
|
history: {
|
|
88
167
|
defaultPageSize: pickInt(
|
|
@@ -114,6 +193,9 @@ export const config = {
|
|
|
114
193
|
: ["auto", "low"],
|
|
115
194
|
),
|
|
116
195
|
},
|
|
196
|
+
github: {
|
|
197
|
+
token: pickStr(env.IMA2_GITHUB_TOKEN, fileCfg.github?.token, ""),
|
|
198
|
+
},
|
|
117
199
|
storage: {
|
|
118
200
|
configDir,
|
|
119
201
|
packageRoot,
|
|
@@ -122,6 +204,16 @@ export const config = {
|
|
|
122
204
|
generatedDirName: pickStr(env.IMA2_GENERATED_DIRNAME, fileCfg.storage?.generatedDirName, "generated"),
|
|
123
205
|
trashDirName: pickStr(env.IMA2_TRASH_DIRNAME, fileCfg.storage?.trashDirName, ".trash"),
|
|
124
206
|
dbPath: pickStr(env.IMA2_DB_PATH, fileCfg.storage?.dbPath, join(configDir, "sessions.db")),
|
|
207
|
+
promptImportIndexCacheFile: pickStr(
|
|
208
|
+
env.IMA2_PROMPT_IMPORT_INDEX_CACHE_FILE,
|
|
209
|
+
fileCfg.storage?.promptImportIndexCacheFile,
|
|
210
|
+
join(configDir, "prompt-import-index.json"),
|
|
211
|
+
),
|
|
212
|
+
promptImportDiscoveryRegistryFile: pickStr(
|
|
213
|
+
env.IMA2_PROMPT_IMPORT_DISCOVERY_REGISTRY_FILE,
|
|
214
|
+
fileCfg.storage?.promptImportDiscoveryRegistryFile,
|
|
215
|
+
join(configDir, "prompt-import-discovery.json"),
|
|
216
|
+
),
|
|
125
217
|
configFile: join(configDir, "config.json"),
|
|
126
218
|
advertiseFile: pickStr(env.IMA2_ADVERTISE_FILE, fileCfg.storage?.advertiseFile, join(configDir, "server.json")),
|
|
127
219
|
staticMaxAge: pickStr(env.IMA2_STATIC_MAX_AGE, fileCfg.storage?.staticMaxAge, "1y"),
|
|
@@ -146,6 +238,12 @@ export const config = {
|
|
|
146
238
|
default: pickStr(env.IMA2_IMAGE_MODEL_DEFAULT, fileCfg.imageModels?.default, "gpt-5.4-mini"),
|
|
147
239
|
valid: new Set(["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"]),
|
|
148
240
|
unsupported: new Set(["gpt-5.3-codex-spark"]),
|
|
241
|
+
reasoningEffort: pickStr(
|
|
242
|
+
env.IMA2_REASONING_EFFORT,
|
|
243
|
+
fileCfg.imageModels?.reasoningEffort,
|
|
244
|
+
"medium",
|
|
245
|
+
),
|
|
246
|
+
validReasoningEfforts: new Set(["low", "medium", "high", "xhigh"]),
|
|
149
247
|
},
|
|
150
248
|
log: {
|
|
151
249
|
level: pickStr(env.IMA2_LOG_LEVEL, fileCfg.log?.level, defaultLogLevelForEnv(env)),
|
|
@@ -164,6 +262,19 @@ export const config = {
|
|
|
164
262
|
false,
|
|
165
263
|
),
|
|
166
264
|
},
|
|
265
|
+
comfy: {
|
|
266
|
+
defaultUrl: pickStr(env.IMA2_COMFY_URL, fileCfg.comfy?.defaultUrl, "http://127.0.0.1:8188"),
|
|
267
|
+
uploadTimeoutMs: pickPositiveInt(
|
|
268
|
+
env.IMA2_COMFY_UPLOAD_TIMEOUT_MS,
|
|
269
|
+
fileCfg.comfy?.uploadTimeoutMs,
|
|
270
|
+
30_000,
|
|
271
|
+
),
|
|
272
|
+
maxUploadBytes: pickPositiveInt(
|
|
273
|
+
env.IMA2_COMFY_MAX_UPLOAD_BYTES,
|
|
274
|
+
fileCfg.comfy?.maxUploadBytes,
|
|
275
|
+
50 * 1024 * 1024,
|
|
276
|
+
),
|
|
277
|
+
},
|
|
167
278
|
dev: {
|
|
168
279
|
viteDevMode: pickBool(env.VITE_IMA2_DEV, fileCfg.dev?.viteDevMode, false),
|
|
169
280
|
},
|
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
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -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,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
|
+
}
|