ima2-gen 1.1.8 → 1.1.9

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 (230) hide show
  1. package/README.md +56 -27
  2. package/bin/commands/annotate.js +137 -0
  3. package/bin/commands/annotate.ts +118 -0
  4. package/bin/commands/cancel.js +37 -33
  5. package/bin/commands/cancel.ts +45 -0
  6. package/bin/commands/canvas-versions.js +91 -0
  7. package/bin/commands/canvas-versions.ts +80 -0
  8. package/bin/commands/cardnews.js +293 -0
  9. package/bin/commands/cardnews.ts +248 -0
  10. package/bin/commands/comfy.js +63 -0
  11. package/bin/commands/comfy.ts +54 -0
  12. package/bin/commands/config.js +270 -0
  13. package/bin/commands/config.ts +265 -0
  14. package/bin/commands/edit.js +97 -72
  15. package/bin/commands/edit.ts +116 -0
  16. package/bin/commands/gen.js +140 -118
  17. package/bin/commands/gen.ts +176 -0
  18. package/bin/commands/history.js +164 -0
  19. package/bin/commands/history.ts +145 -0
  20. package/bin/commands/ls.js +60 -42
  21. package/bin/commands/ls.ts +60 -0
  22. package/bin/commands/metadata.js +45 -0
  23. package/bin/commands/metadata.ts +36 -0
  24. package/bin/commands/multimode.js +159 -0
  25. package/bin/commands/multimode.ts +146 -0
  26. package/bin/commands/node.js +176 -0
  27. package/bin/commands/node.ts +157 -0
  28. package/bin/commands/observability.js +201 -0
  29. package/bin/commands/observability.ts +176 -0
  30. package/bin/commands/ping.js +26 -20
  31. package/bin/commands/ping.ts +29 -0
  32. package/bin/commands/prompt.js +506 -0
  33. package/bin/commands/prompt.ts +421 -0
  34. package/bin/commands/ps.js +78 -71
  35. package/bin/commands/ps.ts +78 -0
  36. package/bin/commands/session.js +308 -0
  37. package/bin/commands/session.ts +265 -0
  38. package/bin/commands/show.js +75 -40
  39. package/bin/commands/show.ts +69 -0
  40. package/bin/ima2.js +324 -310
  41. package/bin/ima2.ts +444 -0
  42. package/bin/lib/args.js +75 -66
  43. package/bin/lib/args.ts +73 -0
  44. package/bin/lib/browser-id.js +15 -0
  45. package/bin/lib/browser-id.ts +16 -0
  46. package/bin/lib/client.js +91 -83
  47. package/bin/lib/client.ts +109 -0
  48. package/bin/lib/error-hints.js +14 -17
  49. package/bin/lib/error-hints.ts +23 -0
  50. package/bin/lib/files.js +26 -28
  51. package/bin/lib/files.ts +39 -0
  52. package/bin/lib/output.js +44 -42
  53. package/bin/lib/output.ts +58 -0
  54. package/bin/lib/platform.js +60 -56
  55. package/bin/lib/platform.ts +97 -0
  56. package/bin/lib/sse.js +73 -0
  57. package/bin/lib/sse.ts +73 -0
  58. package/bin/lib/star-prompt.js +69 -76
  59. package/bin/lib/star-prompt.ts +97 -0
  60. package/bin/lib/storage-doctor.js +34 -35
  61. package/bin/lib/storage-doctor.ts +38 -0
  62. package/config.js +147 -243
  63. package/config.ts +331 -0
  64. package/docs/API.md +48 -8
  65. package/docs/CLI.md +190 -0
  66. package/docs/FAQ.ko.md +5 -5
  67. package/docs/FAQ.md +5 -5
  68. package/docs/README.ja.md +71 -25
  69. package/docs/README.ko.md +61 -24
  70. package/docs/README.zh-CN.md +73 -27
  71. package/lib/assetLifecycle.js +130 -130
  72. package/lib/assetLifecycle.ts +142 -0
  73. package/lib/canvasVersionStore.js +135 -153
  74. package/lib/canvasVersionStore.ts +181 -0
  75. package/lib/cardNewsGenerator.js +127 -142
  76. package/lib/cardNewsGenerator.ts +162 -0
  77. package/lib/cardNewsJobStore.js +78 -84
  78. package/lib/cardNewsJobStore.ts +107 -0
  79. package/lib/cardNewsManifestStore.js +88 -93
  80. package/lib/cardNewsManifestStore.ts +112 -0
  81. package/lib/cardNewsPlanner.js +157 -152
  82. package/lib/cardNewsPlanner.ts +180 -0
  83. package/lib/cardNewsPlannerClient.js +101 -98
  84. package/lib/cardNewsPlannerClient.ts +114 -0
  85. package/lib/cardNewsPlannerPrompt.js +56 -56
  86. package/lib/cardNewsPlannerPrompt.ts +60 -0
  87. package/lib/cardNewsPlannerSchema.js +231 -223
  88. package/lib/cardNewsPlannerSchema.ts +259 -0
  89. package/lib/cardNewsRoleTemplateStore.js +39 -41
  90. package/lib/cardNewsRoleTemplateStore.ts +47 -0
  91. package/lib/cardNewsTemplateStore.js +171 -175
  92. package/lib/cardNewsTemplateStore.ts +210 -0
  93. package/lib/codexDetect.js +44 -47
  94. package/lib/codexDetect.ts +69 -0
  95. package/lib/comfyBridge.js +164 -184
  96. package/lib/comfyBridge.ts +214 -0
  97. package/lib/db.js +41 -51
  98. package/lib/db.ts +166 -0
  99. package/lib/errorClassify.js +62 -78
  100. package/lib/errorClassify.ts +100 -0
  101. package/lib/generationErrors.js +140 -103
  102. package/lib/generationErrors.ts +125 -0
  103. package/lib/historyList.js +149 -147
  104. package/lib/historyList.ts +164 -0
  105. package/lib/imageMetadata.js +86 -89
  106. package/lib/imageMetadata.ts +111 -0
  107. package/lib/imageMetadataStore.js +46 -51
  108. package/lib/imageMetadataStore.ts +67 -0
  109. package/lib/imageModels.js +38 -45
  110. package/lib/imageModels.ts +52 -0
  111. package/lib/inflight.js +131 -150
  112. package/lib/inflight.ts +204 -0
  113. package/lib/localImportStore.js +87 -93
  114. package/lib/localImportStore.ts +111 -0
  115. package/lib/logger.js +105 -112
  116. package/lib/logger.ts +150 -0
  117. package/lib/nodeStore.js +65 -64
  118. package/lib/nodeStore.ts +81 -0
  119. package/lib/oauthLauncher.js +61 -59
  120. package/lib/oauthLauncher.ts +64 -0
  121. package/lib/oauthNormalize.js +15 -19
  122. package/lib/oauthNormalize.ts +30 -0
  123. package/lib/oauthProxy.js +834 -832
  124. package/lib/oauthProxy.ts +995 -0
  125. package/lib/openDirectory.js +41 -40
  126. package/lib/openDirectory.ts +45 -0
  127. package/lib/pngInfo.js +18 -20
  128. package/lib/pngInfo.ts +26 -0
  129. package/lib/promptImport/curatedSources.js +125 -129
  130. package/lib/promptImport/curatedSources.ts +139 -0
  131. package/lib/promptImport/discoveryRegistry.js +185 -203
  132. package/lib/promptImport/discoveryRegistry.ts +236 -0
  133. package/lib/promptImport/errors.js +10 -10
  134. package/lib/promptImport/errors.ts +18 -0
  135. package/lib/promptImport/githubDiscovery.js +209 -219
  136. package/lib/promptImport/githubDiscovery.ts +248 -0
  137. package/lib/promptImport/githubFolder.js +253 -259
  138. package/lib/promptImport/githubFolder.ts +308 -0
  139. package/lib/promptImport/githubSource.js +189 -200
  140. package/lib/promptImport/githubSource.ts +239 -0
  141. package/lib/promptImport/gptImageHints.js +49 -56
  142. package/lib/promptImport/gptImageHints.ts +68 -0
  143. package/lib/promptImport/parsePromptCandidates.js +108 -123
  144. package/lib/promptImport/parsePromptCandidates.ts +153 -0
  145. package/lib/promptImport/promptIndex.js +190 -208
  146. package/lib/promptImport/promptIndex.ts +248 -0
  147. package/lib/promptImport/rankPromptCandidates.js +46 -43
  148. package/lib/promptImport/rankPromptCandidates.ts +49 -0
  149. package/lib/providerOptions.js +31 -0
  150. package/lib/providerOptions.ts +41 -0
  151. package/lib/referenceImageCompress.js +51 -62
  152. package/lib/referenceImageCompress.ts +75 -0
  153. package/lib/refs.js +93 -81
  154. package/lib/refs.ts +117 -0
  155. package/lib/requestLogger.js +32 -38
  156. package/lib/requestLogger.ts +48 -0
  157. package/lib/responsesImageAdapter.js +351 -0
  158. package/lib/responsesImageAdapter.ts +352 -0
  159. package/lib/runtimePorts.js +71 -73
  160. package/lib/runtimePorts.ts +93 -0
  161. package/lib/sessionStore.js +179 -230
  162. package/lib/sessionStore.ts +272 -0
  163. package/lib/storageMigration.js +247 -245
  164. package/lib/storageMigration.ts +284 -0
  165. package/lib/styleSheet.js +86 -90
  166. package/lib/styleSheet.ts +128 -0
  167. package/lib/systemTrash.js +18 -0
  168. package/lib/systemTrash.ts +20 -0
  169. package/package.json +26 -10
  170. package/routes/annotations.js +76 -79
  171. package/routes/annotations.ts +95 -0
  172. package/routes/canvasVersions.js +50 -54
  173. package/routes/canvasVersions.ts +64 -0
  174. package/routes/cardNews.js +158 -171
  175. package/routes/cardNews.ts +183 -0
  176. package/routes/comfy.js +23 -31
  177. package/routes/comfy.ts +39 -0
  178. package/routes/edit.js +183 -214
  179. package/routes/edit.ts +230 -0
  180. package/routes/generate.js +269 -291
  181. package/routes/generate.ts +309 -0
  182. package/routes/health.js +102 -107
  183. package/routes/health.ts +114 -0
  184. package/routes/history.js +136 -144
  185. package/routes/history.ts +153 -0
  186. package/routes/imageImport.js +27 -27
  187. package/routes/imageImport.ts +33 -0
  188. package/routes/index.js +17 -17
  189. package/routes/index.ts +35 -0
  190. package/routes/metadata.js +60 -64
  191. package/routes/metadata.ts +71 -0
  192. package/routes/multimode.js +228 -263
  193. package/routes/multimode.ts +280 -0
  194. package/routes/nodes.js +378 -424
  195. package/routes/nodes.ts +455 -0
  196. package/routes/promptImport.js +284 -324
  197. package/routes/promptImport.ts +354 -0
  198. package/routes/prompts.js +333 -360
  199. package/routes/prompts.ts +379 -0
  200. package/routes/sessions.js +277 -285
  201. package/routes/sessions.ts +292 -0
  202. package/routes/storage.js +29 -31
  203. package/routes/storage.ts +39 -0
  204. package/server.js +189 -196
  205. package/server.ts +235 -0
  206. package/ui/dist/.vite/manifest.json +101 -0
  207. package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
  208. package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
  209. package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
  210. package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
  211. package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
  212. package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
  213. package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
  214. package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
  215. package/ui/dist/assets/index-C9cXwiWE.js +25 -0
  216. package/ui/dist/assets/index-CGMIkZXn.css +1 -0
  217. package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
  218. package/ui/dist/index.html +2 -2
  219. package/assets/phase-a-bg-cleanup-test.png +0 -0
  220. package/assets/screenshot.png +0 -0
  221. package/assets/screenshots/classic-generate-light.png +0 -0
  222. package/assets/screenshots/node-graph-branching.png +0 -0
  223. package/assets/screenshots/settings-oauth-generation.png +0 -0
  224. package/assets/screenshots/settings-workspace.png +0 -0
  225. package/assets/screenshots/style-sheet-editor.png +0 -0
  226. package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  227. package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
  228. package/ui/dist/assets/index-BDffwmLs.css +0 -1
  229. package/ui/dist/assets/index-D0fdHLkJ.js +0 -31
  230. package/ui/dist/assets/index-D0fdHLkJ.js.map +0 -1
