ima2-gen 1.1.21 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -7
- package/bin/commands/video.js +14 -0
- package/bin/ima2.js +14 -4
- package/bin/lib/platform.js +34 -5
- package/docs/README.ko.md +43 -2
- package/lib/agentQueueWorker.js +6 -0
- package/lib/agentRuntime.js +3 -2
- package/lib/atomicWrite.js +14 -0
- package/lib/grokImageAdapter.js +6 -0
- package/lib/grokProxyLauncher.js +5 -3
- package/lib/grokVideoAdapter.js +1 -1
- package/lib/grokVideoPlannerPrompt.js +10 -0
- package/lib/inflight.js +1 -1
- package/lib/oauthLauncher.js +5 -0
- package/lib/videoFrameExtract.js +3 -3
- package/package.json +5 -7
- package/routes/capabilities.js +13 -0
- package/routes/edit.js +2 -1
- package/routes/generate.js +32 -6
- package/routes/health.js +4 -3
- package/routes/multimode.js +2 -1
- package/routes/video.js +35 -3
- package/server.js +29 -2
- package/skills/ima2/SKILL.md +48 -6
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-B_hq9CLg.js → AgentWorkspace-C21zqdTZ.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-wD12J7qk.js → CardNewsWorkspace-BN-ga1lG.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-CI_wuPMf.js → NodeCanvas-BbMa4IhI.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-CUTujJUV.js → PromptBuilderPanel-DRwBJRDQ.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-CUi66jPK.js → PromptImportDialog-Dp85kHCq.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-Cm3vrjY4.js → PromptImportDiscoverySection-BE8Q8MLD.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-DOtWTD9n.js → PromptImportFolderSection-PtH5x0sc.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-BMjQegRa.js → PromptLibraryPanel-FnM9tHI9.js} +2 -2
- package/ui/dist/assets/SettingsWorkspace-MARPGyBL.js +1 -0
- package/ui/dist/assets/index-BAFI6htx.js +42 -0
- package/ui/dist/assets/{index-31uVIdt4.js → index-BSXxr_Bt.js} +1 -1
- package/ui/dist/assets/index-DS-ADE7U.css +1 -0
- package/ui/dist/index.html +2 -2
- package/bin/commands/annotate.ts +0 -119
- package/bin/commands/cancel.ts +0 -48
- package/bin/commands/canvas-versions.ts +0 -80
- package/bin/commands/capabilities.ts +0 -110
- package/bin/commands/cardnews.ts +0 -249
- package/bin/commands/comfy.ts +0 -54
- package/bin/commands/config.ts +0 -186
- package/bin/commands/defaults.ts +0 -192
- package/bin/commands/doctor.ts +0 -202
- package/bin/commands/edit.ts +0 -150
- package/bin/commands/gen.ts +0 -214
- package/bin/commands/grok.ts +0 -90
- package/bin/commands/history.ts +0 -146
- package/bin/commands/ls.ts +0 -64
- package/bin/commands/metadata.ts +0 -39
- package/bin/commands/multimode.ts +0 -196
- package/bin/commands/node.ts +0 -166
- package/bin/commands/observability.ts +0 -176
- package/bin/commands/ping.ts +0 -31
- package/bin/commands/prompt-sub/build.ts +0 -101
- package/bin/commands/prompt.ts +0 -492
- package/bin/commands/ps.ts +0 -81
- package/bin/commands/session.ts +0 -266
- package/bin/commands/show.ts +0 -72
- package/bin/commands/skill.ts +0 -70
- package/bin/commands/video.ts +0 -442
- package/bin/ima2.ts +0 -430
- package/bin/lib/args.ts +0 -92
- package/bin/lib/browser-id.ts +0 -16
- package/bin/lib/client.ts +0 -122
- package/bin/lib/config-store.ts +0 -120
- package/bin/lib/destructive-confirm.ts +0 -19
- package/bin/lib/doctor-checks.ts +0 -91
- package/bin/lib/error-hints.ts +0 -23
- package/bin/lib/files.ts +0 -39
- package/bin/lib/output.ts +0 -73
- package/bin/lib/platform.ts +0 -99
- package/bin/lib/recover-output.ts +0 -139
- package/bin/lib/sse.ts +0 -73
- package/bin/lib/star-prompt.ts +0 -97
- package/bin/lib/storage-doctor.ts +0 -39
- package/bin/lib/ui-build.ts +0 -85
- package/config.ts +0 -354
- package/lib/agentCommandParser.ts +0 -69
- package/lib/agentGenerationPlanner.ts +0 -273
- package/lib/agentQuestionResponder.ts +0 -266
- package/lib/agentQueueStore.ts +0 -270
- package/lib/agentQueueWorker.ts +0 -89
- package/lib/agentRuntime.ts +0 -604
- package/lib/agentSettings.ts +0 -72
- package/lib/agentStore.ts +0 -422
- package/lib/agentStoreRows.ts +0 -136
- package/lib/agentTypes.ts +0 -154
- package/lib/apiCachePolicy.ts +0 -11
- package/lib/assetLifecycle.ts +0 -146
- package/lib/canvasVersionStore.ts +0 -223
- package/lib/capabilities.ts +0 -126
- package/lib/cardNewsGenerator.ts +0 -271
- package/lib/cardNewsJobStore.ts +0 -142
- package/lib/cardNewsManifestStore.ts +0 -154
- package/lib/cardNewsPlanner.ts +0 -236
- package/lib/cardNewsPlannerClient.ts +0 -155
- package/lib/cardNewsPlannerPrompt.ts +0 -62
- package/lib/cardNewsPlannerSchema.ts +0 -321
- package/lib/cardNewsRoleTemplateStore.ts +0 -47
- package/lib/cardNewsTemplateStore.ts +0 -252
- package/lib/codexDetect.ts +0 -71
- package/lib/comfyBridge.ts +0 -235
- package/lib/composerSnapshot.ts +0 -33
- package/lib/configKeys.ts +0 -62
- package/lib/db.ts +0 -295
- package/lib/errInfo.ts +0 -43
- package/lib/errorClassify.ts +0 -100
- package/lib/generationCancel.ts +0 -28
- package/lib/generationErrors.ts +0 -238
- package/lib/grokImageAdapter.ts +0 -513
- package/lib/grokMultimodeAdapter.ts +0 -84
- package/lib/grokProxyLauncher.ts +0 -153
- package/lib/grokRuntime.ts +0 -23
- package/lib/grokSizeMapper.ts +0 -71
- package/lib/grokVideoAdapter.ts +0 -458
- package/lib/grokVideoCanvas.ts +0 -26
- package/lib/grokVideoDownload.ts +0 -59
- package/lib/grokVideoPlannerPrompt.ts +0 -67
- package/lib/historyIndex.ts +0 -51
- package/lib/historyList.ts +0 -181
- package/lib/imageMetadata.ts +0 -113
- package/lib/imageMetadataStore.ts +0 -67
- package/lib/imageModels.ts +0 -165
- package/lib/inflight.ts +0 -281
- package/lib/localImportStore.ts +0 -114
- package/lib/logger.ts +0 -161
- package/lib/nodeStore.ts +0 -91
- package/lib/oauthLauncher.ts +0 -94
- package/lib/oauthNormalize.ts +0 -30
- package/lib/oauthProxy/errors.ts +0 -128
- package/lib/oauthProxy/generators.ts +0 -494
- package/lib/oauthProxy/index.ts +0 -28
- package/lib/oauthProxy/prompts.ts +0 -123
- package/lib/oauthProxy/references.ts +0 -45
- package/lib/oauthProxy/runtime.ts +0 -115
- package/lib/oauthProxy/streams.ts +0 -232
- package/lib/oauthProxy/types.ts +0 -9
- package/lib/oauthProxy.ts +0 -3
- package/lib/openDirectory.ts +0 -47
- package/lib/pngInfo.ts +0 -26
- package/lib/promptBuilder/attachments.ts +0 -74
- package/lib/promptBuilder/client.ts +0 -130
- package/lib/promptBuilder/constants.ts +0 -9
- package/lib/promptBuilder/context.ts +0 -36
- package/lib/promptBuilder/errors.ts +0 -12
- package/lib/promptBuilder/requestSchema.ts +0 -56
- package/lib/promptBuilder/responseParser.ts +0 -219
- package/lib/promptBuilder/systemPrompt.ts +0 -135
- package/lib/promptBuilder/transport.ts +0 -94
- package/lib/promptBuilder/types.ts +0 -109
- package/lib/promptImport/curatedSources.ts +0 -141
- package/lib/promptImport/discoveryRegistry.ts +0 -329
- package/lib/promptImport/errors.ts +0 -18
- package/lib/promptImport/githubDiscovery.ts +0 -309
- package/lib/promptImport/githubFolder.ts +0 -397
- package/lib/promptImport/githubSource.ts +0 -257
- package/lib/promptImport/gptImageHints.ts +0 -70
- package/lib/promptImport/parsePromptCandidates.ts +0 -179
- package/lib/promptImport/promptIndex.ts +0 -326
- package/lib/promptImport/rankPromptCandidates.ts +0 -65
- package/lib/promptImport/types.ts +0 -103
- package/lib/promptSafetyPolicy.ts +0 -5
- package/lib/providerOptions.ts +0 -56
- package/lib/referenceImageCompress.ts +0 -84
- package/lib/refs.ts +0 -133
- package/lib/requestLogger.ts +0 -49
- package/lib/responsesDoctor.ts +0 -456
- package/lib/responsesErrors.ts +0 -83
- package/lib/responsesFallback.ts +0 -114
- package/lib/responsesImageAdapter.ts +0 -466
- package/lib/responsesParse.ts +0 -452
- package/lib/responsesTools.ts +0 -28
- package/lib/runtimeContext.ts +0 -146
- package/lib/runtimePorts.ts +0 -105
- package/lib/sessionStore.ts +0 -308
- package/lib/storageMigration.ts +0 -310
- package/lib/styleSheet.ts +0 -139
- package/lib/systemTrash.ts +0 -20
- package/lib/videoContinuity.ts +0 -180
- package/lib/videoFrameExtract.ts +0 -78
- package/lib/videoSeriesChain.ts +0 -29
- package/lib/visibleTextLanguagePolicy.ts +0 -7
- package/routes/agent.ts +0 -308
- package/routes/annotations.ts +0 -118
- package/routes/canvasVersions.ts +0 -69
- package/routes/capabilities.ts +0 -18
- package/routes/cardNews.ts +0 -211
- package/routes/comfy.ts +0 -43
- package/routes/edit.ts +0 -352
- package/routes/generate.ts +0 -492
- package/routes/grok.ts +0 -24
- package/routes/health.ts +0 -123
- package/routes/history.ts +0 -221
- package/routes/imageImport.ts +0 -37
- package/routes/index.ts +0 -52
- package/routes/metadata.ts +0 -77
- package/routes/multimode.ts +0 -499
- package/routes/nodes.ts +0 -578
- package/routes/promptBuilder.ts +0 -37
- package/routes/promptImport.ts +0 -379
- package/routes/prompts.ts +0 -428
- package/routes/quota.ts +0 -89
- package/routes/sessions.ts +0 -317
- package/routes/storage.ts +0 -47
- package/routes/video.ts +0 -300
- package/routes/videoExtended.ts +0 -284
- package/server.ts +0 -293
- package/ui/dist/assets/SettingsWorkspace-PiaVnsdA.js +0 -1
- package/ui/dist/assets/index-CjgnNtgt.css +0 -1
- package/ui/dist/assets/index-Da2s4_-5.js +0 -36
package/lib/storageMigration.ts
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import { mkdir, readdir, copyFile, stat, constants } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
|
|
5
|
-
const PACKAGE_NAME = "ima2-gen";
|
|
6
|
-
const RECOVERY_DOCS_PATH = "docs/RECOVER_OLD_IMAGES.md";
|
|
7
|
-
|
|
8
|
-
interface CopyStats { copied: number; skippedExisting: number; }
|
|
9
|
-
|
|
10
|
-
function addStats(a: CopyStats, b: CopyStats): CopyStats {
|
|
11
|
-
return {
|
|
12
|
-
copied: a.copied + b.copied,
|
|
13
|
-
skippedExisting: a.skippedExisting + b.skippedExisting,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function copyMissingTree(srcDir: string, dstDir: string): Promise<CopyStats> {
|
|
18
|
-
await mkdir(dstDir, { recursive: true });
|
|
19
|
-
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
20
|
-
let stats: CopyStats = { copied: 0, skippedExisting: 0 };
|
|
21
|
-
for (const entry of entries) {
|
|
22
|
-
const src = join(srcDir, entry.name);
|
|
23
|
-
const dst = join(dstDir, entry.name);
|
|
24
|
-
if (entry.isDirectory()) {
|
|
25
|
-
stats = addStats(stats, await copyMissingTree(src, dst));
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (!entry.isFile()) continue;
|
|
29
|
-
try {
|
|
30
|
-
await copyFile(src, dst, constants.COPYFILE_EXCL);
|
|
31
|
-
stats.copied += 1;
|
|
32
|
-
} catch (err) {
|
|
33
|
-
if ((err as { code?: string })?.code !== "EEXIST") throw err;
|
|
34
|
-
stats.skippedExisting += 1;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return stats;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function isSameOrInside(child: string, parent: string): boolean {
|
|
41
|
-
const a = resolve(child);
|
|
42
|
-
const b = resolve(parent);
|
|
43
|
-
return a === b || a.startsWith(b + sep);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
interface StorageCtx {
|
|
47
|
-
rootDir?: string;
|
|
48
|
-
config?: { storage?: { generatedDir?: string } };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function resolveTargetDir(ctx: StorageCtx): string {
|
|
52
|
-
return ctx.config?.storage?.generatedDir ?? join(ctx.rootDir ?? process.cwd(), "generated");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type EnvLike = NodeJS.ProcessEnv;
|
|
56
|
-
|
|
57
|
-
interface MigrateOptions {
|
|
58
|
-
legacyDirs?: string[];
|
|
59
|
-
env?: EnvLike;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function migrateGeneratedStorage(ctx: StorageCtx, options: MigrateOptions = {}) {
|
|
63
|
-
const targetDir = resolveTargetDir(ctx);
|
|
64
|
-
const candidates = options.legacyDirs || await getLegacyGeneratedCandidates(ctx, options.env);
|
|
65
|
-
const result = {
|
|
66
|
-
copied: 0,
|
|
67
|
-
skippedExisting: 0,
|
|
68
|
-
sourcesScanned: 0,
|
|
69
|
-
sourcesSkipped: 0,
|
|
70
|
-
};
|
|
71
|
-
try {
|
|
72
|
-
for (const legacyDir of candidates) {
|
|
73
|
-
if (isSameOrInside(legacyDir, targetDir) || isSameOrInside(targetDir, legacyDir)) {
|
|
74
|
-
result.sourcesSkipped += 1;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const legacyStat = await stat(legacyDir);
|
|
79
|
-
if (!legacyStat.isDirectory()) continue;
|
|
80
|
-
result.sourcesScanned += 1;
|
|
81
|
-
const copyStats = await copyMissingTree(legacyDir, targetDir);
|
|
82
|
-
result.copied += copyStats.copied;
|
|
83
|
-
result.skippedExisting += copyStats.skippedExisting;
|
|
84
|
-
} catch (err) {
|
|
85
|
-
const e = err as { code?: string; message?: string };
|
|
86
|
-
if (e?.code !== "ENOENT") {
|
|
87
|
-
console.warn("[storage] generated asset migration source skipped:", legacyDir, e.message);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (result.copied > 0) console.log(`[storage] migrated ${result.copied} generated assets to ${targetDir}`);
|
|
92
|
-
} catch (err) {
|
|
93
|
-
console.warn("[storage] generated asset migration skipped:", (err as { message?: string })?.message);
|
|
94
|
-
}
|
|
95
|
-
return result;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function getLegacyGeneratedCandidates(ctx: StorageCtx, env: EnvLike = process.env): Promise<string[]> {
|
|
99
|
-
const home = env.IMA2_TEST_HOME || homedir();
|
|
100
|
-
const execPath = env.IMA2_TEST_EXEC_PATH || process.execPath;
|
|
101
|
-
const argv1 = env.IMA2_TEST_ARGV1 || process.argv[1] || "";
|
|
102
|
-
const nodePrefix = dirname(dirname(execPath));
|
|
103
|
-
const prefixes = getGlobalPrefixCandidates({ env, execPath, argv1 });
|
|
104
|
-
const appData = env.APPDATA || join(home, "AppData", "Roaming");
|
|
105
|
-
const localAppData = env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
106
|
-
const npmCache = env.npm_config_cache || join(home, ".npm");
|
|
107
|
-
const xdgDataHome = env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
108
|
-
const pnpmHome = env.PNPM_HOME || "";
|
|
109
|
-
const nvmHome = env.NVM_HOME || join(appData, "nvm");
|
|
110
|
-
|
|
111
|
-
const candidates = [
|
|
112
|
-
join(ctx.rootDir ?? process.cwd(), "generated"),
|
|
113
|
-
join(appData, "npm", "node_modules", PACKAGE_NAME, "generated"),
|
|
114
|
-
join(home, ".npm-global", "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
115
|
-
join(home, ".nvm", "versions", "node", process.version, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
116
|
-
join(home, ".volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
117
|
-
join(home, ".fnm", "node-versions", process.version, "installation", "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
118
|
-
join(home, ".bun", "install", "global", "node_modules", PACKAGE_NAME, "generated"),
|
|
119
|
-
join(home, ".config", "yarn", "global", "node_modules", PACKAGE_NAME, "generated"),
|
|
120
|
-
join(localAppData, "Yarn", "Data", "global", "node_modules", PACKAGE_NAME, "generated"),
|
|
121
|
-
join(localAppData, "Volta", "tools", "image", "packages", PACKAGE_NAME, "lib", "node_modules", PACKAGE_NAME, "generated"),
|
|
122
|
-
join(nvmHome, process.version, "node_modules", PACKAGE_NAME, "generated"),
|
|
123
|
-
join(dirname(execPath), "node_modules", PACKAGE_NAME, "generated"),
|
|
124
|
-
];
|
|
125
|
-
|
|
126
|
-
for (const prefix of prefixes) {
|
|
127
|
-
candidates.push(join(prefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
|
|
128
|
-
candidates.push(join(prefix, "node_modules", PACKAGE_NAME, "generated"));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
candidates.push(join(nodePrefix, "lib", "node_modules", PACKAGE_NAME, "generated"));
|
|
132
|
-
candidates.push(
|
|
133
|
-
...await expandOneLevelCandidates([
|
|
134
|
-
[join(home, ".nvm", "versions", "node"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
|
|
135
|
-
[join(home, ".fnm", "node-versions"), ["*", "installation", "lib", "node_modules", PACKAGE_NAME, "generated"]],
|
|
136
|
-
[join(home, ".asdf", "installs", "nodejs"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
|
|
137
|
-
[join(home, ".local", "share", "mise", "installs", "node"), ["*", "lib", "node_modules", PACKAGE_NAME, "generated"]],
|
|
138
|
-
[join(home, "Library", "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
139
|
-
[join(xdgDataHome, "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
140
|
-
[join(localAppData, "pnpm", "global"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
141
|
-
[pnpmHome ? join(pnpmHome, "global") : "", ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
142
|
-
[join(npmCache, "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
143
|
-
[join(localAppData, "npm-cache", "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
144
|
-
[join(appData, "npm-cache", "_npx"), ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
145
|
-
[nvmHome, ["*", "node_modules", PACKAGE_NAME, "generated"]],
|
|
146
|
-
]),
|
|
147
|
-
);
|
|
148
|
-
return uniqueResolvedCandidates(candidates);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function inspectGeneratedStorage(ctx: StorageCtx, options: MigrateOptions = {}) {
|
|
152
|
-
const env = options.env || process.env;
|
|
153
|
-
const targetDir = resolveTargetDir(ctx);
|
|
154
|
-
try {
|
|
155
|
-
const candidates = options.legacyDirs || await getLegacyGeneratedCandidates(ctx, env);
|
|
156
|
-
const targetFileCount = await countFiles(targetDir);
|
|
157
|
-
const legacySources: Array<{ path: string; fileCount: number }> = [];
|
|
158
|
-
|
|
159
|
-
for (const candidate of candidates) {
|
|
160
|
-
if (isSameOrInside(candidate, targetDir) || isSameOrInside(targetDir, candidate)) continue;
|
|
161
|
-
try {
|
|
162
|
-
const candidateStat = await stat(candidate);
|
|
163
|
-
if (!candidateStat.isDirectory()) continue;
|
|
164
|
-
const fileCount = await countFiles(candidate);
|
|
165
|
-
if (fileCount > 0) legacySources.push({ path: candidate, fileCount });
|
|
166
|
-
} catch (err) {
|
|
167
|
-
const e = err as { code?: string; message?: string };
|
|
168
|
-
if (e?.code !== "ENOENT") {
|
|
169
|
-
console.warn("[storage] legacy candidate inspect skipped:", candidate, e.message);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const legacyFilesFound = legacySources.reduce((sum, source) => sum + source.fileCount, 0);
|
|
175
|
-
const state =
|
|
176
|
-
targetFileCount > 0 ? "ok"
|
|
177
|
-
: legacyFilesFound > 0 ? "recoverable"
|
|
178
|
-
: "not_found";
|
|
179
|
-
|
|
180
|
-
return {
|
|
181
|
-
ok: true,
|
|
182
|
-
targetDir,
|
|
183
|
-
generatedDirLabel: labelPath(targetDir, env),
|
|
184
|
-
targetExists: await isDirectory(targetDir),
|
|
185
|
-
targetFileCount,
|
|
186
|
-
legacyCandidatesScanned: candidates.length,
|
|
187
|
-
legacySourcesFound: legacySources.length,
|
|
188
|
-
legacyFilesFound,
|
|
189
|
-
legacySources,
|
|
190
|
-
overrides: {
|
|
191
|
-
generatedDir: Boolean(env.IMA2_GENERATED_DIR),
|
|
192
|
-
configDir: Boolean(env.IMA2_CONFIG_DIR),
|
|
193
|
-
},
|
|
194
|
-
state,
|
|
195
|
-
messageKind: state === "not_found" ? "apology" : state,
|
|
196
|
-
recoveryDocsPath: RECOVERY_DOCS_PATH,
|
|
197
|
-
doctorCommand: "ima2 doctor",
|
|
198
|
-
};
|
|
199
|
-
} catch (err) {
|
|
200
|
-
const e = err as { message?: string };
|
|
201
|
-
return {
|
|
202
|
-
ok: false,
|
|
203
|
-
targetDir,
|
|
204
|
-
generatedDirLabel: labelPath(targetDir, env),
|
|
205
|
-
targetExists: false,
|
|
206
|
-
targetFileCount: 0,
|
|
207
|
-
legacyCandidatesScanned: 0,
|
|
208
|
-
legacySourcesFound: 0,
|
|
209
|
-
legacyFilesFound: 0,
|
|
210
|
-
legacySources: [],
|
|
211
|
-
overrides: {
|
|
212
|
-
generatedDir: Boolean(env.IMA2_GENERATED_DIR),
|
|
213
|
-
configDir: Boolean(env.IMA2_CONFIG_DIR),
|
|
214
|
-
},
|
|
215
|
-
state: "unknown",
|
|
216
|
-
messageKind: "unknown",
|
|
217
|
-
recoveryDocsPath: RECOVERY_DOCS_PATH,
|
|
218
|
-
doctorCommand: "ima2 doctor",
|
|
219
|
-
error: e?.message || String(err),
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
type ExpandPattern = [string, string[]];
|
|
225
|
-
|
|
226
|
-
async function expandOneLevelCandidates(patterns: ExpandPattern[]): Promise<string[]> {
|
|
227
|
-
const candidates: string[] = [];
|
|
228
|
-
for (const [baseDir, segments] of patterns) {
|
|
229
|
-
if (!baseDir) continue;
|
|
230
|
-
candidates.push(...await expandOneLevelPattern(baseDir, segments));
|
|
231
|
-
}
|
|
232
|
-
return candidates;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async function expandOneLevelPattern(baseDir: string, segments: string[]): Promise<string[]> {
|
|
236
|
-
const wildcardIndex = segments.indexOf("*");
|
|
237
|
-
if (wildcardIndex < 0) return [join(baseDir, ...segments)];
|
|
238
|
-
|
|
239
|
-
const before = segments.slice(0, wildcardIndex);
|
|
240
|
-
const after = segments.slice(wildcardIndex + 1);
|
|
241
|
-
const wildcardBase = join(baseDir, ...before);
|
|
242
|
-
try {
|
|
243
|
-
const entries = await readdir(wildcardBase, { withFileTypes: true });
|
|
244
|
-
return entries
|
|
245
|
-
.filter((entry) => entry.isDirectory())
|
|
246
|
-
.map((entry) => join(wildcardBase, entry.name, ...after));
|
|
247
|
-
} catch (err) {
|
|
248
|
-
const e = err as { code?: string; message?: string };
|
|
249
|
-
if (e?.code === "ENOENT") return [];
|
|
250
|
-
console.warn("[storage] legacy candidate scan skipped:", wildcardBase, e.message);
|
|
251
|
-
return [];
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async function countFiles(dir: string): Promise<number> {
|
|
256
|
-
try {
|
|
257
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
258
|
-
let count = 0;
|
|
259
|
-
for (const entry of entries) {
|
|
260
|
-
if (entry.name === ".trash") continue;
|
|
261
|
-
const fullPath = join(dir, entry.name);
|
|
262
|
-
if (entry.isDirectory()) count += await countFiles(fullPath);
|
|
263
|
-
else if (entry.isFile()) count += 1;
|
|
264
|
-
}
|
|
265
|
-
return count;
|
|
266
|
-
} catch (err) {
|
|
267
|
-
if ((err as { code?: string })?.code === "ENOENT") return 0;
|
|
268
|
-
throw err;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
async function isDirectory(dir: string): Promise<boolean> {
|
|
273
|
-
try {
|
|
274
|
-
return (await stat(dir)).isDirectory();
|
|
275
|
-
} catch {
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function uniqueResolvedCandidates(candidates: string[]): string[] {
|
|
281
|
-
return Array.from(new Set(candidates.filter(Boolean).map((p) => resolve(p))));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function labelPath(targetPath: string, env: EnvLike = process.env): string {
|
|
285
|
-
const home = env.IMA2_TEST_HOME || homedir();
|
|
286
|
-
const resolved = resolve(targetPath);
|
|
287
|
-
const resolvedHome = resolve(home);
|
|
288
|
-
if (resolved === resolvedHome) return "~";
|
|
289
|
-
if (resolved.startsWith(resolvedHome + sep)) return `~${sep}${resolved.slice(resolvedHome.length + 1)}`;
|
|
290
|
-
return resolved;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
interface PrefixOpts { env: EnvLike; execPath: string; argv1: string; }
|
|
294
|
-
|
|
295
|
-
function getGlobalPrefixCandidates({ env, execPath, argv1 }: PrefixOpts): string[] {
|
|
296
|
-
const prefixes: Set<string> = new Set();
|
|
297
|
-
if (env.npm_config_prefix) prefixes.add(env.npm_config_prefix);
|
|
298
|
-
if (isAbsolute(argv1)) prefixes.add(dirname(dirname(argv1)));
|
|
299
|
-
prefixes.add(dirname(dirname(execPath)));
|
|
300
|
-
addHomebrewPrefix(prefixes, execPath);
|
|
301
|
-
prefixes.add("/opt/homebrew");
|
|
302
|
-
prefixes.add("/usr/local");
|
|
303
|
-
return Array.from(prefixes);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function addHomebrewPrefix(prefixes: Set<string>, execPath: string): void {
|
|
307
|
-
const marker = `${sep}Cellar${sep}node`;
|
|
308
|
-
const idx = execPath.indexOf(marker);
|
|
309
|
-
if (idx > 0) prefixes.add(execPath.slice(0, idx));
|
|
310
|
-
}
|
package/lib/styleSheet.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// Style-sheet extractor (0.10)
|
|
2
|
-
//
|
|
3
|
-
// Uses GPT-5.4 (chat completions) to derive a structured "style guide" from a
|
|
4
|
-
// user prompt and optional reference image. The style guide is stored per
|
|
5
|
-
// session and automatically prepended to subsequent image generations so
|
|
6
|
-
// continuations feel cohesive — closer to ChatGPT 4o's image carry-over.
|
|
7
|
-
//
|
|
8
|
-
// Shape:
|
|
9
|
-
// {
|
|
10
|
-
// palette: string[], // e.g. ["deep navy", "gold leaf"]
|
|
11
|
-
// composition: string, // e.g. "centered 3/4 portrait, shallow depth"
|
|
12
|
-
// mood: string, // e.g. "melancholic, reverent"
|
|
13
|
-
// medium: string, // e.g. "oil painting, glazed layers"
|
|
14
|
-
// subject_details: string, // identity/pose/outfit cues for character continuity
|
|
15
|
-
// negative: string[] // things to avoid
|
|
16
|
-
// }
|
|
17
|
-
//
|
|
18
|
-
// The module is pure JS + openai SDK. When no API key is configured it throws
|
|
19
|
-
// STYLE_SHEET_NO_KEY so callers can surface a friendly "connect key" UI.
|
|
20
|
-
|
|
21
|
-
import type OpenAI from "openai";
|
|
22
|
-
import { config } from "../config.js";
|
|
23
|
-
const STYLE_SHEET_MODEL = config.styleSheet.model;
|
|
24
|
-
|
|
25
|
-
const SYSTEM_PROMPT = `You extract a reusable visual style guide from a user
|
|
26
|
-
image prompt (and an optional reference image). Return ONLY a JSON object with
|
|
27
|
-
these keys: palette (array of 3-6 concrete color names), composition (one
|
|
28
|
-
sentence), mood (2-4 comma-separated adjectives), medium (one short phrase
|
|
29
|
-
naming technique/material), subject_details (one sentence capturing identity
|
|
30
|
-
cues: face, outfit, pose, distinctive features), negative (array of 0-4 short
|
|
31
|
-
phrases of things to avoid). Keep entries tight — each under 120 characters.
|
|
32
|
-
Do not wrap in markdown. Do not add commentary.`;
|
|
33
|
-
|
|
34
|
-
interface StyleSheet {
|
|
35
|
-
palette: string[];
|
|
36
|
-
composition: string;
|
|
37
|
-
mood: string;
|
|
38
|
-
medium: string;
|
|
39
|
-
subject_details: string;
|
|
40
|
-
negative: string[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function coerceStyleSheet(raw: unknown): StyleSheet | null {
|
|
44
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
45
|
-
const arr = (v: unknown, max = 6) =>
|
|
46
|
-
Array.isArray(v)
|
|
47
|
-
? v
|
|
48
|
-
.filter((x: unknown) => typeof x === "string" && x.trim())
|
|
49
|
-
.slice(0, max)
|
|
50
|
-
.map((s: string) => s.trim())
|
|
51
|
-
: [];
|
|
52
|
-
const str = (v: unknown) => (typeof v === "string" ? v.trim().slice(0, 400) : "");
|
|
53
|
-
const r = raw as Record<string, unknown>;
|
|
54
|
-
const sheet: StyleSheet = {
|
|
55
|
-
palette: arr(r.palette, 6),
|
|
56
|
-
composition: str(r.composition),
|
|
57
|
-
mood: str(r.mood),
|
|
58
|
-
medium: str(r.medium),
|
|
59
|
-
subject_details: str(r.subject_details),
|
|
60
|
-
negative: arr(r.negative, 4),
|
|
61
|
-
};
|
|
62
|
-
const hasContent =
|
|
63
|
-
sheet.palette.length > 0 ||
|
|
64
|
-
sheet.negative.length > 0 ||
|
|
65
|
-
sheet.composition ||
|
|
66
|
-
sheet.mood ||
|
|
67
|
-
sheet.medium ||
|
|
68
|
-
sheet.subject_details;
|
|
69
|
-
return hasContent ? sheet : null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function extractStyleSheet(openai: OpenAI | null | undefined, { prompt, referenceDataUrl }: { prompt: string; referenceDataUrl?: string }) {
|
|
73
|
-
if (!openai) {
|
|
74
|
-
const err = new Error("No OpenAI client configured for style-sheet extraction") as Error & { code?: string };
|
|
75
|
-
err.code = "STYLE_SHEET_NO_KEY";
|
|
76
|
-
throw err;
|
|
77
|
-
}
|
|
78
|
-
if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
|
|
79
|
-
const err = new Error("prompt is required") as Error & { code?: string };
|
|
80
|
-
err.code = "STYLE_SHEET_BAD_INPUT";
|
|
81
|
-
throw err;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const userContent = referenceDataUrl
|
|
85
|
-
? [
|
|
86
|
-
{ type: "text" as const, text: prompt },
|
|
87
|
-
{ type: "image_url" as const, image_url: { url: referenceDataUrl } },
|
|
88
|
-
]
|
|
89
|
-
: prompt;
|
|
90
|
-
|
|
91
|
-
const resp = await openai.chat.completions.create({
|
|
92
|
-
model: STYLE_SHEET_MODEL,
|
|
93
|
-
response_format: { type: "json_object" },
|
|
94
|
-
messages: [
|
|
95
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
96
|
-
{ role: "user", content: userContent },
|
|
97
|
-
],
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const raw = resp.choices?.[0]?.message?.content;
|
|
101
|
-
if (!raw) {
|
|
102
|
-
const err = new Error("Empty response from style-sheet model") as Error & { code?: string };
|
|
103
|
-
err.code = "STYLE_SHEET_EMPTY";
|
|
104
|
-
throw err;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
let parsed: unknown;
|
|
108
|
-
try {
|
|
109
|
-
parsed = JSON.parse(raw);
|
|
110
|
-
} catch {
|
|
111
|
-
const err = new Error("Style-sheet model returned non-JSON") as Error & { code?: string };
|
|
112
|
-
err.code = "STYLE_SHEET_PARSE";
|
|
113
|
-
throw err;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const sheet = coerceStyleSheet(parsed);
|
|
117
|
-
if (!sheet) {
|
|
118
|
-
const err = new Error("Style-sheet shape invalid") as Error & { code?: string };
|
|
119
|
-
err.code = "STYLE_SHEET_SHAPE";
|
|
120
|
-
throw err;
|
|
121
|
-
}
|
|
122
|
-
return sheet;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Render a style sheet into a prompt preamble that gpt-image-1/2 can consume.
|
|
126
|
-
// Kept short so it doesn't blow the 4K prompt window on long user prompts.
|
|
127
|
-
export function renderStyleSheetPrefix(sheet: StyleSheet | null | undefined) {
|
|
128
|
-
if (!sheet) return "";
|
|
129
|
-
const parts: string[] = [];
|
|
130
|
-
if (sheet.medium) parts.push(`Medium: ${sheet.medium}.`);
|
|
131
|
-
if (sheet.palette?.length) parts.push(`Palette: ${sheet.palette.join(", ")}.`);
|
|
132
|
-
if (sheet.composition) parts.push(`Composition: ${sheet.composition}.`);
|
|
133
|
-
if (sheet.mood) parts.push(`Mood: ${sheet.mood}.`);
|
|
134
|
-
if (sheet.subject_details) parts.push(`Subject: ${sheet.subject_details}.`);
|
|
135
|
-
if (sheet.negative?.length) parts.push(`Avoid: ${sheet.negative.join(", ")}.`);
|
|
136
|
-
return parts.join(" ");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export { STYLE_SHEET_MODEL, SYSTEM_PROMPT, coerceStyleSheet };
|
package/lib/systemTrash.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { mkdir, rename } from "fs/promises";
|
|
2
|
-
import { basename, join } from "path";
|
|
3
|
-
import trash from "trash";
|
|
4
|
-
|
|
5
|
-
export async function moveToSystemTrash(paths: string[]): Promise<void> {
|
|
6
|
-
if (process.env.NODE_ENV === "test" && process.env.IMA2_TEST_SYSTEM_TRASH_DIR) {
|
|
7
|
-
if (process.env.IMA2_TEST_SYSTEM_TRASH_FAIL === "1") {
|
|
8
|
-
throw new Error("Simulated system trash failure");
|
|
9
|
-
}
|
|
10
|
-
const targetDir = process.env.IMA2_TEST_SYSTEM_TRASH_DIR;
|
|
11
|
-
await mkdir(targetDir, { recursive: true });
|
|
12
|
-
const prefix = `${Date.now()}_`;
|
|
13
|
-
for (const path of paths) {
|
|
14
|
-
await rename(path, join(targetDir, `${prefix}${basename(path)}`));
|
|
15
|
-
}
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
await trash(paths, { glob: false });
|
|
20
|
-
}
|
package/lib/videoContinuity.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
|
|
4
|
-
export const ACTIVE_VIDEO_PROMPT_GUIDANCE = [
|
|
5
|
-
"Active video prompt required.",
|
|
6
|
-
"Describe visual flow, motion flow, sound or no-music intent, dialogue or no-dialogue intent, and the desired ending frame.",
|
|
7
|
-
"Pace the scene to naturally fill the selected duration, expanding even short requests into an opening composition, connected motion/emotion change, and stable ending frame.",
|
|
8
|
-
"Example: From the attached last frame, the subject turns toward camera, rain sound rises, no background music, one whispered line finishes before a still close-up ending.",
|
|
9
|
-
].join(" ");
|
|
10
|
-
|
|
11
|
-
export type VideoContinuityEntry = {
|
|
12
|
-
id: string;
|
|
13
|
-
ordinal: number;
|
|
14
|
-
role: "start" | "ancestor" | "parent" | "current";
|
|
15
|
-
filename: string | null;
|
|
16
|
-
userPrompt: string | null;
|
|
17
|
-
revisedPrompt: string;
|
|
18
|
-
createdAt: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type VideoContinuityLineage = {
|
|
22
|
-
lineageId: string;
|
|
23
|
-
parentFilename: string | null;
|
|
24
|
-
sourceFrame: "last" | null;
|
|
25
|
-
maxEntries: 4;
|
|
26
|
-
retention: "keep-start-plus-latest-3";
|
|
27
|
-
entries: VideoContinuityEntry[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type VideoMeta = {
|
|
31
|
-
prompt?: unknown;
|
|
32
|
-
userPrompt?: unknown;
|
|
33
|
-
revisedPrompt?: unknown;
|
|
34
|
-
createdAt?: unknown;
|
|
35
|
-
videoContinuity?: unknown;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export function requireActiveVideoPrompt(value: unknown): string | null {
|
|
39
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function safeGeneratedVideoFilename(value: unknown): string {
|
|
43
|
-
if (typeof value !== "string" || !value.trim()) throw Object.assign(new Error("video filename required"), { status: 400 });
|
|
44
|
-
const clean = value.replace(/^\/generated\//, "").replace(/^\/+/, "");
|
|
45
|
-
if (clean.includes("..") || clean.includes("/") || clean.includes("\\")) {
|
|
46
|
-
throw Object.assign(new Error("invalid video filename"), { status: 400 });
|
|
47
|
-
}
|
|
48
|
-
if (!/\.mp4$/i.test(clean)) throw Object.assign(new Error("generated video input must be an .mp4 file"), { status: 400 });
|
|
49
|
-
return clean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function readVideoSidecar(generatedDir: string, filename: string): Promise<VideoMeta | null> {
|
|
53
|
-
const safe = safeGeneratedVideoFilename(filename);
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(await readFile(join(generatedDir, `${safe}.json`), "utf-8")) as VideoMeta;
|
|
56
|
-
} catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function stringOrNull(value: unknown): string | null {
|
|
62
|
-
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function numberOrNow(value: unknown): number {
|
|
66
|
-
return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function entryFromMeta(filename: string, meta: VideoMeta | null): VideoContinuityEntry | null {
|
|
70
|
-
const revisedPrompt = stringOrNull(meta?.revisedPrompt) ?? stringOrNull(meta?.prompt);
|
|
71
|
-
if (!revisedPrompt) return null;
|
|
72
|
-
return {
|
|
73
|
-
id: `clip:${filename}`,
|
|
74
|
-
ordinal: 1,
|
|
75
|
-
role: "start",
|
|
76
|
-
filename,
|
|
77
|
-
userPrompt: stringOrNull(meta?.userPrompt) ?? stringOrNull(meta?.prompt),
|
|
78
|
-
revisedPrompt,
|
|
79
|
-
createdAt: numberOrNow(meta?.createdAt),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function normalizeVideoContinuityLineage(value: unknown): VideoContinuityLineage | null {
|
|
84
|
-
if (!value || typeof value !== "object") return null;
|
|
85
|
-
const raw = value as Partial<VideoContinuityLineage>;
|
|
86
|
-
if (!Array.isArray(raw.entries)) return null;
|
|
87
|
-
const entries = raw.entries
|
|
88
|
-
.map((entry, index): VideoContinuityEntry | null => {
|
|
89
|
-
if (!entry || typeof entry !== "object") return null;
|
|
90
|
-
const e = entry as Partial<VideoContinuityEntry>;
|
|
91
|
-
const revisedPrompt = stringOrNull(e.revisedPrompt);
|
|
92
|
-
if (!revisedPrompt) return null;
|
|
93
|
-
return {
|
|
94
|
-
id: stringOrNull(e.id) ?? `entry:${index + 1}`,
|
|
95
|
-
ordinal: index + 1,
|
|
96
|
-
role: index === 0 ? "start" : index === raw.entries!.length - 1 ? "parent" : "ancestor",
|
|
97
|
-
filename: stringOrNull(e.filename),
|
|
98
|
-
userPrompt: stringOrNull(e.userPrompt),
|
|
99
|
-
revisedPrompt,
|
|
100
|
-
createdAt: numberOrNow(e.createdAt),
|
|
101
|
-
};
|
|
102
|
-
})
|
|
103
|
-
.filter((entry): entry is VideoContinuityEntry => Boolean(entry));
|
|
104
|
-
if (entries.length === 0) return null;
|
|
105
|
-
return {
|
|
106
|
-
lineageId: stringOrNull(raw.lineageId) ?? `lineage:${entries[0].id}`,
|
|
107
|
-
parentFilename: stringOrNull(raw.parentFilename),
|
|
108
|
-
sourceFrame: "last",
|
|
109
|
-
maxEntries: 4,
|
|
110
|
-
retention: "keep-start-plus-latest-3",
|
|
111
|
-
entries: trimLineageEntries(entries),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function trimLineageEntries(entries: VideoContinuityEntry[]): VideoContinuityEntry[] {
|
|
116
|
-
const kept = entries.length <= 4 ? entries : [entries[0], ...entries.slice(-3)];
|
|
117
|
-
return kept.map((entry, index) => ({
|
|
118
|
-
...entry,
|
|
119
|
-
ordinal: index + 1,
|
|
120
|
-
role: index === 0 ? "start" : index === kept.length - 1 ? entry.role : "ancestor",
|
|
121
|
-
}));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function lineageFromVideoMetadata(filename: string, meta: VideoMeta | null): VideoContinuityLineage | null {
|
|
125
|
-
const existing = normalizeVideoContinuityLineage(meta?.videoContinuity);
|
|
126
|
-
if (existing) {
|
|
127
|
-
return { ...existing, parentFilename: filename, sourceFrame: "last" };
|
|
128
|
-
}
|
|
129
|
-
const entry = entryFromMeta(filename, meta);
|
|
130
|
-
if (!entry) return null;
|
|
131
|
-
return {
|
|
132
|
-
lineageId: `lineage:${filename.replace(/\.[^.]+$/, "")}`,
|
|
133
|
-
parentFilename: filename,
|
|
134
|
-
sourceFrame: "last",
|
|
135
|
-
maxEntries: 4,
|
|
136
|
-
retention: "keep-start-plus-latest-3",
|
|
137
|
-
entries: [entry],
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function appendVideoContinuityEntry(
|
|
142
|
-
parent: VideoContinuityLineage | null,
|
|
143
|
-
current: { filename: string; userPrompt: string | null; revisedPrompt: string; createdAt?: number },
|
|
144
|
-
): VideoContinuityLineage {
|
|
145
|
-
const parentEntries = parent?.entries ?? [];
|
|
146
|
-
const lineageId = parent?.lineageId ?? `lineage:${current.filename.replace(/\.[^.]+$/, "")}`;
|
|
147
|
-
const currentEntry: VideoContinuityEntry = {
|
|
148
|
-
id: `clip:${current.filename}`,
|
|
149
|
-
ordinal: parentEntries.length + 1,
|
|
150
|
-
role: "current",
|
|
151
|
-
filename: current.filename,
|
|
152
|
-
userPrompt: current.userPrompt,
|
|
153
|
-
revisedPrompt: current.revisedPrompt,
|
|
154
|
-
createdAt: current.createdAt ?? Date.now(),
|
|
155
|
-
};
|
|
156
|
-
const entries = trimLineageEntries([...parentEntries.map((entry) => ({ ...entry, role: entry.role === "current" ? "parent" as const : entry.role })), currentEntry]);
|
|
157
|
-
return {
|
|
158
|
-
lineageId,
|
|
159
|
-
parentFilename: parent?.parentFilename ?? null,
|
|
160
|
-
sourceFrame: parent ? "last" : null,
|
|
161
|
-
maxEntries: 4,
|
|
162
|
-
retention: "keep-start-plus-latest-3",
|
|
163
|
-
entries,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function formatVideoContinuityForPlanner(lineage: VideoContinuityLineage | null | undefined): string {
|
|
168
|
-
if (!lineage?.entries?.length) return "";
|
|
169
|
-
const lines = [
|
|
170
|
-
"[Continuity lineage: branch-local, max 4 entries, start anchor preserved]",
|
|
171
|
-
...lineage.entries.map((entry) => [
|
|
172
|
-
`${entry.ordinal}. Clip ${entry.ordinal} / ${entry.role}`,
|
|
173
|
-
` file: ${entry.filename ? basename(entry.filename) : "unknown"}`,
|
|
174
|
-
` revisedPrompt: ${entry.revisedPrompt}`,
|
|
175
|
-
entry.userPrompt ? ` userPrompt: ${entry.userPrompt}` : null,
|
|
176
|
-
].filter(Boolean).join("\n")),
|
|
177
|
-
"Continue from the final frame and final action/audio state of the latest lineage item. Do not restart the scene.",
|
|
178
|
-
];
|
|
179
|
-
return lines.join("\n");
|
|
180
|
-
}
|