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.
Files changed (214) hide show
  1. package/README.md +44 -7
  2. package/bin/commands/video.js +14 -0
  3. package/bin/ima2.js +14 -4
  4. package/bin/lib/platform.js +34 -5
  5. package/docs/README.ko.md +43 -2
  6. package/lib/agentQueueWorker.js +6 -0
  7. package/lib/agentRuntime.js +3 -2
  8. package/lib/atomicWrite.js +14 -0
  9. package/lib/grokImageAdapter.js +6 -0
  10. package/lib/grokProxyLauncher.js +5 -3
  11. package/lib/grokVideoAdapter.js +1 -1
  12. package/lib/grokVideoPlannerPrompt.js +10 -0
  13. package/lib/inflight.js +1 -1
  14. package/lib/oauthLauncher.js +5 -0
  15. package/lib/videoFrameExtract.js +3 -3
  16. package/package.json +5 -7
  17. package/routes/capabilities.js +13 -0
  18. package/routes/edit.js +2 -1
  19. package/routes/generate.js +32 -6
  20. package/routes/health.js +4 -3
  21. package/routes/multimode.js +2 -1
  22. package/routes/video.js +35 -3
  23. package/server.js +29 -2
  24. package/skills/ima2/SKILL.md +48 -6
  25. package/ui/dist/.vite/manifest.json +12 -12
  26. package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-C21zqdTZ.js} +1 -1
  27. package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-BN-ga1lG.js} +1 -1
  28. package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BbMa4IhI.js} +1 -1
  29. package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DRwBJRDQ.js} +1 -1
  30. package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-Dp85kHCq.js} +2 -2
  31. package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BE8Q8MLD.js} +1 -1
  32. package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-PtH5x0sc.js} +1 -1
  33. package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-FnM9tHI9.js} +2 -2
  34. package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +1 -0
  35. package/ui/dist/assets/index-BAFI6htx.js +42 -0
  36. package/ui/dist/assets/{index-31uVIdt4.js → index-BSXxr_Bt.js} +1 -1
  37. package/ui/dist/assets/index-DS-ADE7U.css +1 -0
  38. package/ui/dist/index.html +2 -2
  39. package/bin/commands/annotate.ts +0 -119
  40. package/bin/commands/cancel.ts +0 -48
  41. package/bin/commands/canvas-versions.ts +0 -80
  42. package/bin/commands/capabilities.ts +0 -110
  43. package/bin/commands/cardnews.ts +0 -249
  44. package/bin/commands/comfy.ts +0 -54
  45. package/bin/commands/config.ts +0 -186
  46. package/bin/commands/defaults.ts +0 -192
  47. package/bin/commands/doctor.ts +0 -202
  48. package/bin/commands/edit.ts +0 -150
  49. package/bin/commands/gen.ts +0 -214
  50. package/bin/commands/grok.ts +0 -90
  51. package/bin/commands/history.ts +0 -146
  52. package/bin/commands/ls.ts +0 -64
  53. package/bin/commands/metadata.ts +0 -39
  54. package/bin/commands/multimode.ts +0 -196
  55. package/bin/commands/node.ts +0 -166
  56. package/bin/commands/observability.ts +0 -176
  57. package/bin/commands/ping.ts +0 -31
  58. package/bin/commands/prompt-sub/build.ts +0 -101
  59. package/bin/commands/prompt.ts +0 -492
  60. package/bin/commands/ps.ts +0 -81
  61. package/bin/commands/session.ts +0 -266
  62. package/bin/commands/show.ts +0 -72
  63. package/bin/commands/skill.ts +0 -70
  64. package/bin/commands/video.ts +0 -442
  65. package/bin/ima2.ts +0 -430
  66. package/bin/lib/args.ts +0 -92
  67. package/bin/lib/browser-id.ts +0 -16
  68. package/bin/lib/client.ts +0 -122
  69. package/bin/lib/config-store.ts +0 -120
  70. package/bin/lib/destructive-confirm.ts +0 -19
  71. package/bin/lib/doctor-checks.ts +0 -91
  72. package/bin/lib/error-hints.ts +0 -23
  73. package/bin/lib/files.ts +0 -39
  74. package/bin/lib/output.ts +0 -73
  75. package/bin/lib/platform.ts +0 -99
  76. package/bin/lib/recover-output.ts +0 -139
  77. package/bin/lib/sse.ts +0 -73
  78. package/bin/lib/star-prompt.ts +0 -97
  79. package/bin/lib/storage-doctor.ts +0 -39
  80. package/bin/lib/ui-build.ts +0 -85
  81. package/config.ts +0 -354
  82. package/lib/agentCommandParser.ts +0 -69
  83. package/lib/agentGenerationPlanner.ts +0 -273
  84. package/lib/agentQuestionResponder.ts +0 -266
  85. package/lib/agentQueueStore.ts +0 -270
  86. package/lib/agentQueueWorker.ts +0 -89
  87. package/lib/agentRuntime.ts +0 -604
  88. package/lib/agentSettings.ts +0 -72
  89. package/lib/agentStore.ts +0 -422
  90. package/lib/agentStoreRows.ts +0 -136
  91. package/lib/agentTypes.ts +0 -154
  92. package/lib/apiCachePolicy.ts +0 -11
  93. package/lib/assetLifecycle.ts +0 -146
  94. package/lib/canvasVersionStore.ts +0 -223
  95. package/lib/capabilities.ts +0 -126
  96. package/lib/cardNewsGenerator.ts +0 -271
  97. package/lib/cardNewsJobStore.ts +0 -142
  98. package/lib/cardNewsManifestStore.ts +0 -154
  99. package/lib/cardNewsPlanner.ts +0 -236
  100. package/lib/cardNewsPlannerClient.ts +0 -155
  101. package/lib/cardNewsPlannerPrompt.ts +0 -62
  102. package/lib/cardNewsPlannerSchema.ts +0 -321
  103. package/lib/cardNewsRoleTemplateStore.ts +0 -47
  104. package/lib/cardNewsTemplateStore.ts +0 -252
  105. package/lib/codexDetect.ts +0 -71
  106. package/lib/comfyBridge.ts +0 -235
  107. package/lib/composerSnapshot.ts +0 -33
  108. package/lib/configKeys.ts +0 -62
  109. package/lib/db.ts +0 -295
  110. package/lib/errInfo.ts +0 -43
  111. package/lib/errorClassify.ts +0 -100
  112. package/lib/generationCancel.ts +0 -28
  113. package/lib/generationErrors.ts +0 -238
  114. package/lib/grokImageAdapter.ts +0 -513
  115. package/lib/grokMultimodeAdapter.ts +0 -84
  116. package/lib/grokProxyLauncher.ts +0 -153
  117. package/lib/grokRuntime.ts +0 -23
  118. package/lib/grokSizeMapper.ts +0 -71
  119. package/lib/grokVideoAdapter.ts +0 -458
  120. package/lib/grokVideoCanvas.ts +0 -26
  121. package/lib/grokVideoDownload.ts +0 -59
  122. package/lib/grokVideoPlannerPrompt.ts +0 -67
  123. package/lib/historyIndex.ts +0 -51
  124. package/lib/historyList.ts +0 -181
  125. package/lib/imageMetadata.ts +0 -113
  126. package/lib/imageMetadataStore.ts +0 -67
  127. package/lib/imageModels.ts +0 -165
  128. package/lib/inflight.ts +0 -281
  129. package/lib/localImportStore.ts +0 -114
  130. package/lib/logger.ts +0 -161
  131. package/lib/nodeStore.ts +0 -91
  132. package/lib/oauthLauncher.ts +0 -94
  133. package/lib/oauthNormalize.ts +0 -30
  134. package/lib/oauthProxy/errors.ts +0 -128
  135. package/lib/oauthProxy/generators.ts +0 -494
  136. package/lib/oauthProxy/index.ts +0 -28
  137. package/lib/oauthProxy/prompts.ts +0 -123
  138. package/lib/oauthProxy/references.ts +0 -45
  139. package/lib/oauthProxy/runtime.ts +0 -115
  140. package/lib/oauthProxy/streams.ts +0 -232
  141. package/lib/oauthProxy/types.ts +0 -9
  142. package/lib/oauthProxy.ts +0 -3
  143. package/lib/openDirectory.ts +0 -47
  144. package/lib/pngInfo.ts +0 -26
  145. package/lib/promptBuilder/attachments.ts +0 -74
  146. package/lib/promptBuilder/client.ts +0 -130
  147. package/lib/promptBuilder/constants.ts +0 -9
  148. package/lib/promptBuilder/context.ts +0 -36
  149. package/lib/promptBuilder/errors.ts +0 -12
  150. package/lib/promptBuilder/requestSchema.ts +0 -56
  151. package/lib/promptBuilder/responseParser.ts +0 -219
  152. package/lib/promptBuilder/systemPrompt.ts +0 -135
  153. package/lib/promptBuilder/transport.ts +0 -94
  154. package/lib/promptBuilder/types.ts +0 -109
  155. package/lib/promptImport/curatedSources.ts +0 -141
  156. package/lib/promptImport/discoveryRegistry.ts +0 -329
  157. package/lib/promptImport/errors.ts +0 -18
  158. package/lib/promptImport/githubDiscovery.ts +0 -309
  159. package/lib/promptImport/githubFolder.ts +0 -397
  160. package/lib/promptImport/githubSource.ts +0 -257
  161. package/lib/promptImport/gptImageHints.ts +0 -70
  162. package/lib/promptImport/parsePromptCandidates.ts +0 -179
  163. package/lib/promptImport/promptIndex.ts +0 -326
  164. package/lib/promptImport/rankPromptCandidates.ts +0 -65
  165. package/lib/promptImport/types.ts +0 -103
  166. package/lib/promptSafetyPolicy.ts +0 -5
  167. package/lib/providerOptions.ts +0 -56
  168. package/lib/referenceImageCompress.ts +0 -84
  169. package/lib/refs.ts +0 -133
  170. package/lib/requestLogger.ts +0 -49
  171. package/lib/responsesDoctor.ts +0 -456
  172. package/lib/responsesErrors.ts +0 -83
  173. package/lib/responsesFallback.ts +0 -114
  174. package/lib/responsesImageAdapter.ts +0 -466
  175. package/lib/responsesParse.ts +0 -452
  176. package/lib/responsesTools.ts +0 -28
  177. package/lib/runtimeContext.ts +0 -146
  178. package/lib/runtimePorts.ts +0 -105
  179. package/lib/sessionStore.ts +0 -308
  180. package/lib/storageMigration.ts +0 -310
  181. package/lib/styleSheet.ts +0 -139
  182. package/lib/systemTrash.ts +0 -20
  183. package/lib/videoContinuity.ts +0 -180
  184. package/lib/videoFrameExtract.ts +0 -78
  185. package/lib/videoSeriesChain.ts +0 -29
  186. package/lib/visibleTextLanguagePolicy.ts +0 -7
  187. package/routes/agent.ts +0 -308
  188. package/routes/annotations.ts +0 -118
  189. package/routes/canvasVersions.ts +0 -69
  190. package/routes/capabilities.ts +0 -18
  191. package/routes/cardNews.ts +0 -211
  192. package/routes/comfy.ts +0 -43
  193. package/routes/edit.ts +0 -352
  194. package/routes/generate.ts +0 -492
  195. package/routes/grok.ts +0 -24
  196. package/routes/health.ts +0 -123
  197. package/routes/history.ts +0 -221
  198. package/routes/imageImport.ts +0 -37
  199. package/routes/index.ts +0 -52
  200. package/routes/metadata.ts +0 -77
  201. package/routes/multimode.ts +0 -499
  202. package/routes/nodes.ts +0 -578
  203. package/routes/promptBuilder.ts +0 -37
  204. package/routes/promptImport.ts +0 -379
  205. package/routes/prompts.ts +0 -428
  206. package/routes/quota.ts +0 -89
  207. package/routes/sessions.ts +0 -317
  208. package/routes/storage.ts +0 -47
  209. package/routes/video.ts +0 -300
  210. package/routes/videoExtended.ts +0 -284
  211. package/server.ts +0 -293
  212. package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
  213. package/ui/dist/assets/index-CjgnNtgt.css +0 -1
  214. 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