package/routes/edit.js CHANGED
@@ -1,234 +1,203 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
2
  import { join } from "path";
3
3
  import { randomBytes } from "crypto";
4
- import { editViaOAuth } from "../lib/oauthProxy.js";
5
4
  import { classifyUpstreamError } from "../lib/errorClassify.js";
6
5
  import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
7
- import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
6
+ import { resolveProviderOptions } from "../lib/providerOptions.js";
7
+ import { editViaResponses } from "../lib/responsesImageAdapter.js";
8
8
  import { startJob, finishJob } from "../lib/inflight.js";
9
9
  import { logEvent, logError } from "../lib/logger.js";
10
10
  import { hasPngAlphaChannel, parsePngInfo } from "../lib/pngInfo.js";
11
-
12
11
  function validateModeration(ctx, moderation) {
13
- if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
14
- return { error: "moderation must be one of: auto, low" };
15
- }
16
- return { moderation };
12
+ if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
13
+ return { error: "moderation must be one of: auto, low" };
14
+ }
15
+ return { moderation };
17
16
  }
18
-
19
17
  const MAX_EDIT_MASK_BYTES = 16 * 1024 * 1024;
20
18
  const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
21
-
22
19
  function stripPngDataUrl(value) {
23
- if (typeof value !== "string") return "";
24
- return value.replace(/^data:image\/png;base64,/, "");
20
+ if (typeof value !== "string")
21
+ return "";
22
+ return value.replace(/^data:image\/png;base64,/, "");
25
23
  }
