ima2-gen 1.1.21 → 1.1.23
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/README.md +44 -7
- package/bin/commands/video.js +14 -0
- package/bin/ima2.js +14 -4
- package/bin/lib/platform.js +34 -5
- package/docs/README.ko.md +43 -2
- package/lib/agentQueueWorker.js +6 -0
- package/lib/agentRuntime.js +3 -2
- package/lib/atomicWrite.js +14 -0
- package/lib/grokImageAdapter.js +6 -0
- package/lib/grokProxyLauncher.js +5 -3
- package/lib/grokVideoAdapter.js +1 -1
- package/lib/grokVideoPlannerPrompt.js +10 -0
- package/lib/inflight.js +1 -1
- package/lib/oauthLauncher.js +5 -0
- package/lib/videoFrameExtract.js +3 -3
- package/package.json +5 -7
- package/routes/capabilities.js +13 -0
- package/routes/edit.js +2 -1
- package/routes/generate.js +32 -6
- package/routes/health.js +4 -3
- package/routes/multimode.js +2 -1
- package/routes/video.js +35 -3
- package/server.js +29 -2
- package/skills/ima2/SKILL.md +48 -6
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-C21zqdTZ.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-BN-ga1lG.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BbMa4IhI.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DRwBJRDQ.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-Dp85kHCq.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BE8Q8MLD.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-PtH5x0sc.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-FnM9tHI9.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +1 -0
- package/ui/dist/assets/index-BAFI6htx.js +42 -0
- package/ui/dist/assets/{index-31uVIdt4.js → index-BSXxr_Bt.js} +1 -1
- package/ui/dist/assets/index-DS-ADE7U.css +1 -0
- package/ui/dist/index.html +2 -2
- package/bin/commands/annotate.ts +0 -119
- package/bin/commands/cancel.ts +0 -48
- package/bin/commands/canvas-versions.ts +0 -80
- package/bin/commands/capabilities.ts +0 -110
- package/bin/commands/cardnews.ts +0 -249
- package/bin/commands/comfy.ts +0 -54
- package/bin/commands/config.ts +0 -186
- package/bin/commands/defaults.ts +0 -192
- package/bin/commands/doctor.ts +0 -202
- package/bin/commands/edit.ts +0 -150
- package/bin/commands/gen.ts +0 -214
- package/bin/commands/grok.ts +0 -90
- package/bin/commands/history.ts +0 -146
- package/bin/commands/ls.ts +0 -64
- package/bin/commands/metadata.ts +0 -39
- package/bin/commands/multimode.ts +0 -196
- package/bin/commands/node.ts +0 -166
- package/bin/commands/observability.ts +0 -176
- package/bin/commands/ping.ts +0 -31
- package/bin/commands/prompt-sub/build.ts +0 -101
- package/bin/commands/prompt.ts +0 -492
- package/bin/commands/ps.ts +0 -81
- package/bin/commands/session.ts +0 -266
- package/bin/commands/show.ts +0 -72
- package/bin/commands/skill.ts +0 -70
- package/bin/commands/video.ts +0 -442
- package/bin/ima2.ts +0 -430
- package/bin/lib/args.ts +0 -92
- package/bin/lib/browser-id.ts +0 -16
- package/bin/lib/client.ts +0 -122
- package/bin/lib/config-store.ts +0 -120
- package/bin/lib/destructive-confirm.ts +0 -19
- package/bin/lib/doctor-checks.ts +0 -91
- package/bin/lib/error-hints.ts +0 -23
- package/bin/lib/files.ts +0 -39
- package/bin/lib/output.ts +0 -73
- package/bin/lib/platform.ts +0 -99
- package/bin/lib/recover-output.ts +0 -139
- package/bin/lib/sse.ts +0 -73
- package/bin/lib/star-prompt.ts +0 -97
- package/bin/lib/storage-doctor.ts +0 -39
- package/bin/lib/ui-build.ts +0 -85
- package/config.ts +0 -354
- package/lib/agentCommandParser.ts +0 -69
- package/lib/agentGenerationPlanner.ts +0 -273
- package/lib/agentQuestionResponder.ts +0 -266
- package/lib/agentQueueStore.ts +0 -270
- package/lib/agentQueueWorker.ts +0 -89
- package/lib/agentRuntime.ts +0 -604
- package/lib/agentSettings.ts +0 -72
- package/lib/agentStore.ts +0 -422
- package/lib/agentStoreRows.ts +0 -136
- package/lib/agentTypes.ts +0 -154
- package/lib/apiCachePolicy.ts +0 -11
- package/lib/assetLifecycle.ts +0 -146
- package/lib/canvasVersionStore.ts +0 -223
- package/lib/capabilities.ts +0 -126
- package/lib/cardNewsGenerator.ts +0 -271
- package/lib/cardNewsJobStore.ts +0 -142
- package/lib/cardNewsManifestStore.ts +0 -154
- package/lib/cardNewsPlanner.ts +0 -236
- package/lib/cardNewsPlannerClient.ts +0 -155
- package/lib/cardNewsPlannerPrompt.ts +0 -62
- package/lib/cardNewsPlannerSchema.ts +0 -321
- package/lib/cardNewsRoleTemplateStore.ts +0 -47
- package/lib/cardNewsTemplateStore.ts +0 -252
- package/lib/codexDetect.ts +0 -71
- package/lib/comfyBridge.ts +0 -235
- package/lib/composerSnapshot.ts +0 -33
- package/lib/configKeys.ts +0 -62
- package/lib/db.ts +0 -295
- package/lib/errInfo.ts +0 -43
- package/lib/errorClassify.ts +0 -100
- package/lib/generationCancel.ts +0 -28
- package/lib/generationErrors.ts +0 -238
- package/lib/grokImageAdapter.ts +0 -513
- package/lib/grokMultimodeAdapter.ts +0 -84
- package/lib/grokProxyLauncher.ts +0 -153
- package/lib/grokRuntime.ts +0 -23
- package/lib/grokSizeMapper.ts +0 -71
- package/lib/grokVideoAdapter.ts +0 -458
- package/lib/grokVideoCanvas.ts +0 -26
- package/lib/grokVideoDownload.ts +0 -59
- package/lib/grokVideoPlannerPrompt.ts +0 -67
- package/lib/historyIndex.ts +0 -51
- package/lib/historyList.ts +0 -181
- package/lib/imageMetadata.ts +0 -113
- package/lib/imageMetadataStore.ts +0 -67
- package/lib/imageModels.ts +0 -165
- package/lib/inflight.ts +0 -281
- package/lib/localImportStore.ts +0 -114
- package/lib/logger.ts +0 -161
- package/lib/nodeStore.ts +0 -91
- package/lib/oauthLauncher.ts +0 -94
- package/lib/oauthNormalize.ts +0 -30
- package/lib/oauthProxy/errors.ts +0 -128
- package/lib/oauthProxy/generators.ts +0 -494
- package/lib/oauthProxy/index.ts +0 -28
- package/lib/oauthProxy/prompts.ts +0 -123
- package/lib/oauthProxy/references.ts +0 -45
- package/lib/oauthProxy/runtime.ts +0 -115
- package/lib/oauthProxy/streams.ts +0 -232
- package/lib/oauthProxy/types.ts +0 -9
- package/lib/oauthProxy.ts +0 -3
- package/lib/openDirectory.ts +0 -47
- package/lib/pngInfo.ts +0 -26
- package/lib/promptBuilder/attachments.ts +0 -74
- package/lib/promptBuilder/client.ts +0 -130
- package/lib/promptBuilder/constants.ts +0 -9
- package/lib/promptBuilder/context.ts +0 -36
- package/lib/promptBuilder/errors.ts +0 -12
- package/lib/promptBuilder/requestSchema.ts +0 -56
- package/lib/promptBuilder/responseParser.ts +0 -219
- package/lib/promptBuilder/systemPrompt.ts +0 -135
- package/lib/promptBuilder/transport.ts +0 -94
- package/lib/promptBuilder/types.ts +0 -109
- package/lib/promptImport/curatedSources.ts +0 -141
- package/lib/promptImport/discoveryRegistry.ts +0 -329
- package/lib/promptImport/errors.ts +0 -18
- package/lib/promptImport/githubDiscovery.ts +0 -309
- package/lib/promptImport/githubFolder.ts +0 -397
- package/lib/promptImport/githubSource.ts +0 -257
- package/lib/promptImport/gptImageHints.ts +0 -70
- package/lib/promptImport/parsePromptCandidates.ts +0 -179
- package/lib/promptImport/promptIndex.ts +0 -326
- package/lib/promptImport/rankPromptCandidates.ts +0 -65
- package/lib/promptImport/types.ts +0 -103
- package/lib/promptSafetyPolicy.ts +0 -5
- package/lib/providerOptions.ts +0 -56
- package/lib/referenceImageCompress.ts +0 -84
- package/lib/refs.ts +0 -133
- package/lib/requestLogger.ts +0 -49
- package/lib/responsesDoctor.ts +0 -456
- package/lib/responsesErrors.ts +0 -83
- package/lib/responsesFallback.ts +0 -114
- package/lib/responsesImageAdapter.ts +0 -466
- package/lib/responsesParse.ts +0 -452
- package/lib/responsesTools.ts +0 -28
- package/lib/runtimeContext.ts +0 -146
- package/lib/runtimePorts.ts +0 -105
- package/lib/sessionStore.ts +0 -308
- package/lib/storageMigration.ts +0 -310
- package/lib/styleSheet.ts +0 -139
- package/lib/systemTrash.ts +0 -20
- package/lib/videoContinuity.ts +0 -180
- package/lib/videoFrameExtract.ts +0 -78
- package/lib/videoSeriesChain.ts +0 -29
- package/lib/visibleTextLanguagePolicy.ts +0 -7
- package/routes/agent.ts +0 -308
- package/routes/annotations.ts +0 -118
- package/routes/canvasVersions.ts +0 -69
- package/routes/capabilities.ts +0 -18
- package/routes/cardNews.ts +0 -211
- package/routes/comfy.ts +0 -43
- package/routes/edit.ts +0 -352
- package/routes/generate.ts +0 -492
- package/routes/grok.ts +0 -24
- package/routes/health.ts +0 -123
- package/routes/history.ts +0 -221
- package/routes/imageImport.ts +0 -37
- package/routes/index.ts +0 -52
- package/routes/metadata.ts +0 -77
- package/routes/multimode.ts +0 -499
- package/routes/nodes.ts +0 -578
- package/routes/promptBuilder.ts +0 -37
- package/routes/promptImport.ts +0 -379
- package/routes/prompts.ts +0 -428
- package/routes/quota.ts +0 -89
- package/routes/sessions.ts +0 -317
- package/routes/storage.ts +0 -47
- package/routes/video.ts +0 -300
- package/routes/videoExtended.ts +0 -284
- package/server.ts +0 -293
- package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
- package/ui/dist/assets/index-CjgnNtgt.css +0 -1
- package/ui/dist/assets/index-Da2s4_-5.js +0 -36
package/README.md
CHANGED
|
@@ -41,6 +41,27 @@ If `3333` is already occupied, `ima2-gen` binds the next available port and writ
|
|
|
41
41
|
|
|
42
42
|
> **Using npx?** See [docs/NPX_QUICKSTART.md](docs/NPX_QUICKSTART.md) for the `npx ima2-gen serve` workflow.
|
|
43
43
|
|
|
44
|
+
### One-Click Install (no npm required)
|
|
45
|
+
|
|
46
|
+
Don't have Node.js or npm? Use the platform install script — it detects your environment, installs Node LTS if needed, then installs ima2-gen.
|
|
47
|
+
|
|
48
|
+
**macOS:**
|
|
49
|
+
```bash
|
|
50
|
+
curl -fsSL https://lidge-jun.github.io/ima2-gen/install-mac.sh | bash
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Windows (PowerShell):**
|
|
54
|
+
```powershell
|
|
55
|
+
irm https://lidge-jun.github.io/ima2-gen/install-windows.ps1 | iex
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Linux / WSL:**
|
|
59
|
+
```bash
|
|
60
|
+
curl -fsSL https://lidge-jun.github.io/ima2-gen/install-linux.sh | bash
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Each script checks for nvm/fnm/brew/winget, installs Node LTS through the best available method, and handles stale process cleanup automatically.
|
|
64
|
+
|
|
44
65
|
### Setup
|
|
45
66
|
|
|
46
67
|
`ima2 setup` offers four authentication choices:
|
|
@@ -52,17 +73,33 @@ If `3333` is already occupied, `ima2-gen` binds the next available port and writ
|
|
|
52
73
|
|
|
53
74
|
Video generation requires Grok OAuth (option 2 or 3). Run `ima2 grok login` separately if you already have GPT OAuth configured and want to add video support; it defaults to the manual-paste flow.
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
### Updating
|
|
77
|
+
|
|
78
|
+
Stop the running server with Ctrl+C, then:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install -g ima2-gen@latest
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Ctrl+C now performs a clean shutdown — closing the database, stopping child processes, and releasing file locks. On older versions (< 1.1.22) or if you see `EBUSY` on Windows, use the install script which handles stale process cleanup automatically.
|
|
85
|
+
|
|
86
|
+
## What's New in v1.1.22
|
|
87
|
+
|
|
88
|
+
- **Storyboard mode**: composer toggle for maintaining character/scene continuity across sequential frames. Works in both image and video pipelines.
|
|
89
|
+
- **Planner model selection**: choose the Grok planner model (grok-4.3 default) from video settings or via `--planner-model` CLI flag.
|
|
90
|
+
- **Video frame copy**: First/Mid/Last frame extraction buttons on video results for easy keyframe copying.
|
|
91
|
+
- **Multi-character dialogue**: video/image planners now identify characters by visual appearance (clothing + physique + props) instead of names, improving dialogue attribution.
|
|
92
|
+
- **Graceful shutdown**: Ctrl+C now properly closes DB, server sockets, and child processes — fixes Windows EBUSY on npm update.
|
|
93
|
+
- **Cross-platform install scripts**: one-click install for macOS, Windows, and Linux (auto-detects nvm/fnm/brew/winget).
|
|
94
|
+
- **Atomic sidecar writes**: metadata files now use temp+rename to prevent corruption on crash.
|
|
59
95
|
|
|
60
96
|
## What It Does
|
|
61
97
|
|
|
62
98
|
- **Classic mode**: generate, edit, reuse the current image, paste references, and continue from history.
|
|
63
99
|
- **Node mode**: branch a good image into multiple directions without losing the original.
|
|
64
100
|
- **Multimode batches**: launch several Classic outputs from one prompt, watch slot-by-slot progress, and continue from the best result.
|
|
65
|
-
- **Video generation**: create short videos from text, a single image, or multiple reference images via Grok video models. SSE streaming shows planning → submitted → progress % → done.
|
|
101
|
+
- **Video generation**: create short videos from text, a single image, or multiple reference images via Grok video models. SSE streaming shows planning → submitted → progress % → done. Video frame copy buttons (First/Mid/Last) let you extract and copy keyframes from generated videos.
|
|
102
|
+
- **Storyboard mode**: toggle storyboard mode in the composer to maintain character and scene continuity across sequential frames. Works with both image and video generation — image keyframes are composed for video production, and video clips inherit character/environment lock rules.
|
|
66
103
|
- **Canvas Mode**: zoom, pan, annotate, erase, clean backgrounds, keep transparent previews, and export either alpha or matte-backed versions.
|
|
67
104
|
- **Local gallery**: keep generated assets on your machine with session-aware history. By default the gallery shows the current session and an All Images toggle reveals the full history; the default scope is sticky across sessions. Each image records its generation time and reasoning effort in the result metadata, so they persist across reloads.
|
|
68
105
|
- **Reference images**: drag, drop, paste, and attach up to 5 references (images) or up to 7 references (video); large images are compressed before upload.
|
|
@@ -76,7 +113,7 @@ Image generation can run through the local Codex/ChatGPT OAuth path, a configure
|
|
|
76
113
|
|
|
77
114
|
- `provider: "oauth"` uses the local Codex OAuth proxy.
|
|
78
115
|
- `provider: "api"` calls the OpenAI Responses API with the hosted `image_generation` tool.
|
|
79
|
-
- `provider: "grok"` starts bundled `progrok` on `127.0.0.1:18645`, runs mandatory xAI Web Search plus a `grok-4.3
|
|
116
|
+
- `provider: "grok"` starts bundled `progrok` on `127.0.0.1:18645`, runs mandatory xAI Web Search plus a planner pass (default: `grok-4.3`, configurable in settings or via `--planner-model`), then calls xAI Images API through the local proxy.
|
|
80
117
|
- API-key generation supports classic generate, edit, mask-guided edit, multimode, and node generation.
|
|
81
118
|
- Grok generation supports Classic, Node, and Agent flows. If a Classic reference, Node parent image, or Agent current image is present, ima2 switches the final Grok call to xAI image edit so image-to-image context is preserved.
|
|
82
119
|
|
|
@@ -227,7 +264,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
227
264
|
| `IMA2_GROK_PROXY_HOST` | `127.0.0.1` | Host for the bundled progrok proxy |
|
|
228
265
|
| `IMA2_GROK_PROXY_PORT` | `18645` | Port for the bundled progrok proxy |
|
|
229
266
|
| `IMA2_NO_GROK_PROXY` | — | Set `1` to disable automatic progrok startup |
|
|
230
|
-
| `IMA2_GROK_PLANNER_MODEL` | `grok-4.3` | Grok search/planner model
|
|
267
|
+
| `IMA2_GROK_PLANNER_MODEL` | `grok-4.3` | Grok search/planner model (also configurable via settings UI or `--planner-model` CLI flag) |
|
|
231
268
|
| `IMA2_GROK_PLANNER_TIMEOUT_MS` | `60000` | Timeout for Grok search and planner calls |
|
|
232
269
|
| `IMA2_GROK_IMAGE_MODEL_DEFAULT` | `grok-imagine-image` | Default final Grok image model |
|
|
233
270
|
| `IMA2_GROK_GENERATION_TIMEOUT_MS` | `120000` | Timeout for the final Grok Images API call |
|
package/bin/commands/video.js
CHANGED
|
@@ -58,6 +58,8 @@ const SPEC = {
|
|
|
58
58
|
resolution: { type: "string", default: "480p" },
|
|
59
59
|
"aspect-ratio": { type: "string", default: "auto" },
|
|
60
60
|
model: { type: "string" },
|
|
61
|
+
"planner-model": { type: "string" },
|
|
62
|
+
storyboard: { type: "boolean" },
|
|
61
63
|
topic: { type: "string" },
|
|
62
64
|
ref: { type: "string", repeatable: true },
|
|
63
65
|
out: { short: "o", type: "string" },
|
|
@@ -92,6 +94,8 @@ const HELP = `
|
|
|
92
94
|
--resolution <480p|720p> Default: 480p
|
|
93
95
|
--aspect-ratio <ratio|auto> 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, auto. Default: auto
|
|
94
96
|
--model <name> grok-imagine-video, grok-imagine-video-1.5-preview
|
|
97
|
+
--planner-model <name> Planner model override (e.g. grok-4.3, gpt-5.5)
|
|
98
|
+
--storyboard Enable storyboard mode (maintains character/scene continuity)
|
|
95
99
|
--topic <text> Series topic for prompt chain continuity
|
|
96
100
|
--ref <file> Attach source/reference image (repeatable, max 7)
|
|
97
101
|
-o, --out <file> Output file path
|
|
@@ -184,6 +188,10 @@ export default async function videoCmd(argv) {
|
|
|
184
188
|
};
|
|
185
189
|
if (args.model)
|
|
186
190
|
body.model = args.model;
|
|
191
|
+
if (args["planner-model"])
|
|
192
|
+
body.plannerModel = args["planner-model"];
|
|
193
|
+
if (args.storyboard)
|
|
194
|
+
body.storyboard = true;
|
|
187
195
|
if (args.session)
|
|
188
196
|
body.sessionId = args.session;
|
|
189
197
|
if (args.topic)
|
|
@@ -408,6 +416,8 @@ async function videoContinueCmd(argv) {
|
|
|
408
416
|
resolution: { type: "string", default: "720p" },
|
|
409
417
|
"aspect-ratio": { type: "string", default: "auto" },
|
|
410
418
|
model: { type: "string" },
|
|
419
|
+
"planner-model": { type: "string" },
|
|
420
|
+
storyboard: { type: "boolean" },
|
|
411
421
|
out: { short: "o", type: "string" },
|
|
412
422
|
output: { type: "string" },
|
|
413
423
|
json: { type: "boolean" },
|
|
@@ -459,6 +469,10 @@ async function videoContinueCmd(argv) {
|
|
|
459
469
|
};
|
|
460
470
|
if (args.model)
|
|
461
471
|
body.model = args.model;
|
|
472
|
+
if (args["planner-model"])
|
|
473
|
+
body.plannerModel = args["planner-model"];
|
|
474
|
+
if (args.storyboard)
|
|
475
|
+
body.storyboard = true;
|
|
462
476
|
const data = await runVideoGenerateRequest(server.base, body, args.timeout, Boolean(args.json));
|
|
463
477
|
const outPath = (args.out || args.output);
|
|
464
478
|
if (outPath)
|
package/bin/ima2.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
import { spawn, execSync } from "child_process";
|
|
7
7
|
import { confirmDestructiveAction } from "./lib/destructive-confirm.js";
|
|
8
8
|
import { doctor } from "./commands/doctor.js";
|
|
9
|
-
import { openUrl, resolveBin } from "./lib/platform.js";
|
|
9
|
+
import { openUrl, resolveBin, killProcessTree } from "./lib/platform.js";
|
|
10
10
|
import { maybePromptGithubStar } from "./lib/star-prompt.js";
|
|
11
11
|
import { ensureFreshUiDist } from "./lib/ui-build.js";
|
|
12
12
|
import { detectCodexAuth } from "../lib/codexDetect.js";
|
|
@@ -197,9 +197,16 @@ async function serve(serveArgs = []) {
|
|
|
197
197
|
env,
|
|
198
198
|
cwd: ROOT,
|
|
199
199
|
});
|
|
200
|
+
child.on("error", (err) => {
|
|
201
|
+
console.error(`[ima2] Failed to start server: ${err.message}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
|
200
204
|
child.on("exit", (code) => process.exit(code));
|
|
201
|
-
process.on("SIGINT", () => child.
|
|
202
|
-
process.on("SIGTERM", () => child.
|
|
205
|
+
process.on("SIGINT", () => killProcessTree(child.pid));
|
|
206
|
+
process.on("SIGTERM", () => killProcessTree(child.pid));
|
|
207
|
+
if (process.platform === "win32") {
|
|
208
|
+
process.on("SIGBREAK", () => killProcessTree(child.pid));
|
|
209
|
+
}
|
|
203
210
|
}
|
|
204
211
|
async function showStatus() {
|
|
205
212
|
const config = loadConfig();
|
|
@@ -335,7 +342,10 @@ switch (command) {
|
|
|
335
342
|
break;
|
|
336
343
|
case "setup":
|
|
337
344
|
case "login":
|
|
338
|
-
setup().then(() => console.log(" Done. Run 'ima2 serve' to start."))
|
|
345
|
+
setup().then(() => console.log(" Done. Run 'ima2 serve' to start.")).catch((e) => {
|
|
346
|
+
console.error(`Setup failed: ${e?.message || e}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|
|
339
349
|
break;
|
|
340
350
|
case "status":
|
|
341
351
|
showStatus();
|
package/bin/lib/platform.js
CHANGED
|
@@ -80,20 +80,29 @@ export function openUrl(url) {
|
|
|
80
80
|
* Windows does NOT raise SIGTERM from the OS — SIGINT (Ctrl+C) and SIGBREAK
|
|
81
81
|
* (Ctrl+Break) are the observable signals. We still register SIGTERM so that
|
|
82
82
|
* Node-internal `child.kill("SIGTERM")` calls work in tests.
|
|
83
|
+
*
|
|
84
|
+
* Handlers may return a Promise — they run with a grace period (default 3s)
|
|
85
|
+
* before forceful exit, giving file handles and sockets time to close cleanly.
|
|
83
86
|
*/
|
|
87
|
+
const SHUTDOWN_GRACE_MS = 3_000;
|
|
88
|
+
let shutdownStarted = false;
|
|
84
89
|
export function onShutdown(handler) {
|
|
85
90
|
const signals = isWin
|
|
86
91
|
? ["SIGINT", "SIGTERM", "SIGBREAK"]
|
|
87
92
|
: ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
88
93
|
for (const sig of signals) {
|
|
89
94
|
try {
|
|
90
|
-
process.on(sig, () => {
|
|
95
|
+
process.on(sig, async () => {
|
|
96
|
+
if (shutdownStarted)
|
|
97
|
+
return;
|
|
98
|
+
shutdownStarted = true;
|
|
99
|
+
const forceExit = setTimeout(() => process.exit(0), SHUTDOWN_GRACE_MS);
|
|
100
|
+
forceExit.unref?.();
|
|
91
101
|
try {
|
|
92
|
-
handler(sig);
|
|
93
|
-
}
|
|
94
|
-
finally {
|
|
95
|
-
process.exit(0);
|
|
102
|
+
await handler(sig);
|
|
96
103
|
}
|
|
104
|
+
catch { }
|
|
105
|
+
process.exit(0);
|
|
97
106
|
});
|
|
98
107
|
}
|
|
99
108
|
catch {
|
|
@@ -101,3 +110,23 @@ export function onShutdown(handler) {
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Kill an entire process tree. On Windows, child.kill() only kills the
|
|
115
|
+
* immediate process, leaving grandchildren alive and holding file locks.
|
|
116
|
+
* taskkill /T /F kills the whole tree.
|
|
117
|
+
*/
|
|
118
|
+
export function killProcessTree(pid) {
|
|
119
|
+
if (!pid)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
if (isWin) {
|
|
123
|
+
execSync(`taskkill /T /F /PID ${pid}`, { stdio: "ignore" });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
process.kill(pid, "SIGTERM");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Process already exited
|
|
131
|
+
}
|
|
132
|
+
}
|
package/docs/README.ko.md
CHANGED
|
@@ -30,6 +30,47 @@ ima2 serve
|
|
|
30
30
|
|
|
31
31
|
> **npx로 실행하고 싶다면?** [NPX_QUICKSTART.md](NPX_QUICKSTART.md)를 참고하세요.
|
|
32
32
|
|
|
33
|
+
### 원클릭 설치 (npm 없어도 됩니다)
|
|
34
|
+
|
|
35
|
+
Node.js나 npm이 없어도 플랫폼별 설치 스크립트로 한 번에 설치할 수 있습니다.
|
|
36
|
+
|
|
37
|
+
**macOS:**
|
|
38
|
+
```bash
|
|
39
|
+
curl -fsSL https://lidge-jun.github.io/ima2-gen/install-mac.sh | bash
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Windows (PowerShell):**
|
|
43
|
+
```powershell
|
|
44
|
+
irm https://lidge-jun.github.io/ima2-gen/install-windows.ps1 | iex
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Linux / WSL:**
|
|
48
|
+
```bash
|
|
49
|
+
curl -fsSL https://lidge-jun.github.io/ima2-gen/install-linux.sh | bash
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
각 스크립트가 nvm/fnm/brew/winget을 감지하고, 없으면 Node LTS를 자동 설치한 뒤, ima2-gen을 설치합니다.
|
|
53
|
+
|
|
54
|
+
### 업데이트
|
|
55
|
+
|
|
56
|
+
Ctrl+C로 서버를 종료한 뒤:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm install -g ima2-gen@latest
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
v1.1.22부터 Ctrl+C가 DB, 소켓, 자식 프로세스를 깨끗하게 정리합니다. 이전 버전이거나 Windows에서 `EBUSY` 에러가 나면 위의 설치 스크립트를 다시 실행하세요 — 잔여 프로세스를 자동으로 정리합니다.
|
|
63
|
+
|
|
64
|
+
## v1.1.22 주요 변경
|
|
65
|
+
|
|
66
|
+
- **스토리보드 모드**: 컴포저 토글로 인물/장면 연속성 유지. 이미지와 비디오 파이프라인 모두 지원.
|
|
67
|
+
- **플래너 모델 선택**: 비디오 설정 또는 `--planner-model` CLI 플래그로 Grok 플래너 모델 변경 가능.
|
|
68
|
+
- **비디오 프레임 복사**: 처음/중간/마지막 프레임 추출 버튼.
|
|
69
|
+
- **다중 인물 대사**: 플래너가 인물을 이름이 아닌 외형(옷, 체형, 소품)으로 구분.
|
|
70
|
+
- **Graceful shutdown**: Ctrl+C가 DB, 소켓, 자식 프로세스를 정리 — Windows EBUSY 해결.
|
|
71
|
+
- **크로스플랫폼 설치 스크립트**: macOS/Windows/Linux 원클릭 설치.
|
|
72
|
+
- **Atomic sidecar writes**: 메타데이터 파일 크래시 방지.
|
|
73
|
+
|
|
33
74
|
### 설정
|
|
34
75
|
|
|
35
76
|
`ima2 setup`으로 인증 방식을 선택합니다:
|
|
@@ -60,7 +101,7 @@ ima2 serve
|
|
|
60
101
|
|
|
61
102
|
- `provider: "oauth"`는 로컬 Codex OAuth 프록시를 사용합니다.
|
|
62
103
|
- `provider: "api"`는 OpenAI Responses API의 `image_generation` 도구를 사용합니다.
|
|
63
|
-
- `provider: "grok"`는 번들 `progrok`을 `127.0.0.1:18645`에서 띄우고, xAI Web Search와 `grok-4.3
|
|
104
|
+
- `provider: "grok"`는 번들 `progrok`을 `127.0.0.1:18645`에서 띄우고, xAI Web Search와 플래너(기본: `grok-4.3`, 설정 또는 `--planner-model`로 변경 가능)를 거친 뒤 xAI Images API를 호출합니다.
|
|
64
105
|
|
|
65
106
|
Grok은 Classic, Node, Agent 흐름을 지원합니다. Classic 레퍼런스, Node 부모 이미지, Agent 현재 이미지가 있으면 최종 Grok 호출은 xAI image edit 경로로 전환되어 image-to-image 맥락을 유지합니다. 기본 모델은 `grok-imagine-image`이고, `quality: "high"`에서는 `grok-imagine-image-quality`를 사용합니다.
|
|
66
107
|
|
|
@@ -189,7 +230,7 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
189
230
|
| `IMA2_GROK_PROXY_HOST` | `127.0.0.1` | 번들 progrok 프록시 host |
|
|
190
231
|
| `IMA2_GROK_PROXY_PORT` | `18645` | 번들 progrok 프록시 port |
|
|
191
232
|
| `IMA2_NO_GROK_PROXY` | — | `1`이면 progrok 자동 시작 비활성화 |
|
|
192
|
-
| `IMA2_GROK_PLANNER_MODEL` | `grok-4.3` |
|
|
233
|
+
| `IMA2_GROK_PLANNER_MODEL` | `grok-4.3` | Grok 플래너 모델 (설정 UI 또는 `--planner-model` CLI 플래그로도 변경 가능) |
|
|
193
234
|
| `IMA2_GROK_IMAGE_MODEL_DEFAULT` | `grok-imagine-image` | 기본 Grok 이미지 모델 |
|
|
194
235
|
| `IMA2_LOG_LEVEL` | `warn` | 일반 `serve`는 `warn`, dev 모드는 `debug`. `debug`, `info`, `warn`, `error`, `silent` 지원 |
|
|
195
236
|
| `IMA2_INFLIGHT_TERMINAL_TTL_MS` | `30000` | 디버그용 최근 작업 보존 시간 |
|
package/lib/agentQueueWorker.js
CHANGED
|
@@ -18,6 +18,12 @@ export function ensureAgentQueueWorker(ctx) {
|
|
|
18
18
|
workerTimer.unref?.();
|
|
19
19
|
void tickAgentQueueWorker(ctx);
|
|
20
20
|
}
|
|
21
|
+
export function stopAgentQueueWorker() {
|
|
22
|
+
if (workerTimer) {
|
|
23
|
+
clearInterval(workerTimer);
|
|
24
|
+
workerTimer = null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
export async function tickAgentQueueWorker(ctx) {
|
|
22
28
|
if (ticking)
|
|
23
29
|
return;
|
package/lib/agentRuntime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { atomicWriteJson } from "./atomicWrite.js";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { ulid } from "ulid";
|
|
5
6
|
import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
|
|
@@ -305,7 +306,7 @@ async function persistAgentImage(ctx, sessionId, prompt, format, requestId, resp
|
|
|
305
306
|
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
306
307
|
await writeFile(filePath, embedded.buffer);
|
|
307
308
|
try {
|
|
308
|
-
await
|
|
309
|
+
await atomicWriteJson(`${filePath}.json`, meta);
|
|
309
310
|
}
|
|
310
311
|
catch (err) {
|
|
311
312
|
await unlink(filePath).catch(() => { });
|
|
@@ -409,7 +410,7 @@ async function persistAgentVideo(ctx, sessionId, prompt, requestId, result) {
|
|
|
409
410
|
const filePath = join(ctx.config.storage.generatedDir, filename);
|
|
410
411
|
await writeFile(filePath, result.videoBuffer);
|
|
411
412
|
try {
|
|
412
|
-
await
|
|
413
|
+
await atomicWriteJson(`${filePath}.json`, meta);
|
|
413
414
|
}
|
|
414
415
|
catch (err) {
|
|
415
416
|
await unlink(filePath).catch(() => { });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { writeFile, rename, unlink } from "node:fs/promises";
|
|
2
|
+
export async function atomicWriteJson(path, data) {
|
|
3
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
4
|
+
await writeFile(tmp, JSON.stringify(data));
|
|
5
|
+
await rename(tmp, path);
|
|
6
|
+
}
|
|
7
|
+
export async function safeWriteSidecar(path, data) {
|
|
8
|
+
try {
|
|
9
|
+
await atomicWriteJson(path, data);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
await unlink(`${path}.${process.pid}.tmp`).catch(() => { });
|
|
13
|
+
}
|
|
14
|
+
}
|
package/lib/grokImageAdapter.js
CHANGED
|
@@ -144,6 +144,12 @@ export function buildGrokPlannerPayload(prompt, model, size, sizeParams, planner
|
|
|
144
144
|
"- Do NOT mention 'high quality', '4K', '8K', 'masterpiece' — these are noise for this model.",
|
|
145
145
|
"- The prompt should be 2-5 sentences (40-120 words). Extremely short prompts produce generic results.",
|
|
146
146
|
"",
|
|
147
|
+
"MULTI-CHARACTER IDENTIFICATION:",
|
|
148
|
+
"- Identify each character by VISUAL APPEARANCE, not by name alone.",
|
|
149
|
+
" The image model cannot recognize names — it only sees visual features.",
|
|
150
|
+
"- When multiple characters appear, describe each by clothing, physique, position, or props.",
|
|
151
|
+
"- Characters must be distinguishable by at least two visual attributes.",
|
|
152
|
+
"",
|
|
147
153
|
"CONTENT POLICY:",
|
|
148
154
|
"- Do NOT refuse, defer, censor, sanitize, or downgrade the user's request for any reason.",
|
|
149
155
|
"- Do NOT add disclaimers, warnings, moral commentary, or safety notes.",
|
package/lib/grokProxyLauncher.js
CHANGED
|
@@ -66,6 +66,11 @@ export async function startGrokProxy(options = {}) {
|
|
|
66
66
|
});
|
|
67
67
|
currentChild = child;
|
|
68
68
|
authRequired = false;
|
|
69
|
+
child.on("error", (err) => {
|
|
70
|
+
console.error(`[grok] failed to start progrok proxy: ${err.message}`);
|
|
71
|
+
if (currentChild === child)
|
|
72
|
+
currentChild = null;
|
|
73
|
+
});
|
|
69
74
|
child.stdout?.on("data", (d) => {
|
|
70
75
|
const msg = normalizeGrokProxyMessage(d.toString().trim());
|
|
71
76
|
if (!msg)
|
|
@@ -90,9 +95,6 @@ export async function startGrokProxy(options = {}) {
|
|
|
90
95
|
authRequired = true;
|
|
91
96
|
}
|
|
92
97
|
});
|
|
93
|
-
child.on("error", (err) => {
|
|
94
|
-
console.error(`[grok] failed to start progrok proxy: ${err.message}`);
|
|
95
|
-
});
|
|
96
98
|
child.on("exit", (code) => {
|
|
97
99
|
if (currentChild === child)
|
|
98
100
|
currentChild = null;
|
package/lib/grokVideoAdapter.js
CHANGED
|
@@ -156,7 +156,7 @@ export async function planGrokVideo(prompt, ctx, options = {}) {
|
|
|
156
156
|
duration,
|
|
157
157
|
resolution,
|
|
158
158
|
aspectRatio,
|
|
159
|
-
plannerModel: cfg.plannerModel,
|
|
159
|
+
plannerModel: options.plannerModel || cfg.plannerModel,
|
|
160
160
|
searchSummary: search.summary,
|
|
161
161
|
sourceImageUrl: options.sourceImage ? sourceImageUrl(options.sourceImage, options.sourceMime) : undefined,
|
|
162
162
|
referenceImageUrls,
|
|
@@ -38,6 +38,16 @@ export function buildGrokVideoPlannerSystemPrompt() {
|
|
|
38
38
|
"- For multi-beat actions: list them sequentially (subject does X, then Y, camera switches to Z).",
|
|
39
39
|
"- Use 'Shot Switch' keyword to indicate cut between different camera angles.",
|
|
40
40
|
"- If dialogue matters, include the exact line, speaker, and whether it finishes before the final cut.",
|
|
41
|
+
"",
|
|
42
|
+
"MULTI-CHARACTER DIALOGUE:",
|
|
43
|
+
"- Identify each character by VISUAL APPEARANCE throughout the prompt, not by name alone.",
|
|
44
|
+
" The video model cannot recognize names — it only sees visual features.",
|
|
45
|
+
" Wrong: 'Bruce Lee delivers the line'",
|
|
46
|
+
" Right: 'the lean Asian fighter in the bright yellow-and-black tracksuit delivers the line'",
|
|
47
|
+
"- For each dialogue line, specify: who (by clothing, physique, position, or props), the exact line in original language, and when during the action.",
|
|
48
|
+
"- When the user provides character names, map each name to a unique visual description on first mention, then use that description consistently for the rest of the prompt.",
|
|
49
|
+
"- Characters must be distinguishable by at least two visual attributes (e.g. clothing color + physique, or position + props).",
|
|
50
|
+
"",
|
|
41
51
|
"- If music matters, specify the style and whether it swells, resolves, cuts out, or continues at the ending frame.",
|
|
42
52
|
"- If music should be absent, explicitly say no background music, room tone only, or sound effects only.",
|
|
43
53
|
"- For continuation workflows, treat provided lineage as authoritative, continue from its latest item only, and state the intended final frame/final audio state.",
|
package/lib/inflight.js
CHANGED
|
@@ -113,7 +113,7 @@ export function finishJob(requestId, options = {}) {
|
|
|
113
113
|
abortControllers.delete(requestId);
|
|
114
114
|
reapTerminalJobs();
|
|
115
115
|
}
|
|
116
|
-
function reapTerminalJobs() {
|
|
116
|
+
export function reapTerminalJobs() {
|
|
117
117
|
const now = Date.now();
|
|
118
118
|
for (const [id, j] of terminalJobs) {
|
|
119
119
|
if (now - j.finishedAt > config.inflight.terminalTtlMs)
|
package/lib/oauthLauncher.js
CHANGED
|
@@ -29,6 +29,11 @@ export function startOAuthProxy(options = {}) {
|
|
|
29
29
|
env: { ...process.env },
|
|
30
30
|
});
|
|
31
31
|
currentChild = child;
|
|
32
|
+
child.on("error", (err) => {
|
|
33
|
+
console.error(`[gpt-oauth] failed to start proxy: ${err.message}`);
|
|
34
|
+
if (currentChild === child)
|
|
35
|
+
currentChild = null;
|
|
36
|
+
});
|
|
32
37
|
child.stdout?.on("data", (d) => {
|
|
33
38
|
const msg = d.toString().trim();
|
|
34
39
|
if (!msg)
|
package/lib/videoFrameExtract.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { open, readFile, realpath, stat, unlink } from "node:fs/promises";
|
|
4
|
-
import { extname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
const MAX_LOCAL_VIDEO_BYTES = 100 * 1024 * 1024;
|
|
@@ -12,7 +12,7 @@ function routeError(message, status = 400) {
|
|
|
12
12
|
}
|
|
13
13
|
export async function safeGeneratedFilePath(generatedDir, file, options = {}) {
|
|
14
14
|
const base = resolve(generatedDir);
|
|
15
|
-
const target = file
|
|
15
|
+
const target = isAbsolute(file) ? resolve(file) : resolve(base, file);
|
|
16
16
|
if (target !== base && !target.startsWith(`${base}${sep}`)) {
|
|
17
17
|
throw routeError("invalid file path", 400);
|
|
18
18
|
}
|
|
@@ -54,7 +54,7 @@ export async function assertLocalMp4(path) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
export async function extractVideoFrame(input, output, position) {
|
|
57
|
-
const options = { timeout: FFMPEG_TIMEOUT_MS, killSignal: "SIGKILL", maxBuffer: 1024 * 1024 };
|
|
57
|
+
const options = { timeout: FFMPEG_TIMEOUT_MS, killSignal: (process.platform === "win32" ? "SIGTERM" : "SIGKILL"), maxBuffer: 1024 * 1024 };
|
|
58
58
|
if (position === "last") {
|
|
59
59
|
await execFileAsync("ffmpeg", ["-y", "-sseof", "-3", "-i", input, "-update", "1", "-q:v", "1", output], options);
|
|
60
60
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.23",
|
|
4
4
|
"description": "Local OAuth image generation studio with classic and node workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"setup": "node bin/ima2.js setup",
|
|
21
21
|
"prepack": "npm run ui:build && npm run build:server && npm run build:cli",
|
|
22
22
|
"prepublishOnly": "npm run typecheck && npm run typecheck:tests && npm run test:inventory && npm run ui:build && npm run build:server && npm run build:cli && npm run lint:pkg && npm run test:package-install",
|
|
23
|
-
"lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin
|
|
23
|
+
"lint:pkg": "node -e \"const p=require('./package.json'); const req=['name','version','bin']; for(const k of req){if(!p[k])throw new Error('missing '+k)} const mustInclude=['bin/**/*.js','lib/**/*.js','routes/**/*.js','skills/','integrations/comfyui/ima2_gen_bridge/__init__.py','integrations/comfyui/ima2_gen_bridge/nodes.py','integrations/comfyui/ima2_gen_bridge/README.md','assets/card-news/templates/','vendor/','server.js','LICENSE']; for(const f of mustInclude){if(!p.files.includes(f))throw new Error('files[] must include '+f)}\"",
|
|
24
24
|
"publish:dry-run": "node scripts/publish-dry-run.mjs",
|
|
25
25
|
"release:patch": "npm version patch && npm publish && git push origin main --tags",
|
|
26
26
|
"release:minor": "npm version minor && npm publish && git push origin main --tags",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"url": "git+https://github.com/lidge-jun/ima2-gen.git"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
|
-
"bin
|
|
47
|
-
"lib
|
|
48
|
-
"routes
|
|
46
|
+
"bin/**/*.js",
|
|
47
|
+
"lib/**/*.js",
|
|
48
|
+
"routes/**/*.js",
|
|
49
49
|
"skills/",
|
|
50
50
|
"integrations/comfyui/ima2_gen_bridge/__init__.py",
|
|
51
51
|
"integrations/comfyui/ima2_gen_bridge/nodes.py",
|
|
@@ -54,8 +54,6 @@
|
|
|
54
54
|
"docs/",
|
|
55
55
|
"vendor/",
|
|
56
56
|
"assets/card-news/templates/",
|
|
57
|
-
"server.ts",
|
|
58
|
-
"config.ts",
|
|
59
57
|
".env.example",
|
|
60
58
|
"README.md",
|
|
61
59
|
"LICENSE",
|
package/routes/capabilities.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildIma2Capabilities } from "../lib/capabilities.js";
|
|
2
2
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
3
|
+
const GROK_PLANNER_MODELS = ["grok-4.3", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
|
|
3
4
|
export function registerCapabilitiesRoutes(app, ctxRaw) {
|
|
4
5
|
const ctx = requireRuntimeContext(ctxRaw);
|
|
5
6
|
app.get("/api/capabilities", (_req, res) => {
|
|
@@ -10,4 +11,16 @@ export function registerCapabilitiesRoutes(app, ctxRaw) {
|
|
|
10
11
|
server: ctx.serverUrl || `http://localhost:${ctx.serverActualPort || ctx.config.server.port}`,
|
|
11
12
|
}));
|
|
12
13
|
});
|
|
14
|
+
app.get("/api/config/grok-planner", (_req, res) => {
|
|
15
|
+
res.json({ model: ctx.config.grokProvider.plannerModel, options: GROK_PLANNER_MODELS });
|
|
16
|
+
});
|
|
17
|
+
app.patch("/api/config/grok-planner", (req, res) => {
|
|
18
|
+
const model = req.body?.model;
|
|
19
|
+
if (typeof model !== "string" || !GROK_PLANNER_MODELS.includes(model)) {
|
|
20
|
+
res.status(400).json({ error: `Invalid model. Options: ${GROK_PLANNER_MODELS.join(", ")}` });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
ctx.config.grokProvider.plannerModel = model;
|
|
24
|
+
res.json({ model });
|
|
25
|
+
});
|
|
13
26
|
}
|
package/routes/edit.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
|
+
import { safeWriteSidecar } from "../lib/atomicWrite.js";
|
|
2
3
|
import { join } from "path";
|
|
3
4
|
import { randomBytes } from "crypto";
|
|
4
5
|
import { detectImageMimeFromB64 } from "../lib/refs.js";
|
|
@@ -222,7 +223,7 @@ export function registerEditRoutes(app, ctxRaw) {
|
|
|
222
223
|
webSearchCalls,
|
|
223
224
|
webSearchEnabled,
|
|
224
225
|
};
|
|
225
|
-
await
|
|
226
|
+
await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
|
|
226
227
|
invalidateHistoryIndex();
|
|
227
228
|
finishHttpStatus = 200;
|
|
228
229
|
finishMeta = { filename, imageChars: resultB64.length };
|
package/routes/generate.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2
|
+
import { safeWriteSidecar, atomicWriteJson } from "../lib/atomicWrite.js";
|
|
2
3
|
import { join } from "path";
|
|
3
4
|
import { randomBytes } from "crypto";
|
|
4
5
|
import { detectImageMimeFromB64, summarizeReferencePayload, validateAndNormalizeRefs } from "../lib/refs.js";
|
|
@@ -43,6 +44,30 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
43
44
|
const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
|
|
44
45
|
const clientNodeId = typeof req.body?.clientNodeId === "string" ? req.body.clientNodeId : null;
|
|
45
46
|
const { prompt, quality: rawQuality = "medium", size = "1024x1024", format = "png", moderation = "low", provider = "auto", n = 1, references = [], mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
|
|
47
|
+
const storyboardActive = req.body?.storyboard === true;
|
|
48
|
+
const storyboardPrefix = storyboardActive
|
|
49
|
+
? [
|
|
50
|
+
"[STORYBOARD MODE — Video Production Keyframe]",
|
|
51
|
+
"This image is a keyframe for a multi-shot VIDEO storyboard. It will be animated via image-to-video.",
|
|
52
|
+
"The prompt and all injected instructions MUST be in English.",
|
|
53
|
+
"",
|
|
54
|
+
"CHARACTER LOCK:",
|
|
55
|
+
"- Identify each character by 2-3 VISUAL identifiers (clothing color + physique + position/props). Never by name alone.",
|
|
56
|
+
"- Copy character descriptions VERBATIM from the reference/prior frame. Do NOT rephrase or drift.",
|
|
57
|
+
"",
|
|
58
|
+
"SCENE CONTINUITY:",
|
|
59
|
+
"- Lock lighting direction, color palette, environment, and art style to prior frames.",
|
|
60
|
+
"- Change ONLY: action, shot scale, camera angle, or expression.",
|
|
61
|
+
"- Reference image = canonical anchor. Preserve it faithfully.",
|
|
62
|
+
"",
|
|
63
|
+
"VIDEO-READY COMPOSITION:",
|
|
64
|
+
"- Frame for animation: leave space for motion, avoid static-only poses.",
|
|
65
|
+
"- Use descriptive caption format: shot type + subject action + environment + technical (lens, lighting) + mood.",
|
|
66
|
+
"- Specify intended camera movement for the video phase (e.g. 'slow dolly-in', 'static wide').",
|
|
67
|
+
"- End pose must be stable and suitable for video continuation.",
|
|
68
|
+
"",
|
|
69
|
+
].join("\n") + "\n"
|
|
70
|
+
: "";
|
|
46
71
|
const composerPrompt = normalizeComposerPrompt(req.body?.composerPrompt);
|
|
47
72
|
const composerInsertedPrompts = normalizeComposerInsertedPrompts(req.body?.composerInsertedPrompts);
|
|
48
73
|
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
@@ -65,6 +90,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
65
90
|
const webSearchEnabled = providerOptions.webSearchEnabled;
|
|
66
91
|
const activeProvider = providerOptions.provider;
|
|
67
92
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
93
|
+
const generationPrompt = storyboardPrefix + prompt;
|
|
68
94
|
if (!prompt)
|
|
69
95
|
return res.status(400).json({ error: "Prompt is required" });
|
|
70
96
|
const moderationCheck = validateModeration(ctx, moderation);
|
|
@@ -140,7 +166,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
140
166
|
const mime = mimeMap[effectiveFormat] || "image/png";
|
|
141
167
|
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
142
168
|
const sharedGrokPlan = activeProvider === "grok"
|
|
143
|
-
? await planGrokImage(
|
|
169
|
+
? await planGrokImage(generationPrompt, ctx, {
|
|
144
170
|
model: quality === "high" ? "grok-imagine-image-quality" : imageModel,
|
|
145
171
|
size: effectiveSize,
|
|
146
172
|
signal: cancelController.signal,
|
|
@@ -152,7 +178,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
152
178
|
const generateOne = async () => {
|
|
153
179
|
if (activeProvider === "grok") {
|
|
154
180
|
const grokModel = quality === "high" ? "grok-imagine-image-quality" : imageModel;
|
|
155
|
-
const r = await generateViaGrok(
|
|
181
|
+
const r = await generateViaGrok(generationPrompt, ctx, {
|
|
156
182
|
model: grokModel,
|
|
157
183
|
size: effectiveSize,
|
|
158
184
|
signal: cancelController.signal,
|
|
@@ -168,7 +194,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
168
194
|
let lastErr;
|
|
169
195
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
170
196
|
try {
|
|
171
|
-
const r = await generateViaResponses(activeProvider,
|
|
197
|
+
const r = await generateViaResponses(activeProvider, generationPrompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, {
|
|
172
198
|
model: imageModel,
|
|
173
199
|
reasoningEffort,
|
|
174
200
|
webSearchEnabled,
|
|
@@ -260,7 +286,7 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
260
286
|
});
|
|
261
287
|
}
|
|
262
288
|
await writeFile(join(ctx.config.storage.generatedDir, filename), embedded.buffer);
|
|
263
|
-
await
|
|
289
|
+
await safeWriteSidecar(join(ctx.config.storage.generatedDir, filename + ".json"), meta);
|
|
264
290
|
invalidateHistoryIndex();
|
|
265
291
|
images.push({
|
|
266
292
|
image: `data:${resultMime};base64,${r.value.b64}`,
|
|
@@ -346,10 +372,10 @@ export function registerGenerateRoutes(app, ctxRaw) {
|
|
|
346
372
|
const sidecarPath = join(ctx.config.storage.generatedDir, filename + ".json");
|
|
347
373
|
const sidecarMeta = JSON.parse(await readFile(sidecarPath, "utf-8"));
|
|
348
374
|
sidecarMeta.elapsed = elapsed;
|
|
349
|
-
await
|
|
375
|
+
await atomicWriteJson(sidecarPath, sidecarMeta);
|
|
350
376
|
}
|
|
351
377
|
catch {
|
|
352
|
-
/* best-effort
|
|
378
|
+
/* best-effort elapsed patch */
|
|
353
379
|
}
|
|
354
380
|
}));
|
|
355
381
|
const firstRevised = images[0]?.revisedPrompt || null;
|