- Before updating a global install on Windows, stop any running `ima2 serve`
56
- process. If npm reports `EBUSY` or `resource busy or locked`, close ima2
57
- terminals, end stale `node.exe` processes if needed, and retry. If the lock
58
- persists, reboot and run the update before starting ima2 again.
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` planner pass, then calls xAI Images API through the local proxy.
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 before the final Images API call |
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 |
@@ -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.kill("SIGINT"));
202
- process.on("SIGTERM", () => child.kill("SIGTERM"));
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();
@@ -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` planner를 거친 뒤 xAI Images API를 호출합니다.
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` | 최종 이미지 호출 Grok 검색/planner 모델 |
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` | 디버그용 최근 작업 보존 시간 |
@@ -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;
@@ -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 writeFile(`${filePath}.json`, JSON.stringify(meta));
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 writeFile(`${filePath}.json`, JSON.stringify(meta));
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
+ }
@@ -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.",
@@ -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;
@@ -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)
@@ -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)
@@ -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.startsWith("/") ? resolve(file) : resolve(base, 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.21",
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/','lib/','routes/','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)}\"",
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",
@@ -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 writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
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 };
@@ -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(prompt, ctx, {
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(prompt, ctx, {
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, prompt, quality, effectiveSize, moderation, refCheck.refDetails || refCheck.refs, requestId, normalizedPromptMode, ctx, {
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 writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
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 writeFile(sidecarPath, JSON.stringify(sidecarMeta));
375
+ await atomicWriteJson(sidecarPath, sidecarMeta);
350
376
  }
351
377
  catch {
352
- /* best-effort, matches the sidecar write .catch(() => {}) above */
378
+ /* best-effort elapsed patch */
353
379
  }
354
380
  }));
355
381
  const firstRevised = images[0]?.revisedPrompt || null;