26
-
27
24
  function decodePngDataUrl(value, invalidCode, pngCode) {
28
- const b64 = stripPngDataUrl(value).replace(/\s+/g, "");
29
- if (!b64 || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) {
30
- return { error: "image must be valid base64", code: invalidCode };
31
- }
32
- const buffer = Buffer.from(b64, "base64");
33
- if (buffer.length === 0 || buffer.toString("base64").replace(/=+$/, "") !== b64.replace(/=+$/, "")) {
34
- return { error: "image must be valid base64", code: invalidCode };
35
- }
36
- const info = parsePngInfo(buffer);
37
- if (info.error) return { error: "image must be a PNG image", code: pngCode };
38
- return { b64, buffer, info };
25
+ const b64 = stripPngDataUrl(value).replace(/\s+/g, "");
26
+ if (!b64 || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) {
27
+ return { error: "image must be valid base64", code: invalidCode };
28
+ }
29
+ const buffer = Buffer.from(b64, "base64");
30
+ if (buffer.length === 0 || buffer.toString("base64").replace(/=+$/, "") !== b64.replace(/=+$/, "")) {
31
+ return { error: "image must be valid base64", code: invalidCode };
32
+ }
33
+ const info = parsePngInfo(buffer);
34
+ if (info.error)
35
+ return { error: "image must be a PNG image", code: pngCode };
36
+ return { b64, buffer, info };
39
37
  }
