ima2-gen 1.1.7 → 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.
- package/README.md +56 -27
- package/bin/commands/annotate.js +137 -0
- package/bin/commands/annotate.ts +118 -0
- package/bin/commands/cancel.js +37 -33
- package/bin/commands/cancel.ts +45 -0
- package/bin/commands/canvas-versions.js +91 -0
- package/bin/commands/canvas-versions.ts +80 -0
- package/bin/commands/cardnews.js +293 -0
- package/bin/commands/cardnews.ts +248 -0
- package/bin/commands/comfy.js +63 -0
- package/bin/commands/comfy.ts +54 -0
- package/bin/commands/config.js +270 -0
- package/bin/commands/config.ts +265 -0
- package/bin/commands/edit.js +97 -72
- package/bin/commands/edit.ts +116 -0
- package/bin/commands/gen.js +140 -118
- package/bin/commands/gen.ts +176 -0
- package/bin/commands/history.js +164 -0
- package/bin/commands/history.ts +145 -0
- package/bin/commands/ls.js +60 -42
- package/bin/commands/ls.ts +60 -0
- package/bin/commands/metadata.js +45 -0
- package/bin/commands/metadata.ts +36 -0
- package/bin/commands/multimode.js +159 -0
- package/bin/commands/multimode.ts +146 -0
- package/bin/commands/node.js +176 -0
- package/bin/commands/node.ts +157 -0
- package/bin/commands/observability.js +201 -0
- package/bin/commands/observability.ts +176 -0
- package/bin/commands/ping.js +26 -20
- package/bin/commands/ping.ts +29 -0
- package/bin/commands/prompt.js +506 -0
- package/bin/commands/prompt.ts +421 -0
- package/bin/commands/ps.js +78 -71
- package/bin/commands/ps.ts +78 -0
- package/bin/commands/session.js +308 -0
- package/bin/commands/session.ts +265 -0
- package/bin/commands/show.js +75 -40
- package/bin/commands/show.ts +69 -0
- package/bin/ima2.js +324 -310
- package/bin/ima2.ts +444 -0
- package/bin/lib/args.js +75 -66
- package/bin/lib/args.ts +73 -0
- package/bin/lib/browser-id.js +15 -0
- package/bin/lib/browser-id.ts +16 -0
- package/bin/lib/client.js +91 -83
- package/bin/lib/client.ts +109 -0
- package/bin/lib/error-hints.js +14 -17
- package/bin/lib/error-hints.ts +23 -0
- package/bin/lib/files.js +26 -28
- package/bin/lib/files.ts +39 -0
- package/bin/lib/output.js +44 -42
- package/bin/lib/output.ts +58 -0
- package/bin/lib/platform.js +60 -56
- package/bin/lib/platform.ts +97 -0
- package/bin/lib/sse.js +73 -0
- package/bin/lib/sse.ts +73 -0
- package/bin/lib/star-prompt.js +69 -76
- package/bin/lib/star-prompt.ts +97 -0
- package/bin/lib/storage-doctor.js +34 -35
- package/bin/lib/storage-doctor.ts +38 -0
- package/config.js +147 -190
- package/config.ts +331 -0
- package/docs/API.md +48 -8
- package/docs/CLI.md +190 -0
- package/docs/FAQ.ko.md +5 -5
- package/docs/FAQ.md +5 -5
- package/docs/README.ja.md +71 -25
- package/docs/README.ko.md +61 -24
- package/docs/README.zh-CN.md +73 -27
- package/lib/assetLifecycle.js +130 -130
- package/lib/assetLifecycle.ts +142 -0
- package/lib/canvasVersionStore.js +135 -153
- package/lib/canvasVersionStore.ts +181 -0
- package/lib/cardNewsGenerator.js +127 -142
- package/lib/cardNewsGenerator.ts +162 -0
- package/lib/cardNewsJobStore.js +78 -84
- package/lib/cardNewsJobStore.ts +107 -0
- package/lib/cardNewsManifestStore.js +88 -93
- package/lib/cardNewsManifestStore.ts +112 -0
- package/lib/cardNewsPlanner.js +157 -152
- package/lib/cardNewsPlanner.ts +180 -0
- package/lib/cardNewsPlannerClient.js +101 -98
- package/lib/cardNewsPlannerClient.ts +114 -0
- package/lib/cardNewsPlannerPrompt.js +56 -56
- package/lib/cardNewsPlannerPrompt.ts +60 -0
- package/lib/cardNewsPlannerSchema.js +231 -223
- package/lib/cardNewsPlannerSchema.ts +259 -0
- package/lib/cardNewsRoleTemplateStore.js +39 -41
- package/lib/cardNewsRoleTemplateStore.ts +47 -0
- package/lib/cardNewsTemplateStore.js +171 -175
- package/lib/cardNewsTemplateStore.ts +210 -0
- package/lib/codexDetect.js +44 -47
- package/lib/codexDetect.ts +69 -0
- package/lib/comfyBridge.js +164 -184
- package/lib/comfyBridge.ts +214 -0
- package/lib/db.js +41 -51
- package/lib/db.ts +166 -0
- package/lib/errorClassify.js +62 -78
- package/lib/errorClassify.ts +100 -0
- package/lib/generationErrors.js +140 -103
- package/lib/generationErrors.ts +125 -0
- package/lib/historyList.js +149 -147
- package/lib/historyList.ts +164 -0
- package/lib/imageMetadata.js +86 -89
- package/lib/imageMetadata.ts +111 -0
- package/lib/imageMetadataStore.js +46 -51
- package/lib/imageMetadataStore.ts +67 -0
- package/lib/imageModels.js +38 -45
- package/lib/imageModels.ts +52 -0
- package/lib/inflight.js +131 -150
- package/lib/inflight.ts +204 -0
- package/lib/localImportStore.js +105 -0
- package/lib/localImportStore.ts +111 -0
- package/lib/logger.js +105 -112
- package/lib/logger.ts +150 -0
- package/lib/nodeStore.js +65 -64
- package/lib/nodeStore.ts +81 -0
- package/lib/oauthLauncher.js +61 -59
- package/lib/oauthLauncher.ts +64 -0
- package/lib/oauthNormalize.js +15 -19
- package/lib/oauthNormalize.ts +30 -0
- package/lib/oauthProxy.js +834 -832
- package/lib/oauthProxy.ts +995 -0
- package/lib/openDirectory.js +41 -40
- package/lib/openDirectory.ts +45 -0
- package/lib/pngInfo.js +18 -20
- package/lib/pngInfo.ts +26 -0
- package/lib/promptImport/curatedSources.js +135 -0
- package/lib/promptImport/curatedSources.ts +139 -0
- package/lib/promptImport/discoveryRegistry.js +218 -0
- package/lib/promptImport/discoveryRegistry.ts +236 -0
- package/lib/promptImport/errors.js +10 -10
- package/lib/promptImport/errors.ts +18 -0
- package/lib/promptImport/githubDiscovery.js +238 -0
- package/lib/promptImport/githubDiscovery.ts +248 -0
- package/lib/promptImport/githubFolder.js +302 -0
- package/lib/promptImport/githubFolder.ts +308 -0
- package/lib/promptImport/githubSource.js +194 -171
- package/lib/promptImport/githubSource.ts +239 -0
- package/lib/promptImport/gptImageHints.js +61 -0
- package/lib/promptImport/gptImageHints.ts +68 -0
- package/lib/promptImport/parsePromptCandidates.js +110 -112
- package/lib/promptImport/parsePromptCandidates.ts +153 -0
- package/lib/promptImport/promptIndex.js +230 -0
- package/lib/promptImport/promptIndex.ts +248 -0
- package/lib/promptImport/rankPromptCandidates.js +52 -0
- package/lib/promptImport/rankPromptCandidates.ts +49 -0
- package/lib/providerOptions.js +31 -0
- package/lib/providerOptions.ts +41 -0
- package/lib/referenceImageCompress.js +51 -62
- package/lib/referenceImageCompress.ts +75 -0
- package/lib/refs.js +93 -81
- package/lib/refs.ts +117 -0
- package/lib/requestLogger.js +32 -38
- package/lib/requestLogger.ts +48 -0
- package/lib/responsesImageAdapter.js +351 -0
- package/lib/responsesImageAdapter.ts +352 -0
- package/lib/runtimePorts.js +71 -73
- package/lib/runtimePorts.ts +93 -0
- package/lib/sessionStore.js +179 -230
- package/lib/sessionStore.ts +272 -0
- package/lib/storageMigration.js +247 -245
- package/lib/storageMigration.ts +284 -0
- package/lib/styleSheet.js +86 -90
- package/lib/styleSheet.ts +128 -0
- package/lib/systemTrash.js +18 -0
- package/lib/systemTrash.ts +20 -0
- package/package.json +26 -10
- package/routes/annotations.js +76 -79
- package/routes/annotations.ts +95 -0
- package/routes/canvasVersions.js +50 -54
- package/routes/canvasVersions.ts +64 -0
- package/routes/cardNews.js +158 -171
- package/routes/cardNews.ts +183 -0
- package/routes/comfy.js +23 -31
- package/routes/comfy.ts +39 -0
- package/routes/edit.js +183 -214
- package/routes/edit.ts +230 -0
- package/routes/generate.js +269 -291
- package/routes/generate.ts +309 -0
- package/routes/health.js +102 -107
- package/routes/health.ts +114 -0
- package/routes/history.js +136 -144
- package/routes/history.ts +153 -0
- package/routes/imageImport.js +33 -0
- package/routes/imageImport.ts +33 -0
- package/routes/index.js +18 -16
- package/routes/index.ts +35 -0
- package/routes/metadata.js +60 -64
- package/routes/metadata.ts +71 -0
- package/routes/multimode.js +228 -263
- package/routes/multimode.ts +280 -0
- package/routes/nodes.js +378 -424
- package/routes/nodes.ts +455 -0
- package/routes/promptImport.js +291 -152
- package/routes/promptImport.ts +354 -0
- package/routes/prompts.js +333 -360
- package/routes/prompts.ts +379 -0
- package/routes/sessions.js +277 -285
- package/routes/sessions.ts +292 -0
- package/routes/storage.js +29 -31
- package/routes/storage.ts +39 -0
- package/server.js +189 -196
- package/server.ts +235 -0
- package/ui/dist/.vite/manifest.json +101 -0
- package/ui/dist/assets/CardNewsWorkspace-BJOCey7Z.js +2 -0
- package/ui/dist/assets/NodeCanvas-BZV40eAE.css +1 -0
- package/ui/dist/assets/NodeCanvas-C3dzYNsk.js +7 -0
- package/ui/dist/assets/PromptImportDialog-Dqu1VpUh.js +2 -0
- package/ui/dist/assets/PromptImportDiscoverySection-Dg8T9X0L.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-DBaqsFO4.js +1 -0
- package/ui/dist/assets/PromptLibraryPanel-p5QqR97M.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-B5bSAZ6u.js +1 -0
- package/ui/dist/assets/index-C9cXwiWE.js +25 -0
- package/ui/dist/assets/index-CGMIkZXn.css +1 -0
- package/ui/dist/assets/index-Cvld7dUZ.js +1 -0
- package/ui/dist/index.html +6 -3
- package/assets/screenshot.png +0 -0
- package/assets/screenshots/classic-generate-light.png +0 -0
- package/assets/screenshots/node-graph-branching.png +0 -0
- package/assets/screenshots/settings-oauth-generation.png +0 -0
- package/assets/screenshots/settings-workspace.png +0 -0
- package/assets/screenshots/style-sheet-editor.png +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/ui/dist/assets/index-DARPdT4Q.css +0 -1
- package/ui/dist/assets/index-ht80GMq4.js +0 -31
- package/ui/dist/assets/index-ht80GMq4.js.map +0 -1
|
@@ -3,179 +3,161 @@ import { constants } from "fs";
|
|
|
3
3
|
import { basename, join, normalize, parse } from "path";
|
|
4
4
|
import { randomBytes } from "crypto";
|
|
5
5
|
import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
|
|
6
|
-
|
|
7
6
|
const PNG_SIGNATURE = "89504e470d0a1a0a";
|
|
8
|
-
|
|
9
7
|
function assertPngBuffer(buffer) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
9
|
+
const err = new Error("PNG body is required");
|
|
10
|
+
err.status = 400;
|
|
11
|
+
err.code = "EMPTY_CANVAS_VERSION";
|
|
12
|
+
throw err;
|
|
13
|
+
}
|
|
14
|
+
if (buffer.subarray(0, 8).toString("hex") !== PNG_SIGNATURE) {
|
|
15
|
+
const err = new Error("Canvas version body must be a PNG image");
|
|
16
|
+
err.status = 400;
|
|
17
|
+
err.code = "CANVAS_VERSION_NOT_PNG";
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
22
20
|
}
|
|
23
|
-
|
|
24
21
|
function assertSafeFilename(filename) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw err;
|
|
36
|
-
}
|
|
22
|
+
if (typeof filename !== "string" ||
|
|
23
|
+
filename.length === 0 ||
|
|
24
|
+
filename !== basename(filename) ||
|
|
25
|
+
filename.includes("..") ||
|
|
26
|
+
!/^canvas-[a-zA-Z0-9._-]+\.png$/.test(filename)) {
|
|
27
|
+
const err = new Error("Invalid canvas version filename");
|
|
28
|
+
err.status = 400;
|
|
29
|
+
err.code = "INVALID_CANVAS_VERSION_FILENAME";
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
37
32
|
}
|
|
38
|
-
|
|
39
33
|
function safeSourceBase(sourceFilename) {
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
const parsed = parse(basename(String(sourceFilename || "image")));
|
|
35
|
+
return parsed.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "image";
|
|
42
36
|
}
|
|
43
|
-
|
|
44
37
|
function ensureInsideGeneratedDir(generatedDir, filename) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
const full = normalize(join(generatedDir, filename));
|
|
39
|
+
const root = normalize(generatedDir);
|
|
40
|
+
if (!full.startsWith(root)) {
|
|
41
|
+
const err = new Error("Canvas version path escapes generated directory");
|
|
42
|
+
err.status = 400;
|
|
43
|
+
err.code = "CANVAS_VERSION_PATH_ESCAPE";
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
return full;
|
|
54
47
|
}
|
|
55
|
-
|
|
56
48
|
function makeCanvasFilename(sourceFilename) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
|
50
|
+
const rand = randomBytes(3).toString("hex");
|
|
51
|
+
return `canvas-${safeSourceBase(sourceFilename)}-${stamp}-${rand}.png`;
|
|
60
52
|
}
|
|
61
|
-
|
|
62
53
|
async function writeCanvasPng(ctx, filename, buffer, meta) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
55
|
+
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
|
|
56
|
+
const embedded = await embedImageMetadataBestEffort(buffer, "png", meta, {
|
|
57
|
+
version: ctx.packageVersion,
|
|
58
|
+
});
|
|
59
|
+
await writeFile(full, embedded.buffer);
|
|
60
|
+
await writeFile(`${full}.json`, JSON.stringify(meta)).catch(() => { });
|
|
70
61
|
}
|
|
71
|
-
|
|
72
62
|
async function readGeneratedMetadata(ctx, filename) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
if (!filename)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, basename(filename));
|
|
67
|
+
return JSON.parse(await readFile(`${full}.json`, "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
80
72
|
}
|
|
81
|
-
|
|
82
73
|
function firstString(...values) {
|
|
83
|
-
|
|
74
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0) ?? null;
|
|
84
75
|
}
|
|
85
|
-
|
|
86
76
|
function toGenerateItem(filename, meta) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
77
|
+
const url = `/generated/${encodeURIComponent(filename)}`;
|
|
78
|
+
return {
|
|
79
|
+
image: url,
|
|
80
|
+
url,
|
|
81
|
+
thumb: url,
|
|
82
|
+
filename,
|
|
83
|
+
prompt: meta.prompt || undefined,
|
|
84
|
+
userPrompt: meta.userPrompt || meta.prompt || null,
|
|
85
|
+
revisedPrompt: null,
|
|
86
|
+
promptMode: meta.promptMode || "direct",
|
|
87
|
+
provider: meta.provider || "canvas",
|
|
88
|
+
quality: meta.quality || null,
|
|
89
|
+
size: meta.size || null,
|
|
90
|
+
format: "png",
|
|
91
|
+
moderation: meta.moderation || null,
|
|
92
|
+
model: meta.model || null,
|
|
93
|
+
usage: null,
|
|
94
|
+
createdAt: meta.createdAt,
|
|
95
|
+
kind: "edit",
|
|
96
|
+
canvasMergedAt: meta.canvasMergedAt,
|
|
97
|
+
canvasVersion: true,
|
|
98
|
+
canvasSourceFilename: meta.canvasSourceFilename || null,
|
|
99
|
+
canvasEditableFilename: filename,
|
|
100
|
+
};
|
|
111
101
|
}
|
|
112
|
-
|
|
113
102
|
export async function createCanvasVersion(ctx, input) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
103
|
+
assertPngBuffer(input.buffer);
|
|
104
|
+
const sourceFilename = basename(String(input.sourceFilename || ""));
|
|
105
|
+
if (!sourceFilename) {
|
|
106
|
+
const err = new Error("sourceFilename is required");
|
|
107
|
+
err.status = 400;
|
|
108
|
+
err.code = "CANVAS_SOURCE_REQUIRED";
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
const filename = makeCanvasFilename(sourceFilename);
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
|
|
114
|
+
const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt);
|
|
115
|
+
const meta = {
|
|
116
|
+
kind: "edit",
|
|
117
|
+
provider: "canvas",
|
|
118
|
+
format: "png",
|
|
119
|
+
prompt,
|
|
120
|
+
userPrompt: prompt,
|
|
121
|
+
promptMode: sourceMeta?.promptMode || "direct",
|
|
122
|
+
createdAt: now,
|
|
123
|
+
canvasMergedAt: now,
|
|
124
|
+
canvasVersion: true,
|
|
125
|
+
canvasSourceFilename: sourceFilename,
|
|
126
|
+
canvasEditableFilename: filename,
|
|
127
|
+
};
|
|
128
|
+
await writeCanvasPng(ctx, filename, input.buffer, meta);
|
|
129
|
+
return toGenerateItem(filename, meta);
|
|
141
130
|
}
|
|
142
|
-
|
|
143
131
|
export async function updateCanvasVersion(ctx, filename, input) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
canvasVersion: true,
|
|
176
|
-
canvasSourceFilename: sourceFilename,
|
|
177
|
-
canvasEditableFilename: filename,
|
|
178
|
-
};
|
|
179
|
-
await writeCanvasPng(ctx, filename, input.buffer, meta);
|
|
180
|
-
return toGenerateItem(filename, meta);
|
|
132
|
+
assertSafeFilename(filename);
|
|
133
|
+
assertPngBuffer(input.buffer);
|
|
134
|
+
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
|
|
135
|
+
await access(full, constants.F_OK).catch(() => {
|
|
136
|
+
const err = new Error("Canvas version not found");
|
|
137
|
+
err.status = 404;
|
|
138
|
+
err.code = "CANVAS_VERSION_NOT_FOUND";
|
|
139
|
+
throw err;
|
|
140
|
+
});
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const sourceFilename = typeof input.sourceFilename === "string"
|
|
143
|
+
? basename(input.sourceFilename)
|
|
144
|
+
: null;
|
|
145
|
+
const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
|
|
146
|
+
const previousMeta = await readGeneratedMetadata(ctx, filename);
|
|
147
|
+
const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt, previousMeta?.userPrompt, previousMeta?.prompt);
|
|
148
|
+
const meta = {
|
|
149
|
+
kind: "edit",
|
|
150
|
+
provider: "canvas",
|
|
151
|
+
format: "png",
|
|
152
|
+
prompt,
|
|
153
|
+
userPrompt: prompt,
|
|
154
|
+
promptMode: sourceMeta?.promptMode || previousMeta?.promptMode || "direct",
|
|
155
|
+
createdAt: now,
|
|
156
|
+
canvasMergedAt: now,
|
|
157
|
+
canvasVersion: true,
|
|
158
|
+
canvasSourceFilename: sourceFilename,
|
|
159
|
+
canvasEditableFilename: filename,
|
|
160
|
+
};
|
|
161
|
+
await writeCanvasPng(ctx, filename, input.buffer, meta);
|
|
162
|
+
return toGenerateItem(filename, meta);
|
|
181
163
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { mkdir, writeFile, access, readFile } from "fs/promises";
|
|
2
|
+
import { constants } from "fs";
|
|
3
|
+
import { basename, join, normalize, parse } from "path";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
import { embedImageMetadataBestEffort } from "./imageMetadataStore.js";
|
|
6
|
+
|
|
7
|
+
const PNG_SIGNATURE = "89504e470d0a1a0a";
|
|
8
|
+
|
|
9
|
+
function assertPngBuffer(buffer) {
|
|
10
|
+
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
11
|
+
const err: any = new Error("PNG body is required");
|
|
12
|
+
err.status = 400;
|
|
13
|
+
err.code = "EMPTY_CANVAS_VERSION";
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
if (buffer.subarray(0, 8).toString("hex") !== PNG_SIGNATURE) {
|
|
17
|
+
const err: any = new Error("Canvas version body must be a PNG image");
|
|
18
|
+
err.status = 400;
|
|
19
|
+
err.code = "CANVAS_VERSION_NOT_PNG";
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertSafeFilename(filename) {
|
|
25
|
+
if (
|
|
26
|
+
typeof filename !== "string" ||
|
|
27
|
+
filename.length === 0 ||
|
|
28
|
+
filename !== basename(filename) ||
|
|
29
|
+
filename.includes("..") ||
|
|
30
|
+
!/^canvas-[a-zA-Z0-9._-]+\.png$/.test(filename)
|
|
31
|
+
) {
|
|
32
|
+
const err: any = new Error("Invalid canvas version filename");
|
|
33
|
+
err.status = 400;
|
|
34
|
+
err.code = "INVALID_CANVAS_VERSION_FILENAME";
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeSourceBase(sourceFilename) {
|
|
40
|
+
const parsed = parse(basename(String(sourceFilename || "image")));
|
|
41
|
+
return parsed.name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "image";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureInsideGeneratedDir(generatedDir, filename) {
|
|
45
|
+
const full = normalize(join(generatedDir, filename));
|
|
46
|
+
const root = normalize(generatedDir);
|
|
47
|
+
if (!full.startsWith(root)) {
|
|
48
|
+
const err: any = new Error("Canvas version path escapes generated directory");
|
|
49
|
+
err.status = 400;
|
|
50
|
+
err.code = "CANVAS_VERSION_PATH_ESCAPE";
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
return full;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeCanvasFilename(sourceFilename) {
|
|
57
|
+
const stamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
|
|
58
|
+
const rand = randomBytes(3).toString("hex");
|
|
59
|
+
return `canvas-${safeSourceBase(sourceFilename)}-${stamp}-${rand}.png`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function writeCanvasPng(ctx, filename, buffer, meta) {
|
|
63
|
+
await mkdir(ctx.config.storage.generatedDir, { recursive: true });
|
|
64
|
+
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
|
|
65
|
+
const embedded = await embedImageMetadataBestEffort(buffer, "png", meta, {
|
|
66
|
+
version: ctx.packageVersion,
|
|
67
|
+
});
|
|
68
|
+
await writeFile(full, embedded.buffer);
|
|
69
|
+
await writeFile(`${full}.json`, JSON.stringify(meta)).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readGeneratedMetadata(ctx, filename) {
|
|
73
|
+
if (!filename) return null;
|
|
74
|
+
try {
|
|
75
|
+
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, basename(filename));
|
|
76
|
+
return JSON.parse(await readFile(`${full}.json`, "utf8"));
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function firstString(...values) {
|
|
83
|
+
return values.find((value) => typeof value === "string" && value.trim().length > 0) ?? null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toGenerateItem(filename, meta) {
|
|
87
|
+
const url = `/generated/${encodeURIComponent(filename)}`;
|
|
88
|
+
return {
|
|
89
|
+
image: url,
|
|
90
|
+
url,
|
|
91
|
+
thumb: url,
|
|
92
|
+
filename,
|
|
93
|
+
prompt: meta.prompt || undefined,
|
|
94
|
+
userPrompt: meta.userPrompt || meta.prompt || null,
|
|
95
|
+
revisedPrompt: null,
|
|
96
|
+
promptMode: meta.promptMode || "direct",
|
|
97
|
+
provider: meta.provider || "canvas",
|
|
98
|
+
quality: meta.quality || null,
|
|
99
|
+
size: meta.size || null,
|
|
100
|
+
format: "png",
|
|
101
|
+
moderation: meta.moderation || null,
|
|
102
|
+
model: meta.model || null,
|
|
103
|
+
usage: null,
|
|
104
|
+
createdAt: meta.createdAt,
|
|
105
|
+
kind: "edit",
|
|
106
|
+
canvasMergedAt: meta.canvasMergedAt,
|
|
107
|
+
canvasVersion: true,
|
|
108
|
+
canvasSourceFilename: meta.canvasSourceFilename || null,
|
|
109
|
+
canvasEditableFilename: filename,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function createCanvasVersion(ctx, input) {
|
|
114
|
+
assertPngBuffer(input.buffer);
|
|
115
|
+
const sourceFilename = basename(String(input.sourceFilename || ""));
|
|
116
|
+
if (!sourceFilename) {
|
|
117
|
+
const err: any = new Error("sourceFilename is required");
|
|
118
|
+
err.status = 400;
|
|
119
|
+
err.code = "CANVAS_SOURCE_REQUIRED";
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
const filename = makeCanvasFilename(sourceFilename);
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
|
|
125
|
+
const prompt = firstString(input.prompt, sourceMeta?.userPrompt, sourceMeta?.prompt);
|
|
126
|
+
const meta = {
|
|
127
|
+
kind: "edit",
|
|
128
|
+
provider: "canvas",
|
|
129
|
+
format: "png",
|
|
130
|
+
prompt,
|
|
131
|
+
userPrompt: prompt,
|
|
132
|
+
promptMode: sourceMeta?.promptMode || "direct",
|
|
133
|
+
createdAt: now,
|
|
134
|
+
canvasMergedAt: now,
|
|
135
|
+
canvasVersion: true,
|
|
136
|
+
canvasSourceFilename: sourceFilename,
|
|
137
|
+
canvasEditableFilename: filename,
|
|
138
|
+
};
|
|
139
|
+
await writeCanvasPng(ctx, filename, input.buffer, meta);
|
|
140
|
+
return toGenerateItem(filename, meta);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function updateCanvasVersion(ctx, filename, input) {
|
|
144
|
+
assertSafeFilename(filename);
|
|
145
|
+
assertPngBuffer(input.buffer);
|
|
146
|
+
const full = ensureInsideGeneratedDir(ctx.config.storage.generatedDir, filename);
|
|
147
|
+
await access(full, constants.F_OK).catch(() => {
|
|
148
|
+
const err: any = new Error("Canvas version not found");
|
|
149
|
+
err.status = 404;
|
|
150
|
+
err.code = "CANVAS_VERSION_NOT_FOUND";
|
|
151
|
+
throw err;
|
|
152
|
+
});
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const sourceFilename = typeof input.sourceFilename === "string"
|
|
155
|
+
? basename(input.sourceFilename)
|
|
156
|
+
: null;
|
|
157
|
+
const sourceMeta = await readGeneratedMetadata(ctx, sourceFilename);
|
|
158
|
+
const previousMeta = await readGeneratedMetadata(ctx, filename);
|
|
159
|
+
const prompt = firstString(
|
|
160
|
+
input.prompt,
|
|
161
|
+
sourceMeta?.userPrompt,
|
|
162
|
+
sourceMeta?.prompt,
|
|
163
|
+
previousMeta?.userPrompt,
|
|
164
|
+
previousMeta?.prompt,
|
|
165
|
+
);
|
|
166
|
+
const meta = {
|
|
167
|
+
kind: "edit",
|
|
168
|
+
provider: "canvas",
|
|
169
|
+
format: "png",
|
|
170
|
+
prompt,
|
|
171
|
+
userPrompt: prompt,
|
|
172
|
+
promptMode: sourceMeta?.promptMode || previousMeta?.promptMode || "direct",
|
|
173
|
+
createdAt: now,
|
|
174
|
+
canvasMergedAt: now,
|
|
175
|
+
canvasVersion: true,
|
|
176
|
+
canvasSourceFilename: sourceFilename,
|
|
177
|
+
canvasEditableFilename: filename,
|
|
178
|
+
};
|
|
179
|
+
await writeCanvasPng(ctx, filename, input.buffer, meta);
|
|
180
|
+
return toGenerateItem(filename, meta);
|
|
181
|
+
}
|