40
-
41
38
  function validateEditMask(imageB64, mask) {
42
- if (mask == null) return { mask: null, maskBytes: 0 };
43
- if (typeof mask !== "string" || mask.length === 0) {
44
- return { error: "mask must be a PNG data URL or base64 string", code: "INVALID_EDIT_MASK" };
45
- }
46
- const maskCheck = decodePngDataUrl(mask, "INVALID_EDIT_MASK_BASE64", "INVALID_EDIT_MASK_PNG");
47
- if (maskCheck.error) return maskCheck;
48
- if (maskCheck.buffer.length > MAX_EDIT_MASK_BYTES) {
49
- return { error: "mask is too large", code: "EDIT_MASK_TOO_LARGE" };
50
- }
51
- if (!hasPngAlphaChannel(maskCheck.info)) {
52
- return { error: "mask PNG must include an alpha channel", code: "EDIT_MASK_NO_ALPHA" };
53
- }
54
- const imageCheck = decodePngDataUrl(imageB64, "INVALID_EDIT_IMAGE_BASE64", "INVALID_EDIT_IMAGE_PNG");
55
- if (imageCheck.error) return imageCheck;
56
- if (imageCheck.info.width !== maskCheck.info.width || imageCheck.info.height !== maskCheck.info.height) {
57
- return { error: "mask dimensions must match image dimensions", code: "EDIT_MASK_DIMENSION_MISMATCH" };
58
- }
59
- return { mask: maskCheck.b64, maskBytes: maskCheck.buffer.length };
39
+ if (mask == null)
40
+ return { mask: null, maskBytes: 0 };
41
+ if (typeof mask !== "string" || mask.length === 0) {
42
+ return { error: "mask must be a PNG data URL or base64 string", code: "INVALID_EDIT_MASK" };
43
+ }
44
+ const maskCheck = decodePngDataUrl(mask, "INVALID_EDIT_MASK_BASE64", "INVALID_EDIT_MASK_PNG");
45
+ if (maskCheck.error)
46
+ return maskCheck;
47
+ if (maskCheck.buffer.length > MAX_EDIT_MASK_BYTES) {
48
+ return { error: "mask is too large", code: "EDIT_MASK_TOO_LARGE" };
49
+ }
50
+ if (!hasPngAlphaChannel(maskCheck.info)) {
51
+ return { error: "mask PNG must include an alpha channel", code: "EDIT_MASK_NO_ALPHA" };
52
+ }
53
+ const imageCheck = decodePngDataUrl(imageB64, "INVALID_EDIT_IMAGE_BASE64", "INVALID_EDIT_IMAGE_PNG");
54
+ if (imageCheck.error)
55
+ return imageCheck;
56
+ if (imageCheck.info.width !== maskCheck.info.width || imageCheck.info.height !== maskCheck.info.height) {
57
+ return { error: "mask dimensions must match image dimensions", code: "EDIT_MASK_DIMENSION_MISMATCH" };
58
+ }
59
+ return { mask: maskCheck.b64, maskBytes: maskCheck.buffer.length };
60
60
  }
61
-
62
61
  export function registerEditRoutes(app, ctx) {
63
- app.post("/api/edit", async (req, res) => {
64
- const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
65
- let finishStatus = "completed";
66
- let finishHttpStatus;
67
- let finishErrorCode;
68
- let finishMeta = {};
69
- try {
70
- const {
71
- prompt,
72
- image: imageB64,
73
- mask: rawMask,
74
- quality: rawQuality = "medium",
75
- size = "1024x1024",
76
- moderation = "low",
77
- provider = "oauth",
78
- mode: promptMode = "auto",
79
- model: rawModel,
80
- reasoningEffort: rawReasoningEffort,
81
- webSearchEnabled: rawWebSearchEnabled = true,
82
- } = req.body;
83
- const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
84
- const modelCheck = normalizeImageModel(ctx, rawModel);
85
- if (modelCheck.error) {
86
- finishStatus = "error";
87
- finishHttpStatus = modelCheck.status;
88
- finishErrorCode = modelCheck.code;
89
- return res.status(modelCheck.status).json({ error: modelCheck.error, code: modelCheck.code });
90
- }
91
- const imageModel = modelCheck.model;
92
- const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
93
- if (reasoningCheck.error) {
94
- finishStatus = "error";
95
- finishHttpStatus = reasoningCheck.status;
96
- finishErrorCode = reasoningCheck.code;
97
- return res.status(reasoningCheck.status).json({ error: reasoningCheck.error, code: reasoningCheck.code });
98
- }
99
- const reasoningEffort = reasoningCheck.effort;
100
- const webSearchEnabled = rawWebSearchEnabled !== false;
101
- const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
102
- const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
103
-
104
- startJob({
105
- requestId,
106
- kind: "classic",
107
- prompt,
108
- meta: {
109
- kind: "edit",
110
- sessionId,
111
- quality,
112
- model: imageModel,
113
- size,
114
- },
115
- });
116
-
117
- if (!prompt || !imageB64) {
118
- finishStatus = "error";
119
- finishHttpStatus = 400;
120
- finishErrorCode = "INVALID_EDIT_INPUT";
121
- return res.status(400).json({ error: "Prompt and image are required" });
122
- }
123
- const maskCheck = validateEditMask(imageB64, rawMask);
124
- if (maskCheck.error) {
125
- finishStatus = "error";
126
- finishHttpStatus = 400;
127
- finishErrorCode = maskCheck.code;
128
- return res.status(400).json({ error: maskCheck.error, code: maskCheck.code });
129
- }
130
- const moderationCheck = validateModeration(ctx, moderation);
131
- if (moderationCheck.error) {
132
- finishStatus = "error";
133
- finishHttpStatus = 400;
134
- finishErrorCode = "INVALID_MODERATION";
135
- return res.status(400).json({ error: moderationCheck.error });
136
- }
137
- if (provider === "api") {
138
- finishStatus = "error";
139
- finishHttpStatus = 403;
140
- finishErrorCode = "APIKEY_DISABLED";
141
- return res.status(403).json({ error: "API key provider is disabled. Use OAuth (Codex login).", code: "APIKEY_DISABLED" });
142
- }
143
-
144
- logEvent("edit", "request", {
145
- requestId,
146
- client: req.get("x-ima2-client") || "ui",
147
- provider: "oauth",
148
- quality,
149
- model: imageModel,
150
- size,
151
- moderation,
152
- sessionId,
153
- promptChars: typeof prompt === "string" ? prompt.length : 0,
154
- promptMode: normalizedPromptMode,
155
- webSearchEnabled,
156
- inputImageChars: typeof imageB64 === "string" ? imageB64.length : 0,
157
- maskPresent: Boolean(maskCheck.mask),
158
- maskBytes: maskCheck.maskBytes ?? 0,
159
- });
160
- const startTime = Date.now();
161
- const { b64: resultB64, usage, revisedPrompt, webSearchCalls = 0 } = await editViaOAuth(
162
- prompt,
163
- imageB64,
164
- quality,
165
- size,
166
- moderation,
167
- normalizedPromptMode,
168
- ctx,
169
- requestId,
170
- { model: imageModel, reasoningEffort, webSearchEnabled, mask: maskCheck.mask },
171
- );
172
-
173
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
174
- await mkdir(ctx.config.storage.generatedDir, { recursive: true });
175
- const filename = `${Date.now()}_${randomBytes(ctx.config.ids.generatedHexBytes).toString("hex")}.png`;
176
- await writeFile(join(ctx.config.storage.generatedDir, filename), Buffer.from(resultB64, "base64"));
177
- const meta = {
178
- prompt,
179
- userPrompt: prompt,
180
- revisedPrompt: revisedPrompt || null,
181
- promptMode: normalizedPromptMode,
182
- quality,
183
- size,
184
- moderation,
185
- model: imageModel,
186
- format: "png",
187
- provider: "oauth",
188
- kind: "edit",
189
- createdAt: Date.now(),
190
- usage: usage || null,
191
- webSearchCalls,
192
- webSearchEnabled,
193
- };
194
- await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => {});
195
- finishHttpStatus = 200;
196
- finishMeta = { filename, imageChars: resultB64.length };
197
- logEvent("edit", "saved", {
198
- requestId,
199
- filename,
200
- imageChars: resultB64.length,
201
- elapsedMs: Date.now() - startTime,
202
- });
203
-
204
- res.json({
205
- image: `data:image/png;base64,${resultB64}`,
206
- elapsed,
207
- filename,
208
- usage,
209
- provider: "oauth",
210
- model: imageModel,
211
- moderation,
212
- warnings: qualityWarnings,
213
- revisedPrompt: revisedPrompt || null,
214
- promptMode: normalizedPromptMode,
215
- webSearchCalls,
216
- webSearchEnabled,
217
- });
218
- } catch (err) {
219
- const fallbackCode = err.code || classifyUpstreamError(err.message);
220
- finishStatus = "error";
221
- finishHttpStatus = err.status || 500;
222
- finishErrorCode = fallbackCode || "EDIT_FAILED";
223
- logError("edit", "error", err, { requestId, code: finishErrorCode });
224
- res.status(err.status || 500).json({ error: err.message, code: fallbackCode });
225
- } finally {
226
- finishJob(requestId, {
227
- status: finishStatus,
228
- httpStatus: finishHttpStatus,
229
- errorCode: finishErrorCode,
230
- meta: finishMeta,
231
- });
232
- }
233
- });
62
+ app.post("/api/edit", async (req, res) => {
63
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
64
+ let finishStatus = "completed";
65
+ let finishHttpStatus;
66
+ let finishErrorCode;
67
+ let finishMeta = {};
68
+ try {
69
+ const { prompt, image: imageB64, mask: rawMask, quality: rawQuality = "medium", size = "1024x1024", moderation = "low", provider = "oauth", mode: promptMode = "auto", model: rawModel, reasoningEffort: rawReasoningEffort, webSearchEnabled: rawWebSearchEnabled = true, } = req.body;
70
+ const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
71
+ const providerOptions = resolveProviderOptions(ctx, {
72
+ provider,
73
+ rawModel,
74
+ rawReasoningEffort,
75
+ rawSize: size,
76
+ rawWebSearchEnabled,
77
+ });
78
+ if (providerOptions.error) {
79
+ finishStatus = "error";
80
+ finishHttpStatus = providerOptions.status;
81
+ finishErrorCode = providerOptions.code;
82
+ return res.status(providerOptions.status).json({ error: providerOptions.error, code: providerOptions.code });
83
+ }
84
+ const imageModel = providerOptions.model;
85
+ const reasoningEffort = providerOptions.reasoningEffort;
86
+ const effectiveSize = providerOptions.size;
87
+ const webSearchEnabled = providerOptions.webSearchEnabled;
88
+ const activeProvider = providerOptions.provider;
89
+ const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
90
+ const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
91
+ startJob({
92
+ requestId,
93
+ kind: "classic",
94
+ prompt,
95
+ meta: {
96
+ kind: "edit",
97
+ sessionId,
98
+ quality,
99
+ model: imageModel,
100
+ size: effectiveSize,
101
+ },
102
+ });
103
+ if (!prompt || !imageB64) {
104
+ finishStatus = "error";
105
+ finishHttpStatus = 400;
106
+ finishErrorCode = "INVALID_EDIT_INPUT";
107
+ return res.status(400).json({ error: "Prompt and image are required" });
108
+ }
109
+ const maskCheck = validateEditMask(imageB64, rawMask);
110
+ if (maskCheck.error) {
111
+ finishStatus = "error";
112
+ finishHttpStatus = 400;
113
+ finishErrorCode = maskCheck.code;
114
+ return res.status(400).json({ error: maskCheck.error, code: maskCheck.code });
115
+ }
116
+ const moderationCheck = validateModeration(ctx, moderation);
117
+ if (moderationCheck.error) {
118
+ finishStatus = "error";
119
+ finishHttpStatus = 400;
120
+ finishErrorCode = "INVALID_MODERATION";
121
+ return res.status(400).json({ error: moderationCheck.error });
122
+ }
123
+ logEvent("edit", "request", {
124
+ requestId,
125
+ client: req.get("x-ima2-client") || "ui",
126
+ provider: activeProvider,
127
+ quality,
128
+ model: imageModel,
129
+ size: effectiveSize,
130
+ moderation,
131
+ sessionId,
132
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
133
+ promptMode: normalizedPromptMode,
134
+ webSearchEnabled,
135
+ inputImageChars: typeof imageB64 === "string" ? imageB64.length : 0,
136
+ maskPresent: Boolean(maskCheck.mask),
137
+ maskBytes: maskCheck.maskBytes ?? 0,
138
+ });
139
+ const startTime = Date.now();
140
+ const { b64: resultB64, usage, revisedPrompt, webSearchCalls = 0 } = await editViaResponses(activeProvider, prompt, imageB64, quality, effectiveSize, moderation, normalizedPromptMode, ctx, requestId, { model: imageModel, reasoningEffort, webSearchEnabled, mask: maskCheck.mask });
141
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
142
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
143
+ const filename = `${Date.now()}_${randomBytes(ctx.config.ids.generatedHexBytes).toString("hex")}.png`;
144
+ await writeFile(join(ctx.config.storage.generatedDir, filename), Buffer.from(resultB64, "base64"));
145
+ const meta = {
146
+ prompt,
147
+ userPrompt: prompt,
148
+ revisedPrompt: revisedPrompt || null,
149
+ promptMode: normalizedPromptMode,
150
+ quality,
151
+ size: effectiveSize,
152
+ moderation,
153
+ model: imageModel,
154
+ format: "png",
155
+ provider: activeProvider,
156
+ kind: "edit",
157
+ createdAt: Date.now(),
158
+ usage: usage || null,
159
+ webSearchCalls,
160
+ webSearchEnabled,
161
+ };
162
+ await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => { });
163
+ finishHttpStatus = 200;
164
+ finishMeta = { filename, imageChars: resultB64.length };
165
+ logEvent("edit", "saved", {
166
+ requestId,
167
+ filename,
168
+ imageChars: resultB64.length,
169
+ elapsedMs: Date.now() - startTime,
170
+ });
171
+ res.json({
172
+ image: `data:image/png;base64,${resultB64}`,
173
+ elapsed,
174
+ filename,
175
+ usage,
176
+ provider: activeProvider,
177
+ model: imageModel,
178
+ moderation,
179
+ warnings: qualityWarnings,
180
+ revisedPrompt: revisedPrompt || null,
181
+ promptMode: normalizedPromptMode,
182
+ webSearchCalls,
183
+ webSearchEnabled,
184
+ });
185
+ }
186
+ catch (err) {
187
+ const fallbackCode = err.code || classifyUpstreamError(err.message);
188
+ finishStatus = "error";
189
+ finishHttpStatus = err.status || 500;
190
+ finishErrorCode = fallbackCode || "EDIT_FAILED";
191
+ logError("edit", "error", err, { requestId, code: finishErrorCode });
192
+ res.status(err.status || 500).json({ error: err.message, code: fallbackCode });
193
+ }
194
+ finally {
195
+ finishJob(requestId, {
196
+ status: finishStatus,
197
+ httpStatus: finishHttpStatus,
198
+ errorCode: finishErrorCode,
199
+ meta: finishMeta,
200
+ });
201
+ }
202
+ });
234
203
  }
package/routes/edit.ts ADDED
@@ -0,0 +1,230 @@
1
+ import { mkdir, writeFile } from "fs/promises";
2
+ import { join } from "path";
3
+ import { randomBytes } from "crypto";
4
+ import { classifyUpstreamError } from "../lib/errorClassify.js";
5
+ import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
6
+ import { resolveProviderOptions } from "../lib/providerOptions.js";
7
+ import { editViaResponses } from "../lib/responsesImageAdapter.js";
8
+ import { startJob, finishJob } from "../lib/inflight.js";
9
+ import { logEvent, logError } from "../lib/logger.js";
10
+ import { hasPngAlphaChannel, parsePngInfo } from "../lib/pngInfo.js";
11
+
12
+ function validateModeration(ctx, moderation) {
13
+ if (typeof moderation !== "string" || !ctx.config.oauth.validModeration.has(moderation)) {
14
+ return { error: "moderation must be one of: auto, low" };
15
+ }
16
+ return { moderation };
17
+ }
18
+
19
+ const MAX_EDIT_MASK_BYTES = 16 * 1024 * 1024;
20
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
21
+
22
+ function stripPngDataUrl(value) {
23
+ if (typeof value !== "string") return "";
24
+ return value.replace(/^data:image\/png;base64,/, "");
25
+ }
26
+
27
+ function decodePngDataUrl(value, invalidCode, pngCode) {
28
+ const b64 = stripPngDataUrl(value).replace(/\s+/g, "");
29
+ if (!b64 || b64.length % 4 !== 0 || !BASE64_RE.test(b64)) {
30
+ return { error: "image must be valid base64", code: invalidCode };
31
+ }
32
+ const buffer = Buffer.from(b64, "base64");
33
+ if (buffer.length === 0 || buffer.toString("base64").replace(/=+$/, "") !== b64.replace(/=+$/, "")) {
34
+ return { error: "image must be valid base64", code: invalidCode };
35
+ }
36
+ const info = parsePngInfo(buffer);
37
+ if (info.error) return { error: "image must be a PNG image", code: pngCode };
38
+ return { b64, buffer, info };
39
+ }
40
+
41
+ function validateEditMask(imageB64, mask) {
42
+ if (mask == null) return { mask: null, maskBytes: 0 };
43
+ if (typeof mask !== "string" || mask.length === 0) {
44
+ return { error: "mask must be a PNG data URL or base64 string", code: "INVALID_EDIT_MASK" };
45
+ }
46
+ const maskCheck = decodePngDataUrl(mask, "INVALID_EDIT_MASK_BASE64", "INVALID_EDIT_MASK_PNG");
47
+ if (maskCheck.error) return maskCheck;
48
+ if (maskCheck.buffer.length > MAX_EDIT_MASK_BYTES) {
49
+ return { error: "mask is too large", code: "EDIT_MASK_TOO_LARGE" };
50
+ }
51
+ if (!hasPngAlphaChannel(maskCheck.info)) {
52
+ return { error: "mask PNG must include an alpha channel", code: "EDIT_MASK_NO_ALPHA" };
53
+ }
54
+ const imageCheck = decodePngDataUrl(imageB64, "INVALID_EDIT_IMAGE_BASE64", "INVALID_EDIT_IMAGE_PNG");
55
+ if (imageCheck.error) return imageCheck;
56
+ if (imageCheck.info.width !== maskCheck.info.width || imageCheck.info.height !== maskCheck.info.height) {
57
+ return { error: "mask dimensions must match image dimensions", code: "EDIT_MASK_DIMENSION_MISMATCH" };
58
+ }
59
+ return { mask: maskCheck.b64, maskBytes: maskCheck.buffer.length };
60
+ }
61
+
62
+ export function registerEditRoutes(app, ctx) {
63
+ app.post("/api/edit", async (req, res) => {
64
+ const requestId = typeof req.body?.requestId === "string" ? req.body.requestId : req.id;
65
+ let finishStatus = "completed";
66
+ let finishHttpStatus;
67
+ let finishErrorCode;
68
+ let finishMeta = {};
69
+ try {
70
+ const {
71
+ prompt,
72
+ image: imageB64,
73
+ mask: rawMask,
74
+ quality: rawQuality = "medium",
75
+ size = "1024x1024",
76
+ moderation = "low",
77
+ provider = "oauth",
78
+ mode: promptMode = "auto",
79
+ model: rawModel,
80
+ reasoningEffort: rawReasoningEffort,
81
+ webSearchEnabled: rawWebSearchEnabled = true,
82
+ } = req.body;
83
+ const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
84
+ const providerOptions = resolveProviderOptions(ctx, {
85
+ provider,
86
+ rawModel,
87
+ rawReasoningEffort,
88
+ rawSize: size,
89
+ rawWebSearchEnabled,
90
+ });
91
+ if (providerOptions.error) {
92
+ finishStatus = "error";
93
+ finishHttpStatus = providerOptions.status;
94
+ finishErrorCode = providerOptions.code;
95
+ return res.status(providerOptions.status).json({ error: providerOptions.error, code: providerOptions.code });
96
+ }
97
+ const imageModel = providerOptions.model;
98
+ const reasoningEffort = providerOptions.reasoningEffort;
99
+ const effectiveSize = providerOptions.size;
100
+ const webSearchEnabled = providerOptions.webSearchEnabled;
101
+ const activeProvider = providerOptions.provider;
102
+ const sessionId = typeof req.body?.sessionId === "string" ? req.body.sessionId : null;
103
+ const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
104
+
105
+ startJob({
106
+ requestId,
107
+ kind: "classic",
108
+ prompt,
109
+ meta: {
110
+ kind: "edit",
111
+ sessionId,
112
+ quality,
113
+ model: imageModel,
114
+ size: effectiveSize,
115
+ },
116
+ });
117
+
118
+ if (!prompt || !imageB64) {
119
+ finishStatus = "error";
120
+ finishHttpStatus = 400;
121
+ finishErrorCode = "INVALID_EDIT_INPUT";
122
+ return res.status(400).json({ error: "Prompt and image are required" });
123
+ }
124
+ const maskCheck: any = validateEditMask(imageB64, rawMask);
125
+ if (maskCheck.error) {
126
+ finishStatus = "error";
127
+ finishHttpStatus = 400;
128
+ finishErrorCode = maskCheck.code;
129
+ return res.status(400).json({ error: maskCheck.error, code: maskCheck.code });
130
+ }
131
+ const moderationCheck = validateModeration(ctx, moderation);
132
+ if (moderationCheck.error) {
133
+ finishStatus = "error";
134
+ finishHttpStatus = 400;
135
+ finishErrorCode = "INVALID_MODERATION";
136
+ return res.status(400).json({ error: moderationCheck.error });
137
+ }
138
+
139
+ logEvent("edit", "request", {
140
+ requestId,
141
+ client: req.get("x-ima2-client") || "ui",
142
+ provider: activeProvider,
143
+ quality,
144
+ model: imageModel,
145
+ size: effectiveSize,
146
+ moderation,
147
+ sessionId,
148
+ promptChars: typeof prompt === "string" ? prompt.length : 0,
149
+ promptMode: normalizedPromptMode,
150
+ webSearchEnabled,
151
+ inputImageChars: typeof imageB64 === "string" ? imageB64.length : 0,
152
+ maskPresent: Boolean(maskCheck.mask),
153
+ maskBytes: maskCheck.maskBytes ?? 0,
154
+ });
155
+ const startTime = Date.now();
156
+ const { b64: resultB64, usage, revisedPrompt, webSearchCalls = 0 } = await editViaResponses(
157
+ activeProvider,
158
+ prompt,
159
+ imageB64,
160
+ quality,
161
+ effectiveSize,
162
+ moderation,
163
+ normalizedPromptMode,
164
+ ctx,
165
+ requestId,
166
+ { model: imageModel, reasoningEffort, webSearchEnabled, mask: maskCheck.mask },
167
+ );
168
+
169
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
170
+ await mkdir(ctx.config.storage.generatedDir, { recursive: true });
171
+ const filename = `${Date.now()}_${randomBytes(ctx.config.ids.generatedHexBytes).toString("hex")}.png`;
172
+ await writeFile(join(ctx.config.storage.generatedDir, filename), Buffer.from(resultB64, "base64"));
173
+ const meta = {
174
+ prompt,
175
+ userPrompt: prompt,
176
+ revisedPrompt: revisedPrompt || null,
177
+ promptMode: normalizedPromptMode,
178
+ quality,
179
+ size: effectiveSize,
180
+ moderation,
181
+ model: imageModel,
182
+ format: "png",
183
+ provider: activeProvider,
184
+ kind: "edit",
185
+ createdAt: Date.now(),
186
+ usage: usage || null,
187
+ webSearchCalls,
188
+ webSearchEnabled,
189
+ };
190
+ await writeFile(join(ctx.config.storage.generatedDir, filename + ".json"), JSON.stringify(meta)).catch(() => {});
191
+ finishHttpStatus = 200;
192
+ finishMeta = { filename, imageChars: resultB64.length };
193
+ logEvent("edit", "saved", {
194
+ requestId,
195
+ filename,
196
+ imageChars: resultB64.length,
197
+ elapsedMs: Date.now() - startTime,
198
+ });
199
+
200
+ res.json({
201
+ image: `data:image/png;base64,${resultB64}`,
202
+ elapsed,
203
+ filename,
204
+ usage,
205
+ provider: activeProvider,
206
+ model: imageModel,
207
+ moderation,
208
+ warnings: qualityWarnings,
209
+ revisedPrompt: revisedPrompt || null,
210
+ promptMode: normalizedPromptMode,
211
+ webSearchCalls,
212
+ webSearchEnabled,
213
+ });
214
+ } catch (err) {
215
+ const fallbackCode = err.code || classifyUpstreamError(err.message);
216
+ finishStatus = "error";
217
+ finishHttpStatus = err.status || 500;
218
+ finishErrorCode = fallbackCode || "EDIT_FAILED";
219
+ logError("edit", "error", err, { requestId, code: finishErrorCode });
220
+ res.status(err.status || 500).json({ error: err.message, code: fallbackCode });
221
+ } finally {
222
+ finishJob(requestId, {
223
+ status: finishStatus,
224
+ httpStatus: finishHttpStatus,
225
+ errorCode: finishErrorCode,
226
+ meta: finishMeta,
227
+ });
228
+ }
229
+ });
230
+ }